50% отстъпка за първия Ви месец с кода LAUNCH50 Само за месечни планове. Започнете сега

Започнете безплатен пробен период

Изтеглете статиите си.
Публикувайте ги по Вашия начин.

Всяка статия, която пишем за Вас, може да се чете по HTTP: заглавието, метаданните, Markdown, HTML, основното изображение и FAQ. Вземете ги във Вашия сайт на Next.js, Astro или по поръчка, когато компилацията Ви се изпълни, публикувайте ги и ни кажете на кой адрес е публикувана всяка от тях.

Един bearer token. JSON вход, JSON изход. Няма SDK за инсталиране и няма нищо за настройване освен ключ.

Webhook изпраща. API Ви позволява да изтегляте.

Те пренасят една и съща статия в един и същ формат, така че парсер, написан за едното, чете и другото без промяна. Разликата е кой започва комуникацията.

Webhook, когато сайтът Ви може да приема публикация по всяко време

Извикваме Вашата крайна точка в момента, в който статията е готова, и отново, когато се промени. Няма какво да проверявате периодично, няма какво да планирате. WordPress, Shopify, Ghost, тригер в Zapier или маршрут, който сте написали сами.

API, когато сайтът Ви се изгражда и внедрява като едно цяло

Статичен сайт не може да приеме публикация в шест и половина сутринта, първо нещо трябва да се изгради наново. Затова Вашата компилация пита какво чака, взема го и после ни казва адреса. Вие избирате кога.

И двете, ако ги искате

Това са отделни връзки и нито една не изключва другата. Webhook, който подава към бюлетин, и API, което подава към уебсайта, е напълно нормална конфигурация.

Един ключ, изпратен като bearer token.

Създайте ключ в таблото под Settings, после Connections. Показва се само веднъж, когато бъде създаден, защото се съхранява само хешът му: ако го загубите, отменете го и създайте друг. Ключът чете всеки сайт в акаунта и се отменя от същия екран.

Всяка заявка

Authorization: Bearer sns_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Проверете, че работи

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
}

Ограничение на заявките

120 заявки в минута, броени спрямо ключа, а не спрямо адреса, от който идват, защото изпълнителите на компилации споделят адреси. Над това получавате 429 със стандартните заглавки Retry-After и X-RateLimit. Изтеглянето на статии за компилация е няколко заявки, така че това не е ограничение, което би трябвало да достигнете случайно.

Не дръжте ключа в хранилището си

Той чете всичко, което сме написали за Вашия акаунт, включително статии, които още не са публикувани. Поставете го във Вашата среда за компилация, не в изходния код. Отменете го тук веднага щом се озове някъде, където не трябва, и всичко, което го използва, веднага ще получи 401.

Пет са, и четири са само за четене.

Всичко е под /api/v1/. Версията е в пътя от първия ден, така че един ден може да съществува v2, без да счупи това, което пишете днес.

GET /api/v1/me

Акаунтът, към който принадлежи ключът, както и собственото име и префиксът на ключа. Полезно е най-вече за едно: да Ви покаже, че ключът в тази среда е ключът, който мислите, че е.

Пример

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

Всеки сайт в акаунта. id е това, по което списъкът със статии филтрира, а домейнът е там, за да може скрипт за компилация да съпоставя по нещо, което вече знае, вместо да носи id.

Пример

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

Страница със статии, първо с най-новата промяна, без самия текст. Подредени са по това кога всяка статия е променена за последно, а не кога е написана, което прави updated_since полезен: статия, която вече е публикувана, може да бъде редактирана по-късно, когато в нея се добави или премахне връзка.

Параметри на заявката

Име Какво прави
site_id Един сайт, от /sites. id, което не е във Вашия акаунт, връща 404.
status Едно от draft, qa, needs_repair, review, ready, published, failed. Всичко друго е 422, а не празна страница. "ready" е това, което иска една компилация: написано, проверено и още непубликувано никъде.
updated_since ISO 8601, например 2026-09-06T00:00:00Z. Запомнете времевия отпечатък от последното си изпълнение и го подайте обратно при следващото.
per_page До 100. По подразбиране е 25.
page От 1. meta.has_more показва дали да поискате още една.

Пример

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}

Една статия, с всичко, което списъкът пропуска: content_markdown, content_html, FAQ като schema.org JSON-LD, готов за поставяне в страницата, и всички exchange връзки, които статията съдържа.

Имената на полетата са същите като тези, които нашият webhook изпраща, умишлено. Ако вече имате приемник за webhook, същият парсер ще прочете и това.

Пример

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

Вие публикувате статията; тук ни казвате адреса. Това е единствената крайна точка, която променя нещо.

Това прави точно същото като доставка, която сме направили сами: статията се отбелязва като публикувана, месечният Ви лимит я отчита, елементът от плана се затваря, всички exchange връзки в нея получават адреса, който проверяващият е чакал, и Вашата Facebook страница се уведомява, ако сте свързали такава. Без това извикване статията остава готова завинаги и нищо от това не се случва.

Пример

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

Отговори

200 Записано. Статията се връща с новия си статус, published_at и published_url.
422 URL адресът липсва или не е пълен адрес с http:// или https://.
409 Статията не е готова за публикуване или вече е отбелязана като публикувана. Статията се връща с отказа, за да видите кое от двете е.

Свържете "REST API" като Ваш механизъм за публикуване.

Под Settings, после Publishing, има механизъм, наречен "REST API: Вие извличате и публикувате". Той не иска нищо, защото няма какво да изпращаме. Това, което променя, е какво се случва всяка сутрин: готовата статия остава готова без адрес, а екранът за публикуване показва, че чака да я извлечете, вместо да показва неуспешно изпращане.

Можете да използвате API, без да го свързвате: крайните точки работят за всеки акаунт с ключ. Свързването му е начинът, по който останалата част от продукта разбира, че трябва да спре да очаква собствен адрес, и това спира статия, която публикувате сами, да се отчита като неуспех.

Всеки неуспех е JSON със съобщение.

Една и съща форма навсякъде, независимо дали отказът идва от проверката на ключа или от валидатора, така че нищо да не трябва да обработва две.

{ "message": "That API key is not valid, or it has been revoked." }
Статус Какво означава
401 Няма ключ, ключът не е наш или е отменен.
404 Няма такава статия или сайт в този акаунт. Статия от друг акаунт връща 404, а не 403: API, което казва "това не е Ваше", е потвърдило, че това съществува.
409 Заявката е наред, но статията не е в подходящо състояние за това.
422 Липсва параметър или е грешен. Отговорът съдържа обект errors с името на полето, както и съобщението.
429 Над ограничението на заявките. Retry-After показва колко да изчакате.

Какво всъщност прави стъпката за изграждане.

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

Проверете дали изпращането е от нас.

Всяко webhook изпращане в нашия формат съдържа три header-а от спецификацията Standard Webhooks. Проверявайте ги с ключа за подписване от настройките за публикуване и отхвърляйте всичко, което не съвпада или е по-старо от пет минути.

webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
Header Какво е това
webhook-id Изпращането, същата стойност като delivery_id в тялото. Запомняйте кои сте обработили и отговаряйте на повторение с 200, без да вършите работата отново.
webhook-timestamp Кога сме го подписали, в цели секунди от 1970 г. Отхвърляйте всичко, което се разминава с повече от пет минути с Вашия часовник, за да не може старо изпращане да Ви бъде изпратено отново.
webhook-signature v1, след това base64 HMAC-SHA256 на id, времевия печат и суровото тяло, съединени с точки, направен с Вашия ключ за подписване. Може да има повече от един, разделени с интервали. Приемете изпращането, ако който и да е от тях съвпада.

Ключът за подписване

Започва с whsec_. Махнете това и декодирайте останалото с base64: тези байтове са HMAC ключът, а не текстът, който сте копирали. Показва се само веднъж, когато свържете webhook-а или натиснете "New signing key" на страницата за публикуване, и ние никога не го изпращаме.

Проверете суровото тяло

Изчислете подписа от байтовете точно както са пристигнали, преди нещо да ги обработи. JSON, който е декодиран и кодиран отново, не е същите байтове и подписът му няма да съвпадне.

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

Или използвайте библиотека

Официалните библиотеки на Standard Webhooks за PHP, JavaScript, Python, Go, Ruby и други правят всичко това с едно извикване. Подайте им ключа за подписване точно както Ви е показан, включително whsec_. github.com/standard-webhooks

По-старите header-и

Изпращанията съдържат и тайната за подписване като bearer token, както и X-AutoSEO-LT-Signature, HMAC само на тялото, направен с тази тайна. Те все още се изпращат, за да продължат да работят endpoint-ите, изградени върху тях. Нов endpoint трябва вместо това да проверява webhook-signature: старият подпис не включва време и id, а ключът му се изпраща във всяка заявка.

Форматите AutoSEO, Outrank и BabyLoveGrowth изпращат точно това, което са изпратили тези услуги, затова не съдържат тези header-и.

Създайте ключ и го изпробвайте.

Пробният период е достатъчен, за да ползвате API: статиите, написани през него, са реални статии и се връщат през тези крайни точки като всички останали.

Започнете безплатен пробен период