50% di sconto sul Suo primo mese con il codice LAUNCH50 Solo piani mensili. Inizi ora

Inizi la prova gratuita

Recuperi i Suoi articoli.
Li pubblichi a modo Suo.

Ogni articolo che scriviamo per Lei può essere letto via HTTP: il titolo, i metadati, il Markdown, l'HTML, l'immagine hero e le FAQ. Li porti nel Suo sito Next.js, Astro o personalizzato quando la build viene eseguita, li pubblichi e ci comunichi l'indirizzo finale di ciascuno.

Un bearer token. JSON in ingresso, JSON in uscita. Nessun SDK da installare e nulla da configurare oltre a una chiave.

Il webhook invia. L'API Le permette di recuperare.

Trasportano lo stesso articolo nello stesso formato, quindi un parser scritto per uno legge anche l'altro senza modifiche. La differenza è chi avvia la conversazione.

Il webhook, quando il Suo sito può accettare un post in qualsiasi momento

Chiamiamo il Suo endpoint nel momento in cui un articolo è pronto, e di nuovo quando cambia. Nulla da interrogare, nulla da pianificare. WordPress, Shopify, Ghost, un trigger Zapier o una route scritta da Lei.

L'API, quando il Suo sito viene creato e distribuito come un'unità

Un sito statico non può accettare un post alle sei e mezza del mattino; prima qualcosa deve essere ricompilato. Quindi la Sua build chiede cosa è in attesa, lo prende e poi ci comunica l'indirizzo. Sceglie Lei quando.

Entrambi, se li desidera

Sono connessioni separate e nessuna esclude l'altra. Un webhook che alimenta una newsletter e un'API che alimenta il sito web è una configurazione normale.

Una chiave, inviata come bearer token.

Crei una chiave nella dashboard in Impostazioni, poi Connessioni. Viene mostrata una sola volta, quando viene creata, perché viene memorizzato solo il suo hash: se la perde, la revochi e ne crei un'altra. Una chiave legge ogni sito dell'account e si revoca dalla stessa schermata.

Ogni richiesta

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Verifichi che funzioni

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 di richieste

120 richieste al minuto, conteggiate sulla chiave anziché sull'indirizzo da cui provengono, perché i runner di build condividono gli indirizzi. Oltre questo limite riceverà un 429 con le intestazioni standard Retry-After e X-RateLimit. Recuperare gli articoli per una build richiede una manciata di richieste, quindi non è un limite che dovrebbe raggiungere per errore.

Tenga la chiave fuori dal repository

Legge tutto ciò che abbiamo scritto per il Suo account, compresi gli articoli non ancora pubblicati. La inserisca nell'ambiente di build, non nel codice sorgente. La revochi qui nel momento in cui si trova dove non dovrebbe essere, e tutto ciò che la usa riceverà subito un 401.

Sono cinque, e quattro sono di sola lettura.

Tutto si trova sotto /api/v1/. La versione è nel percorso fin dal primo giorno, così un giorno potrà esistere una v2 senza rompere ciò che scrive oggi.

GET /api/v1/me

L'account a cui appartiene la chiave, e il nome e prefisso della chiave stessa. Utile soprattutto per una cosa: dirLe che la chiave in questo ambiente è la chiave che pensa che sia.

Esempio

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

Ogni sito dell'account. L'id è ciò su cui filtra l'elenco degli articoli, e il dominio è presente così uno script di build può confrontarlo con qualcosa che già conosce invece di portarsi dietro un id.

Esempio

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

Una pagina di articoli, con la modifica più recente per prima, senza il testo. Ordinati in base all'ultima modifica di ciascun articolo anziché a quando è stato scritto, ed è questo che rende utile updated_since: un articolo già pubblicato può essere modificato in seguito, quando vi viene inserito o rimosso un link.

Parametri di query

Nome Che cosa fa
site_id Un sito, da /sites. Un id che non appartiene al Suo account risponde con 404.
status Uno tra draft, qa, needs_repair, review, ready, published, failed. Qualsiasi altro valore restituisce un 422 anziché una pagina vuota. "ready" è ciò che una build vuole: scritto, controllato e non ancora pubblicato da nessuna parte.
updated_since ISO 8601, ad esempio 2026-09-06T00:00:00Z. Ricordi il timestamp dell'ultima esecuzione e lo ripassi alla successiva.
per_page Fino a 100. Il valore predefinito è 25.
page Da 1. meta.has_more indica se chiederne un'altra.

Esempio

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 articolo, con tutto ciò che l'elenco omette: content_markdown, content_html, le FAQ come schema.org JSON-LD pronte da inserire nella pagina ed eventuali link di scambio presenti nell'articolo.

I nomi dei campi sono quelli che il nostro webhook invia, volutamente. Se ha già un ricevitore webhook, lo stesso parser legge anche questo.

Esempio

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

È Lei a mettere online l'articolo; qui ci comunica l'indirizzo. È l'unico endpoint che modifica qualcosa.

Fa esattamente ciò che fa una consegna effettuata da noi: l'articolo viene contrassegnato come pubblicato, viene conteggiato nel Suo limite mensile, l'elemento del piano si chiude, eventuali link di scambio al suo interno ricevono l'indirizzo che il verificatore stava aspettando e la Sua Pagina Facebook viene avvisata se ne ha collegata una. Senza questa chiamata l'articolo resta pronto per sempre e nulla di tutto questo accade.

Esempio

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

Risposte

200 Registrato. L'articolo torna con il nuovo stato, published_at e published_url.
422 L'url manca o non è un indirizzo completo http:// o https://.
409 L'articolo non è pronto per essere pubblicato, oppure risulta già registrato come pubblicato. L'articolo viene restituito con il rifiuto, così può vedere quale dei due casi è.

Colleghi "REST API" come motore di pubblicazione.

In Settings, poi Publishing, c'è un motore chiamato "REST API: Lei recupera e pubblica". Non chiede nulla, perché non c'è nulla da inviare da parte nostra. Quello che cambia è ciò che accade ogni mattina: un articolo completato resta pronto senza indirizzo, e la schermata di pubblicazione indica che è in attesa che Lei lo recuperi invece di mostrare una consegna non riuscita.

Può usare l'API senza collegarla: gli endpoint funzionano per qualsiasi account con una chiave. Collegarla è il modo in cui il resto del prodotto sa che deve smettere di aspettarsi un proprio indirizzo, ed è ciò che impedisce che un articolo pubblicato da Lei venga conteggiato come errore.

Ogni errore è JSON con un messaggio.

Un solo formato ovunque, sia che il rifiuto provenga dal controllo della chiave sia dal validatore, così nulla deve analizzarne due.

{ "message": "That API key is not valid, or it has been revoked." }
Stato Che cosa significa
401 Nessuna chiave, una chiave che non è nostra o una chiave che è stata revocata.
404 Nessun articolo o sito di questo tipo su questo account. L'articolo di un altro account risponde con 404 anziché 403: un'API che dice "non è suo" ha confermato che la cosa esiste.
409 La richiesta era valida, ma l'articolo non è in uno stato adatto.
422 Manca un parametro o è errato. La risposta contiene un oggetto errors che indica il campo, oltre al messaggio.
429 Oltre il limite di richieste. Retry-After indica quanto attendere.

Che cosa fa davvero una fase di build.

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

Verifichi che una consegna provenga da noi.

Ogni consegna webhook nel nostro formato include tre header della specifica Standard Webhooks. Li controlli con la chiave di firma nelle impostazioni di Publishing e rifiuti tutto ciò che non corrisponde o che ha più di cinque minuti.

webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
Header Che cos'è
webhook-id La consegna, lo stesso valore di delivery_id nel corpo. Tenga memoria di quelle già gestite e risponda a una ripetizione con 200 senza rifare il lavoro.
webhook-timestamp Quando l'abbiamo firmata, in secondi interi dal 1970. Rifiuti tutto ciò che differisce di più di cinque minuti dal Suo orologio, così una vecchia consegna non può esserLe inviata di nuovo.
webhook-signature v1, poi l'HMAC-SHA256 in base64 dell'id, del timestamp e del corpo grezzo uniti da punti, creato con la Sua chiave di firma. Può essercene più di uno, separati da spazi. Accetti la consegna se uno qualsiasi corrisponde.

La chiave di firma

Inizia con whsec_. Tolga quel prefisso e decodifichi il resto da base64: quei byte sono la chiave HMAC, non il testo che ha copiato. Viene mostrata una sola volta, quando collega il webhook o preme "New signing key" nella pagina Publishing, e noi non la inviamo mai.

Controlli il corpo grezzo

Calcoli la firma dai byte esattamente come sono arrivati, prima che qualcosa li analizzi. Un JSON che è stato decodificato e codificato di nuovo non ha gli stessi byte e la sua firma non corrisponderà.

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

Oppure usi una libreria

Le librerie ufficiali Standard Webhooks, per PHP, JavaScript, Python, Go, Ruby e altro, fanno tutto questo con una sola chiamata. Fornisca loro la chiave di firma esattamente come Le è stata mostrata, whsec_ incluso. github.com/standard-webhooks

Gli header precedenti

Le consegne includono anche il segreto di firma come bearer token e X-AutoSEO-LT-Signature, un HMAC del solo corpo creato con quel segreto. Vengono ancora inviati perché gli endpoint che si basano su di essi continuino a funzionare. Un nuovo endpoint dovrebbe invece controllare webhook-signature: la vecchia firma non copre né l'ora né l'id, e la sua chiave viaggia in ogni richiesta.

I formati AutoSEO, Outrank e BabyLoveGrowth inviano esattamente ciò che quei servizi hanno inviato, quindi non includono questi header.

Crei una chiave e la provi.

La prova è sufficiente per leggere l'API: gli articoli scritti durante questo periodo sono articoli reali e tornano tramite questi endpoint come tutti gli altri.

Inizi la prova gratuita