50% de réduction sur votre premier mois avec le code LAUNCH50 Offres mensuelles uniquement. Commencer maintenant

Commencer l'essai gratuit

Récupérez vos articles.
Publiez-les à votre façon.

Chaque article que nous écrivons pour vous peut être lu via HTTP : le titre, les métadonnées, le Markdown, le HTML, l’image principale et la FAQ. Récupérez-les dans votre site Next.js, Astro ou personnalisé lors de l’exécution de votre build, mettez-les en ligne, puis indiquez-nous l’adresse finale de chacun.

Un jeton bearer. JSON en entrée, JSON en sortie. Aucun SDK à installer et rien à configurer au-delà d’une clé.

Le webhook envoie. L’API vous permet de récupérer.

Ils transportent le même article dans le même format, donc un parseur écrit pour l’un lit l’autre sans modification. La différence tient à qui lance l’échange.

Le webhook, lorsque votre site peut accepter une publication à toute heure

Nous appelons votre point de terminaison dès qu’un article est prêt, puis de nouveau lorsqu’il change. Rien à interroger, rien à planifier. WordPress, Shopify, Ghost, un déclencheur Zapier ou une route que vous avez écrite vous-même.

L’API, lorsque votre site est construit et déployé comme un tout

Un site statique ne peut pas accepter une publication à six heures et demie du matin ; il faut d’abord reconstruire quelque chose. Votre build demande donc ce qui est en attente, le récupère, puis nous indique l’adresse ensuite. Vous choisissez quand.

Les deux, si vous le souhaitez

Ce sont des connexions distinctes et aucune n’exclut l’autre. Un webhook qui alimente une newsletter et une API qui alimente le site web est une configuration normale.

Une clé, envoyée comme jeton bearer.

Créez une clé dans le tableau de bord sous Settings, puis Connections. Elle n’est affichée qu’une seule fois, lors de sa création, car seul son hash est stocké : si vous la perdez, révoquez-la et créez-en une autre. Une clé lit tous les sites du compte et se révoque depuis le même écran.

Chaque requête

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Vérifiez que cela fonctionne

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
}

Limite de débit

120 requêtes par minute, comptées sur la clé plutôt que sur l’adresse d’où elles viennent, car les exécuteurs de build partagent des adresses. Au-delà, vous recevez une 429 avec les en-têtes standard Retry-After et X-RateLimit. Récupérer des articles pour un build représente une poignée de requêtes, donc ce n’est pas une limite que vous devriez atteindre par accident.

Gardez la clé hors de votre dépôt

Elle lit tout ce que nous avons écrit pour votre compte, y compris les articles qui ne sont pas encore publiés. Placez-la dans votre environnement de build, pas dans votre code source. Révoquez-la ici dès qu’elle se trouve là où elle ne devrait pas être, et tout ce qui l’utilise reçoit immédiatement une 401.

Il y en a cinq, et quatre sont en lecture seule.

Tout se trouve sous /api/v1/. La version est dans le chemin dès le premier jour, afin qu’une v2 puisse exister un jour sans casser ce que vous écrivez aujourd’hui.

GET /api/v1/me

Le compte auquel la clé appartient, ainsi que le nom et le préfixe de la clé. Cela sert avant tout à vous indiquer que la clé dans cet environnement est bien celle que vous pensez.

Exemple

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

Chaque site du compte. L’id est ce sur quoi la liste des articles filtre, et le domaine est là pour qu’un script de build puisse faire la correspondance avec quelque chose qu’il connaît déjà plutôt que de transporter un id.

Exemple

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

Une page d’articles, changement le plus récent d’abord, sans le contenu. Triée selon la date de dernière modification de chaque article plutôt que selon sa date d’écriture, ce qui donne son intérêt à updated_since : un article déjà en ligne peut être modifié plus tard, lorsqu’un lien y est ajouté ou retiré.

Paramètres de requête

Nom Ce que cela fait
site_id Un site, depuis /sites. Un id qui n’est pas sur votre compte renvoie 404.
status L’un de draft, qa, needs_repair, review, ready, published, failed. Toute autre valeur renvoie une 422 plutôt qu’une page vide. "ready" est ce qu’un build veut : écrit, vérifié et encore publié nulle part.
updated_since ISO 8601, par exemple 2026-09-06T00:00:00Z. Conservez l’horodatage de votre dernière exécution et renvoyez-le lors de la suivante.
per_page Jusqu’à 100. La valeur par défaut est 25.
page À partir de 1. meta.has_more indique s’il faut en demander une autre.

Exemple

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}

Un article, avec tout ce que la liste laisse de côté : content_markdown, content_html, la FAQ en schema.org JSON-LD prête à être insérée dans la page, et tous les liens d’échange que l’article contient.

Les noms de champs sont ceux que notre webhook envoie, volontairement. Si vous avez déjà un récepteur de webhook, le même parseur lit ceci.

Exemple

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

Vous mettez l’article en ligne ; c’est ici que vous nous indiquez l’adresse. C’est le seul point de terminaison qui modifie quoi que ce soit.

Cela fait exactement ce qu’une livraison effectuée par nous-mêmes fait : l’article est marqué comme publié, il est compté dans votre quota mensuel, l’élément du plan est clôturé, tous les liens d’échange qu’il contient reçoivent l’adresse que le vérificateur attendait, et votre Page Facebook est informée si vous en avez connecté une. Sans cet appel, l’article reste prêt pour toujours et rien de tout cela ne se produit.

Exemple

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

Réponses

200 Enregistré. L’article revient avec son nouveau statut, published_at et published_url.
422 L’url est manquante ou n’est pas une adresse complète en http:// ou https://.
409 L’article n’est pas prêt à être publié, ou est déjà enregistré comme publié. L’article revient avec le refus, afin que vous puissiez voir lequel des deux cas s’applique.

Connectez "REST API" comme moteur de publication.

Sous Settings, puis Publishing, il existe un moteur appelé "REST API: you fetch and publish". Il ne demande rien, car nous n’avons rien à envoyer. Ce qu’il change, c’est ce qui se passe chaque matin : un article terminé reste prêt sans adresse, et l’écran de publication indique qu’il attend que vous veniez le récupérer au lieu d’afficher un envoi qui a échoué.

Vous pouvez utiliser l’API sans la connecter : les points de terminaison fonctionnent pour tout compte disposant d’une clé. La connecter permet au reste du produit de savoir qu’il ne doit plus attendre une adresse de sa part, et c’est ce qui empêche qu’un article que vous publiez vous-même soit compté comme un échec.

Chaque échec est du JSON avec un message.

Une seule forme partout, que le refus vienne de la vérification de la clé ou du validateur, afin que rien n’ait à en analyser deux.

{ "message": "That API key is not valid, or it has been revoked." }
Statut Ce que cela signifie
401 Aucune clé, une clé qui n’est pas la nôtre, ou une clé qui a été révoquée.
404 Aucun article ou site de ce type sur ce compte. L’article d’un autre compte renvoie 404 plutôt que 403 : une API qui dit "ce n’est pas à vous" a confirmé que la chose existe.
409 La requête était correcte, mais l’article n’est pas dans un état qui le permet.
422 Un paramètre est manquant ou incorrect. La réponse contient un objet errors indiquant le champ, ainsi que le message.
429 Limite de débit dépassée. Retry-After indique combien de temps attendre.

Ce qu’une étape de build fait réellement.

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

Vérifiez qu'une livraison vient bien de nous.

Chaque livraison webhook dans notre propre format contient trois en-têtes de la spécification Standard Webhooks. Vérifiez-les avec la clé de signature des réglages de publication, et refusez tout ce qui ne correspond pas ou date de plus de cinq minutes.

webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
En-tête Ce que c'est
webhook-id La livraison, la même valeur que delivery_id dans le corps. Gardez en mémoire celles que vous avez traitées, et répondez 200 à un doublon sans refaire le travail.
webhook-timestamp Le moment où nous l'avons signée, en secondes entières depuis 1970. Refusez tout ce qui s'écarte de plus de cinq minutes de votre propre horloge, pour qu'une ancienne livraison ne puisse pas vous être renvoyée.
webhook-signature v1, puis le HMAC-SHA256 en base64 de l'id, de l'horodatage et du corps brut reliés par des points, créé avec votre clé de signature. Il peut y en avoir plusieurs, séparés par des espaces. Acceptez la livraison si l'un d'eux correspond.

La clé de signature

Elle commence par whsec_. Retirez cela et décodez le reste en base64 : ces octets sont la clé HMAC, pas le texte que vous avez copié. Elle n'est affichée qu'une fois, quand vous connectez le webhook ou appuyez sur "New signing key" sur la page de publication, et nous ne l'envoyons jamais.

Vérifiez le corps brut

Calculez la signature à partir des octets exactement tels qu'ils sont arrivés, avant tout traitement. Un JSON qui a été décodé puis réencodé n'a pas les mêmes octets, et sa signature ne correspondra pas.

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 });
});

Ou utilisez une bibliothèque

Les bibliothèques officielles Standard Webhooks, pour PHP, JavaScript, Python, Go, Ruby et d'autres, font tout cela en un seul appel. Donnez-leur la clé de signature telle qu'elle vous a été affichée, whsec_ inclus. github.com/standard-webhooks

Les anciens en-têtes

Les livraisons contiennent aussi le secret de signature comme jeton bearer, ainsi que X-AutoSEO-LT-Signature, un HMAC du seul corps créé avec ce secret. Ils sont toujours envoyés pour que les points de terminaison qui s'appuient sur eux continuent de fonctionner. Un nouveau point de terminaison doit plutôt vérifier webhook-signature : l'ancienne signature ne couvre ni l'heure ni l'id, et sa clé est envoyée dans chaque requête.

Les formats AutoSEO, Outrank et BabyLoveGrowth envoient exactement ce que ces services ont envoyé, ils ne contiennent donc pas ces en-têtes.

Créez une clé et essayez-la.

L’essai suffit pour utiliser l’API : les articles rédigés pendant cette période sont de vrais articles, et ils reviennent par ces points de terminaison comme les autres.

Commencer l'essai gratuit