Sign in Start free trial

Pull your articles.
Publish them your way.

Every article we write for you can be read over HTTP: the title, the metadata, the Markdown, the HTML, the hero image and the FAQ. Take them into your Next.js, Astro or custom site when your build runs, put them live, and tell us the address each one ended up at.

One bearer token. JSON in, JSON out. No SDK to install and nothing to configure beyond a key.

The webhook pushes. The API lets you pull.

They carry the same article in the same shape, so a parser written for one reads the other unchanged. The difference is who starts the conversation.

The webhook, when your site can accept a post at any hour

We call your endpoint the moment an article is ready, and again when it changes. Nothing to poll, nothing to schedule. WordPress, Shopify, Ghost, a Zapier trigger, or a route you wrote yourself.

The API, when your site is built and deployed as a unit

A static site cannot accept a post at half past six in the morning; something has to rebuild first. So your build asks what is waiting, takes it, and tells us the address afterwards. You choose when.

Both, if you want them

They are separate connections and neither excludes the other. A webhook that feeds a newsletter and an API that feeds the website is a normal arrangement.

One key, sent as a bearer token.

Make a key in the dashboard under Settings, then Connections. It is shown once, when it is made, because only its hash is stored: if you lose it, revoke it and make another. A key reads every site on the account and is revoked from the same screen.

Every request

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Check that it works

curl https://seonis.ai/api/v1/me \
  -H "Authorization: Bearer $SEONIS_API_KEY"
{
  "organisation": { "id": 12, "name": "Peppervale", "country": "GB" },
  "key": {
    "name": "Production build",
    "prefix": "sns_live_ab",
    "created_at": "2026-09-06T09:12:44+00:00",
    "last_used_at": "2026-09-06T09:31:02+00:00"
  },
  "sites": 2
}

Rate limit

120 requests a minute, counted against the key rather than the address it comes from, because build runners share addresses. Over that you get 429 with the standard Retry-After and X-RateLimit headers. Pulling articles for a build is a handful of requests, so this is not a limit you should meet by accident.

Keep the key out of your repository

It reads everything we have written for your account, including articles that are not published yet. Put it in your build environment, not in your source. Revoke it here the moment it is anywhere it should not be, and anything using it gets a 401 immediately.

Five of them, and four only read.

Everything lives under /api/v1/. The version is in the path from the first day, so a v2 can exist one day without breaking what you write today.

GET /api/v1/me

The account the key belongs to, and the key’s own name and prefix. Useful for one thing above all: telling you that the key in this environment is the key you think it is.

Example

curl https://seonis.ai/api/v1/me \
  -H "Authorization: Bearer $SEONIS_API_KEY"
GET /api/v1/sites

Every site on the account. The id is what the article list filters on, and the domain is there so a build script can match on something it already knows rather than carry an id.

Example

curl https://seonis.ai/api/v1/sites \
  -H "Authorization: Bearer $SEONIS_API_KEY"
{
  "sites": [
    {
      "id": 5,
      "name": "Peppervale",
      "domain": "peppervale.co.uk",
      "url": "https://peppervale.co.uk",
      "language": "en",
      "country": "GB",
      "market": "en-GB",
      "status": "active",
      "articles_published": 34,
      "created_at": "2026-06-02T11:04:19+00:00"
    }
  ]
}
GET /api/v1/articles

A page of articles, newest change first, without the writing. Ordered by when each article last changed rather than when it was written, which is what makes updated_since worth having: an article that is already live can be edited later, when a link is inserted into it or taken out again.

Query parameters

Name What it does
site_id One site, from /sites. An id that is not on your account answers 404.
status One of draft, qa, needs_repair, review, ready, published, failed. Anything else is a 422 rather than an empty page. "ready" is what a build wants: written, checked, and not yet anywhere.
updated_since ISO 8601, such as 2026-09-06T00:00:00Z. Remember the timestamp of your last run and pass it back on the next one.
per_page Up to 100. The default is 25.
page From 1. meta.has_more says whether to ask for another.

Example

curl -G https://seonis.ai/api/v1/articles \
  -H "Authorization: Bearer $SEONIS_API_KEY" \
  -d site_id=5 \
  -d status=ready \
  -d updated_since=2026-09-01T00:00:00Z \
  -d per_page=50
{
  "articles": [
    {
      "id": 918,
      "site_id": 5,
      "language": "en",
      "status": "ready",
      "title": "How to choose a pepper grinder",
      "slug": "how-to-choose-a-pepper-grinder",
      "meta_title": "How to choose a pepper grinder | Peppervale",
      "meta_description": "Burr or blade, ceramic or steel: what matters in a grinder.",
      "meta_keywords": ["pepper grinder", "burr grinder"],
      "tags": ["Kitchen"],
      "excerpt": "A burr grinder gives an even grind; a blade one does not.",
      "key_takeaways": ["A burr grinder gives an even grind; a blade one does not."],
      "faq": [{ "question": "Ceramic or steel?", "answer": "Steel for pepper, ceramic for salt." }],
      "hero_image": { "url": "https://seonis.ai/storage/images/5/918.jpg", "alt": "A pepper grinder" },
      "infographic_url": null,
      "word_count": 1812,
      "published_url": null,
      "published_at": null,
      "updated_at": "2026-09-06T06:31:12+00:00",
      "created_at": "2026-09-05T02:14:55+00:00",
      "quality": { "seo_score": 88, "language_score": 96, "language_findings": [] }
    }
  ],
  "meta": { "page": 1, "per_page": 50, "total": 1, "last_page": 1, "has_more": false }
}
GET /api/v1/articles/{id}

One article, with everything the list leaves out: content_markdown, content_html, the FAQ as schema.org JSON-LD ready to drop into the page, and any exchange links the article carries.

The field names are the ones our webhook sends, on purpose. If you already have a webhook receiver, the same parser reads this.

Example

curl https://seonis.ai/api/v1/articles/918 \
  -H "Authorization: Bearer $SEONIS_API_KEY"
{
  "article": {
    "id": 918,
    "site_id": 5,
    "language": "en",
    "status": "ready",
    "title": "How to choose a pepper grinder",
    "slug": "how-to-choose-a-pepper-grinder",
    "meta_description": "Burr or blade, ceramic or steel: what matters in a grinder.",
    "content_markdown": "Grinders differ.\n\n## Burr or blade\n\nA **burr** grinder ...",
    "content_html": "<p>Grinders differ.</p>\n<h2>Burr or blade</h2> ...",
    "faq_schema": { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [] },
    "hero_image": { "url": "https://seonis.ai/storage/images/5/918.jpg", "alt": "A pepper grinder" },
    "word_count": 1812,
    "published_url": null,
    "updated_at": "2026-09-06T06:31:12+00:00",
    "quality": { "seo_score": 88, "language_score": 96, "language_findings": [] },
    "exchange_links": []
  }
}
POST /api/v1/articles/{id}/published

You put the article live; this is where you tell us the address. It is the one endpoint that changes anything.

It does exactly what a delivery we made ourselves does: the article is marked published, your monthly allowance counts it, the plan item closes, any exchange links in it get the address the verifier has been waiting for, and your Facebook Page is told if you have connected one. Without this call the article stays ready for ever and none of that happens.

Example

curl -X POST https://seonis.ai/api/v1/articles/918/published \
  -H "Authorization: Bearer $SEONIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://peppervale.co.uk/blog/how-to-choose-a-pepper-grinder"}'

Answers

200 Recorded. The article comes back with its new status, published_at and published_url.
422 The url is missing or is not a full http:// or https:// address.
409 The article is not ready to be published, or is already recorded as published. The article comes back with the refusal, so you can see which.

Connect "REST API" as your publishing engine.

Under Settings, then Publishing, there is an engine called "REST API: you fetch and publish". It asks for nothing, because there is nothing for us to send. What it changes is what happens each morning: a finished article stays ready with no address, and the publishing screen says it is waiting for you to fetch it rather than showing a delivery that failed.

You can use the API without connecting it: the endpoints work for any account with a key. Connecting it is how the rest of the product knows to stop expecting an address of its own, and it is what stops an article you publish yourself being counted as a failure.

Every failure is JSON with a message.

One shape throughout, whether the refusal came from the key check or from the validator, so nothing has to parse two.

{ "message": "That API key is not valid, or it has been revoked." }
Status What it means
401 No key, a key that is not ours, or a key that has been revoked.
404 No such article or site on this account. Another account’s article answers 404 rather than 403: an API that says "that is not yours" has confirmed the thing exists.
409 The request was fine but the article is not in a state for it.
422 A parameter is missing or wrong. The reply carries an errors object naming the field, as well as the message.
429 Over the rate limit. Retry-After says how long to wait.

What a build step actually does.

#!/usr/bin/env bash
set -euo pipefail

# 1. What is finished and not yet on the site?
ready=$(curl -sG https://seonis.ai/api/v1/articles \
  -H "Authorization: Bearer $SEONIS_API_KEY" \
  -d site_id=5 -d status=ready -d per_page=100)

for id in $(echo "$ready" | jq -r '.articles[].id'); do
  # 2. Take the article, body and all.
  curl -s https://seonis.ai/api/v1/articles/$id \
    -H "Authorization: Bearer $SEONIS_API_KEY" \
    | jq -r '.article.content_markdown' > "content/posts/$id.md"

  # 3. Your build puts it live, and you know where it landed.
  slug=$(echo "$ready" | jq -r ".articles[] | select(.id==$id) | .slug")

  curl -s -X POST https://seonis.ai/api/v1/articles/$id/published \
    -H "Authorization: Bearer $SEONIS_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"url\": \"https://peppervale.co.uk/blog/$slug\"}"
done

Make a key and try it.

The trial is enough to read the API: the articles written during it are real articles, and they come back through these endpoints like any others.

Start free trial