50% korting op uw eerste maand met de code LAUNCH50 Alleen maandabonnementen. Start nu

Start gratis proefperiode

Haal uw artikelen op.
Publiceer ze op uw manier.

Elk artikel dat wij voor u schrijven kan via HTTP worden gelezen: de titel, de metadata, de Markdown, de HTML, de hero-afbeelding en de FAQ. Neem ze op in uw Next.js-, Astro- of aangepaste site wanneer uw build draait, zet ze live en geef ons het adres door waarop elk artikel is uitgekomen.

Eén bearer token. JSON in, JSON uit. Geen SDK om te installeren en niets om te configureren behalve een sleutel.

De webhook pusht. Met de API kunt u pullen.

Ze dragen hetzelfde artikel in dezelfde vorm, dus een parser die voor de ene is geschreven leest de andere ongewijzigd. Het verschil is wie het gesprek begint.

De webhook, wanneer uw site op elk moment een post kan ontvangen

Wij roepen uw endpoint aan zodra een artikel klaar is, en opnieuw wanneer het verandert. Niets om te pollen, niets om in te plannen. WordPress, Shopify, Ghost, een Zapier-trigger, of een route die u zelf heeft geschreven.

De API, wanneer uw site als één geheel wordt gebouwd en uitgerold

Een statische site kan niet om half zeven 's ochtends een bericht aannemen, eerst moet er iets opnieuw worden gebouwd. Daarom vraagt uw build op wat er klaarstaat, neemt het mee en geeft ons daarna het adres door. U kiest wanneer.

Allebei, als u dat wilt

Het zijn aparte koppelingen en de ene sluit de andere niet uit. Een webhook die een nieuwsbrief voedt en een API die de website voedt, is een normale opzet.

Eén sleutel, verzonden als bearer token.

Maak een sleutel in het dashboard onder Settings, daarna Connections. Die wordt één keer getoond, wanneer hij wordt gemaakt, omdat alleen de hash wordt opgeslagen: als u hem kwijtraakt, trek hem dan in en maak een nieuwe. Een sleutel leest elke site in het account en wordt vanuit hetzelfde scherm ingetrokken.

Elke aanvraag

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Controleer of het werkt

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 aanvragen per minuut, geteld voor de sleutel in plaats van voor het adres waar die vandaan komt, omdat build runners adressen delen. Daarboven krijgt u 429 met de standaardheaders Retry-After en X-RateLimit. Artikelen ophalen voor een build is een handvol aanvragen, dus dit is geen limiet die u per ongeluk zou moeten bereiken.

Houd de sleutel uit uw repository

Deze leest alles wat wij voor uw account hebben geschreven, ook artikelen die nog niet zijn gepubliceerd. Zet hem in uw buildomgeving, niet in uw broncode. Trek hem hier meteen in zodra hij ergens staat waar hij niet hoort, en alles wat hem gebruikt krijgt direct een 401.

Vijf in totaal, en vier zijn alleen-lezen.

Alles staat onder /api/v1/. De versie zit vanaf dag één in het pad, zodat er ooit een v2 kan bestaan zonder te breken wat u vandaag schrijft.

GET /api/v1/me

Het account waartoe de sleutel behoort, en de eigen naam en prefix van de sleutel. Vooral nuttig voor één ding: u laten weten dat de sleutel in deze omgeving de sleutel is die u denkt dat het is.

Voorbeeld

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

Elke site in het account. Op de id filtert de artikellijst, en het domein staat erbij zodat een buildscript kan matchen op iets wat het al kent in plaats van een id mee te dragen.

Voorbeeld

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

Een pagina met artikelen, nieuwste wijziging eerst, zonder de tekst. Gesorteerd op wanneer elk artikel voor het laatst is gewijzigd in plaats van wanneer het is geschreven, en dat maakt updated_since nuttig: een artikel dat al live staat kan later nog worden bewerkt, wanneer er een link in wordt gezet of weer uit wordt gehaald.

Queryparameters

Naam Wat het doet
site_id Eén site, uit /sites. Een id die niet in uw account staat geeft 404.
status Een van draft, qa, needs_repair, review, ready, published, failed. Alles anders geeft een 422 in plaats van een lege pagina. "ready" is wat een build wil: geschreven, gecontroleerd en nog nergens live.
updated_since ISO 8601, zoals 2026-09-06T00:00:00Z. Bewaar de tijdstempel van uw laatste run en geef die bij de volgende weer mee.
per_page Tot 100. De standaard is 25.
page Vanaf 1. meta.has_more geeft aan of u nog een keer moet opvragen.

Voorbeeld

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}

Eén artikel, met alles wat de lijst weglaat: content_markdown, content_html, de FAQ als schema.org JSON-LD, klaar om in de pagina te zetten, en eventuele exchange-links die het artikel bevat.

De veldnamen zijn bewust dezelfde als die onze webhook verstuurt. Als u al een webhook-ontvanger heeft, leest dezelfde parser dit.

Voorbeeld

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

U zet het artikel live; hier geeft u ons het adres door. Dit is het enige endpoint dat iets verandert.

Het doet precies wat een levering die wij zelf hebben gedaan ook doet: het artikel wordt als gepubliceerd gemarkeerd, het telt mee voor uw maandelijkse tegoed, het planitem wordt afgesloten, eventuele exchange-links erin krijgen het adres waarop de verifier heeft gewacht, en uw Facebook Page krijgt bericht als u er een hebt gekoppeld. Zonder deze aanroep blijft het artikel voor altijd klaarstaan en gebeurt niets daarvan.

Voorbeeld

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"}'

Antwoorden

200 Vastgelegd. Het artikel komt terug met de nieuwe status, published_at en published_url.
422 De url ontbreekt of is geen volledig http://- of https://-adres.
409 Het artikel is niet klaar om te worden gepubliceerd, of staat al als gepubliceerd geregistreerd. Het artikel komt met de weigering terug, zodat u kunt zien welke van de twee het is.

Verbind "REST API" als uw publicatie-engine.

Onder Settings, dan Publishing, staat een engine met de naam "REST API: you fetch and publish". Die vraagt om niets, omdat er niets is dat wij moeten versturen. Wat het verandert, is wat er elke ochtend gebeurt: een voltooid artikel blijft klaarstaan zonder adres, en het publicatiescherm zegt dat het wacht tot u het ophaalt in plaats van een mislukte aflevering te tonen.

U kunt de API gebruiken zonder die te koppelen: de endpoints werken voor elk account met een sleutel. Door die te koppelen weet de rest van het product dat het niet langer een eigen adres moet verwachten, en zo voorkomt u dat een artikel dat u zelf publiceert als mislukking wordt geteld.

Elke fout is JSON met een bericht.

Overal dezelfde vorm, of de weigering nu uit de sleutelcontrole komt of van de validator, zodat niets er twee hoeft te parseren.

{ "message": "That API key is not valid, or it has been revoked." }
Status Wat het betekent
401 Geen sleutel, een sleutel die niet van ons is, of een sleutel die is ingetrokken.
404 Geen dergelijk artikel of dergelijke site in dit account. Een artikel van een ander account geeft 404 in plaats van 403: een API die zegt "dat is niet van u" heeft bevestigd dat het bestaat.
409 De aanvraag was in orde, maar het artikel is daar niet in de juiste status voor.
422 Een parameter ontbreekt of is onjuist. Het antwoord bevat een errors-object met de veldnaam en het bericht.
429 Boven de rate limit. Retry-After geeft aan hoe lang u moet wachten.

Wat een buildstap daadwerkelijk doet.

#!/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

Controleer of een levering van ons kwam.

Elke webhooklevering in onze eigen indeling bevat drie headers uit de Standard Webhooks-specificatie. Controleer ze met de ondertekeningssleutel uit de instellingen voor Publishing, en weiger alles wat niet overeenkomt of meer dan vijf minuten oud is.

webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
Header Wat het is
webhook-id De levering, dezelfde waarde als delivery_id in de body. Onthoud welke u al hebt verwerkt, en beantwoord een herhaling met 200 zonder het werk opnieuw te doen.
webhook-timestamp Toen wij het ondertekenden, in hele seconden sinds 1970. Weiger alles wat meer dan vijf minuten afwijkt van uw eigen klok, zodat een oude levering niet opnieuw naar u kan worden gestuurd.
webhook-signature v1, gevolgd door de base64-HMAC-SHA256 van de id, de tijdstempel en de onbewerkte body, samengevoegd met punten, gemaakt met uw ondertekeningssleutel. Er kunnen er meer dan één zijn, gescheiden door spaties. Accepteer de levering als een ervan overeenkomt.

De ondertekeningssleutel

Het begint met whsec_. Haal dat weg en decodeer de rest met base64: die bytes zijn de HMAC-sleutel, niet de tekst die u hebt gekopieerd. Die wordt één keer getoond, wanneer u de webhook koppelt of op de pagina Publishing op "New signing key" drukt, en we versturen die nooit.

Controleer de onbewerkte body

Bereken de handtekening op basis van de bytes precies zoals ze zijn aangekomen, voordat iets ze parseert. JSON dat is gedecodeerd en opnieuw gecodeerd, bestaat niet uit dezelfde bytes, en de handtekening komt dan niet overeen.

PHP

<?php
$raw = getenv('SEONIS_SIGNING_KEY'); // whsec_...

// No key, no deliveries. A missing or empty variable would otherwise become
// an empty HMAC key, and anybody can sign with that.
$secret = is_string($raw) && str_starts_with($raw, 'whsec_')
    ? base64_decode(substr($raw, strlen('whsec_')), true)
    : false;

if ($secret === false || strlen($secret) < 24) {
    http_response_code(500);
    exit('Signing key not configured.');
}

$id        = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$header    = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$body      = file_get_contents('php://input');

if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit('Too old.');
}

$expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $secret, true));

$valid = false;
foreach (explode(' ', $header) as $signature) {
    [$version, $value] = array_pad(explode(',', $signature, 2), 2, '');
    $valid = $valid || ($version === 'v1' && hash_equals($expected, $value));
}

if (! $valid) {
    http_response_code(401);
    exit('Bad signature.');
}

$delivery = json_decode($body, true);

Node.js

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// No key, no server. An empty variable would otherwise become an empty HMAC
// key, and anybody can sign with that.
const raw = process.env.SEONIS_SIGNING_KEY ?? '';
if (!raw.startsWith('whsec_')) throw new Error('SEONIS_SIGNING_KEY is not set.');
const key = Buffer.from(raw.slice('whsec_'.length), 'base64');
if (key.length < 24) throw new Error('SEONIS_SIGNING_KEY is not a valid key.');

// express.raw keeps the body as the bytes we signed.
app.post('/hooks/seonis', express.raw({ type: 'application/json' }), (req, res) => {
  const id = req.get('webhook-id') ?? '';
  const timestamp = req.get('webhook-timestamp') ?? '';
  const header = req.get('webhook-signature') ?? '';

  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(401).send('Too old.');
  }

  const expected = crypto.createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(req.body)
    .digest();

  const valid = header.split(' ').some((signature) => {
    const [version, value = ''] = signature.split(',');
    const given = Buffer.from(value, 'base64');
    return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });

  if (!valid) return res.status(401).send('Bad signature.');

  const delivery = JSON.parse(req.body);
  res.json({ ok: true });
});

Of gebruik een bibliotheek

De officiële Standard Webhooks-bibliotheken, voor PHP, JavaScript, Python, Go, Ruby en meer, doen dit alles in één aanroep. Geef ze de ondertekeningssleutel zoals die aan u is getoond, inclusief whsec_. github.com/standard-webhooks

De oudere headers

Leveringen bevatten ook het ondertekeningsgeheim als bearer token, en X-AutoSEO-LT-Signature, een HMAC van alleen de body gemaakt met dat geheim. Die worden nog steeds meegestuurd zodat endpoints die daarop zijn gebouwd blijven werken. Een nieuw endpoint moet in plaats daarvan webhook-signature controleren: de oude handtekening dekt geen tijd en geen id, en de sleutel ervan gaat mee in elk verzoek.

De indelingen AutoSEO, Outrank en BabyLoveGrowth sturen precies wat die diensten hebben gestuurd, dus ze bevatten deze headers niet.

Maak een sleutel en probeer het.

De proefperiode is voldoende om de API te lezen: de artikelen die daarin worden geschreven zijn echte artikelen, en ze komen via deze endpoints terug zoals alle andere.

Start gratis proefperiode