50% reducere la prima lună cu codul LAUNCH50 Doar pentru planurile lunare. Începeți acum

Începeți perioada de probă gratuită

Preluați-vă articolele.
Publicați-le în felul dumneavoastră.

Fiecare articol pe care îl scriem pentru dumneavoastră poate fi citit prin HTTP: titlul, metadatele, Markdown-ul, HTML-ul, imaginea principală și întrebările frecvente. Preluați-le în site-ul dumneavoastră Next.js, Astro sau personalizat când rulează build-ul, publicați-le și spuneți-ne adresa la care a ajuns fiecare.

Un token bearer. JSON la intrare, JSON la ieșire. Niciun SDK de instalat și nimic de configurat în afară de o cheie.

Webhook-ul trimite. API-ul vă permite să preluați.

Transmit același articol în aceeași formă, astfel încât un parser scris pentru una o citește și pe cealaltă fără modificări. Diferența este cine începe conversația.

Webhook-ul, când site-ul dumneavoastră poate accepta o postare la orice oră

Apelăm endpoint-ul dumneavoastră în momentul în care un articol este gata și din nou când se schimbă. Nimic de interogat periodic, nimic de programat. WordPress, Shopify, Ghost, un trigger Zapier sau o rută scrisă chiar de dumneavoastră.

API-ul, când site-ul dumneavoastră este construit și implementat ca o unitate

Un site static nu poate accepta o postare la șase și jumătate dimineața, mai întâi trebuie refăcut ceva. Așa că build-ul dumneavoastră întreabă ce așteaptă, îl preia și ne spune adresa după aceea. Dumneavoastră alegeți când.

Ambele, dacă le doriți

Sunt conexiuni separate și niciuna nu o exclude pe cealaltă. Un webhook care alimentează un newsletter și un API care alimentează site-ul web este o configurație normală.

O cheie, trimisă ca token bearer.

Creați o cheie în panoul de control, la Settings, apoi Connections. Este afișată o singură dată, când este creată, deoarece este stocat doar hash-ul ei: dacă o pierdeți, revocați-o și creați alta. O cheie citește fiecare site din cont și este revocată din același ecran.

Fiecare cerere

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Verificați că funcționează

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
}

Limită de rată

120 cereri pe minut, numărate pentru cheie, nu pentru adresa de la care vin, deoarece runner-ele de build împart adresele. Peste această limită primiți 429 cu antetele standard Retry-After și X-RateLimit. Preluarea articolelor pentru un build înseamnă doar câteva cereri, deci nu este o limită pe care ar trebui să o atingeți din greșeală.

Nu păstrați cheia în repository-ul dumneavoastră

Citește tot ce am scris pentru contul dumneavoastră, inclusiv articolele care nu sunt încă publicate. Puneți-o în mediul dumneavoastră de build, nu în sursă. Revocați-o aici în momentul în care ajunge undeva unde nu ar trebui să fie, iar orice o folosește primește imediat 401.

Sunt cinci, iar patru doar citesc.

Totul se află sub /api/v1/. Versiunea este în cale din prima zi, astfel încât într-o zi să poată exista un v2 fără să strice ce scrieți astăzi.

GET /api/v1/me

Contul căruia îi aparține cheia și numele și prefixul cheii. Util mai ales pentru un singur lucru: să vă spună că cheia din acest mediu este cheia care credeți că este.

Exemplu

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

Fiecare site din cont. id este ceea ce folosește lista de articole pentru filtrare, iar domeniul este acolo pentru ca un script de build să poată potrivi după ceva ce știe deja, în loc să poarte un id.

Exemplu

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

O pagină de articole, cu cea mai nouă modificare prima, fără conținut. Ordonate după momentul ultimei modificări a fiecărui articol, nu după momentul în care a fost scris, ceea ce face ca updated_since să fie util: un articol deja publicat poate fi editat mai târziu, când un link este adăugat în el sau scos din nou.

Parametri de interogare

Nume Ce face
site_id Un site, din /sites. Un id care nu este în contul dumneavoastră răspunde cu 404.
status Una dintre draft, qa, needs_repair, review, ready, published, failed. Orice altceva este 422, nu o pagină goală. "ready" este ce vrea un build: scris, verificat și încă nicăieri.
updated_since ISO 8601, cum ar fi 2026-09-06T00:00:00Z. Rețineți timestamp-ul ultimei rulări și trimiteți-l înapoi la următoarea.
per_page Până la 100. Valoarea implicită este 25.
page De la 1. meta.has_more spune dacă să cereți încă una.

Exemplu

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 articol, cu tot ce omite lista: content_markdown, content_html, întrebările frecvente ca schema.org JSON-LD gata de pus în pagină și orice linkuri de schimb pe care le conține articolul.

Numele câmpurilor sunt cele trimise de webhook-ul nostru, intenționat. Dacă aveți deja un receptor pentru webhook, același parser citește și asta.

Exemplu

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

Publicați articolul; aici ne spuneți adresa. Este singurul endpoint care schimbă ceva.

Face exact ce face o livrare făcută chiar de noi: articolul este marcat ca publicat, este numărat în alocarea dumneavoastră lunară, elementul din plan se închide, orice linkuri de schimb din el primesc adresa pe care o aștepta verificatorul, iar Pagina dumneavoastră de Facebook este notificată dacă ați conectat una. Fără acest apel, articolul rămâne pregătit pentru totdeauna și nimic din toate acestea nu se întâmplă.

Exemplu

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ăspunsuri

200 Înregistrat. Articolul revine cu noul său status, published_at și published_url.
422 Lipsește URL-ul sau nu este o adresă completă http:// sau https://.
409 Articolul nu este gata de publicare sau este deja înregistrat ca publicat. Articolul revine împreună cu refuzul, ca să puteți vedea care dintre situații se aplică.

Conectați "REST API" ca motor de publicare.

Sub Settings, apoi Publishing, există un motor numit "REST API: dumneavoastră preluați și publicați". Nu cere nimic, pentru că nu avem nimic de trimis. Ce schimbă este ce se întâmplă în fiecare dimineață: un articol finalizat rămâne pregătit fără adresă, iar ecranul de publicare spune că așteaptă să îl preluați, în loc să afișeze o livrare eșuată.

Puteți folosi API-ul fără să îl conectați: endpoint-urile funcționează pentru orice cont cu o cheie. Conectarea lui este modul în care restul produsului știe să nu mai aștepte o adresă proprie și este ceea ce împiedică un articol publicat de dumneavoastră să fie considerat un eșec.

Fiecare eroare este JSON cu un mesaj.

O singură formă peste tot, fie că refuzul vine din verificarea cheii sau de la validator, astfel încât nimic să nu trebuiască să interpreteze două.

{ "message": "That API key is not valid, or it has been revoked." }
Stare Ce înseamnă
401 Nicio cheie, o cheie care nu este a noastră sau o cheie care a fost revocată.
404 Nu există un astfel de articol sau site în acest cont. Articolul altui cont răspunde cu 404, nu cu 403: un API care spune "acela nu este al dumneavoastră" a confirmat că acel lucru există.
409 Cererea a fost în regulă, dar articolul nu este într-o stare potrivită pentru asta.
422 Lipsește un parametru sau este greșit. Răspunsul include un obiect errors care numește câmpul, precum și mesajul.
429 Peste limita de rată. Retry-After spune cât trebuie să așteptați.

Ce face de fapt un pas de 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

Verificați că o livrare a venit de la noi.

Fiecare livrare webhook în formatul nostru include trei headere din specificația Standard Webhooks. Verificați-le cu cheia de semnare din setările Publishing și refuzați orice nu se potrivește sau este mai vechi de cinci minute.

webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
Header Ce este
webhook-id Livrarea, aceeași valoare ca delivery_id din corp. Țineți minte livrările pe care le-ați procesat și răspundeți la o repetare cu 200 fără să faceți din nou aceeași muncă.
webhook-timestamp Momentul când am semnat-o, în secunde întregi de la 1970. Refuzați orice diferă cu mai mult de cinci minute față de ceasul dumneavoastră, pentru ca o livrare veche să nu vă poată fi trimisă din nou.
webhook-signature v1, apoi HMAC-SHA256 în base64 al id-ului, al marcajului temporal și al corpului brut, unite prin puncte, făcut cu cheia dumneavoastră de semnare. Pot fi mai multe, separate prin spații. Acceptați livrarea dacă oricare dintre ele se potrivește.

Cheia de semnare

Începe cu whsec_. Scoateți asta și decodificați restul din base64: acei bytes sunt cheia HMAC, nu textul pe care l-ați copiat. Este afișată o singură dată, când conectați webhookul sau apăsați pe "New signing key" în pagina Publishing, iar noi nu o trimitem niciodată.

Verificați corpul brut

Calculați semnătura din bytes exact așa cum au sosit, înainte ca ceva să îi parseze. Un JSON care a fost decodat și codificat din nou nu mai are aceiași bytes, iar semnătura lui nu se va potrivi.

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

Sau folosiți o bibliotecă

Bibliotecile oficiale Standard Webhooks, pentru PHP, JavaScript, Python, Go, Ruby și altele, fac toate acestea într-un singur apel. Dați-le cheia de semnare exact cum v-a fost afișată, inclusiv whsec_. github.com/standard-webhooks

Headerele mai vechi

Livrările includ și secretul de semnare ca token bearer și X-AutoSEO-LT-Signature, un HMAC doar al corpului, făcut cu acel secret. Acestea sunt trimise în continuare pentru ca endpointurile construite pe ele să funcționeze în continuare. Un endpoint nou ar trebui să verifice în schimb webhook-signature: semnătura veche nu acoperă nici timpul, nici id-ul, iar cheia ei este trimisă în fiecare cerere.

Formatele AutoSEO, Outrank și BabyLoveGrowth trimit exact ce au trimis acele servicii, așa că nu includ aceste headere.

Creați o cheie și încercați-o.

Perioada de probă este suficientă pentru a citi API-ul: articolele scrise în această perioadă sunt articole reale și revin prin aceste endpoint-uri ca oricare altele.

Începeți perioada de probă gratuită