Webhook reference

The signed POST Seovyn sends to your own site when an article is approved or updated: the events, headers and payload, how to verify the signature in Next.js, Express or Python, retries, and how to test it.

Updated 4 min read
Markdown
On this page

If your site isn't built on a platform Seovyn has an integration for, a webhook lets you receive each article and publish it your own way: into a database, a static build or a custom CMS.

Set it up

On Publishing, choose Webhook under For developers and enter the address Seovyn should POST to, for example https://yoursite.com/api/content-webhook. When you save, Seovyn shows a signing secret once. Copy it into your site's environment then; to get a new one, set the webhook up again.

The request

Seovyn sends a POST with a JSON body and two headers:

HeaderValue
content-typeapplication/json
x-contentautopilot-signatureHex-encoded HMAC-SHA256 of the raw request body, keyed with your signing secret

Answer with a 2xx status within 8 seconds. Anything else, or no answer in time, is a failed delivery.

Events

eventWhen
article.approvedAn article is approved for the first time
article.updatedAn existing article is republished at the same address: after a refresh, a new search snippet, or new links to other articles

Both carry the same shape. Upsert by slug: it identifies the page either way.

The payload

POST /api/content-webhookJSON
{
  "event": "article.approved",
  "data": {
    "itemId": "…",
    "title": "…",
    "pageType": "blog",
    "slug": "how-to-…",
    "body": "# Markdown…",
    "summary": "…",
    "metaTitle": "…",
    "metaDescription": "…",
    "focusKeyword": "…",
    "tags": ["…"],
    "sourceUrl": "https://…",
    "readingMinutes": 7,
    "sources": [{ "title": "…", "url": "https://…" }],
    "image": { "url": "https://…", "alt": "…", "width": 1200, "height": 630 },
    "author": "Jane Doe",
    "approvedAt": "2026-10-01T09:30:00.000Z",
    "updatedAt": null,
    "jsonLd": { "@context": "https://schema.org", "…": "…" }
  }
}
FieldNotes
pageTypeblog, comparison or landing. Map them to /blog/…, /compare/… and /features/…
bodyThe article in Markdown
metaTitle, metaDescriptionThe <title> and meta description
sourcesThe references the writer drew on. Render them as a "Sources" section
imageA 1200 × 630 cover image, or null. Use it as the hero and as og:image
author, updatedAtShow the byline, and use updatedAt as the page's modified date
jsonLdA ready-made schema.org graph: put it in a <script type="application/ld+json">

Verify the signature

Always verify before you trust a request. Compute the HMAC of the exact bytes you received and compare it in constant time.

app/api/content-webhook/route.tsTypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(request: Request) {
  const raw = await request.text(); // the raw body, not re-serialized JSON
  const sent = request.headers.get("x-contentautopilot-signature") ?? "";
  const expected = createHmac("sha256", process.env.SEOVYN_WEBHOOK_SECRET!).update(raw).digest("hex");

  const a = Buffer.from(sent);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response("Bad signature", { status: 401 });
  }

  const { event, data } = JSON.parse(raw);
  // Save or update the page identified by data.slug.
  return new Response("ok");
}

Test it

Send yourself a signed request with the same secret before you connect it:

Terminal
body='{"event":"article.approved","data":{"slug":"hello-seovyn","title":"Hello, Seovyn"}}'
sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$SEOVYN_WEBHOOK_SECRET" | sed 's/^.* //')

curl -i https://yoursite.com/api/content-webhook \
  -H "content-type: application/json" \
  -H "x-contentautopilot-signature: $sig" \
  --data "$body"

A 200 means your handler accepted it; a 401 means the signature check failed.

Retries

A delivery that fails with a server error, a timeout, a rate limit or a network problem is tried again after 10 minutes, 30 minutes, 2, 6 and 12 hours: six attempts over about a day. A 4xx answer (a bad signature, an unknown route, unauthorized) fails the same way every time, so it isn't retried. Failed deliveries appear under Needs attention on the Publishing page with the reason.

Good practice

  • Answer fast. Return 200 as soon as the request is verified and stored; do slow work (rebuilding a site, resizing images) afterwards.
  • Make it idempotent. The same event can arrive more than once. Upsert by slug.
  • Keep the secret out of source control. Read it from the environment.
  • Confirm the page is reachable. After delivery, Seovyn loads the article's address to check it's live, at https://yoursite.com/<section>/<slug>. A page that doesn't load is flagged. See Publishing.