Webhook

Content Autopilot POSTs each new or refreshed article to your endpoint as JSON. The two events, the exact payload, authentication and a working receiver.

Updated 14 November 2026

Connect a webhook under Settings, Publishing and every article is sent to your endpoint as JSON when it is published, and again when it is refreshed. Your code decides what to do with it.

Setting it up

You give two things: an https address, and an access token you make up. When you press Connect we send a test event. If your endpoint does not answer with a 2xx status, the connection is not saved and you are shown what it answered.

Authentication

Every request carries the token you chose:

Authorization: Bearer your-token
Content-Type: application/json

Compare it on your side and reject requests without it. There is no signature header; the token is the check.

Events

event_type Sent when data
publish_articles An article is published { "articles": [ article ] }
update_article A published article is refreshed { "article": article }
test You connect the webhook { }

Note the difference: a new article arrives in a list, an update arrives as a single object.

Payload

{
  "event_type": "publish_articles",
  "timestamp": "2026-11-14T09:00:03.120Z",
  "data": {
    "articles": [
      {
        "id": "…",
        "title": "How to choose a standing desk",
        "slug": "how-to-choose-a-standing-desk",
        "content_markdown": "…",
        "content_html": "…",
        "meta_description": "…",
        "image_url": "https://…/api/images/…",
        "created_at": "2026-11-13T09:00:00.000Z",
        "tags": ["standing desk"]
      }
    ]
  }
}

image_url is null when images are switched off. tags holds the keyword the article was written for. The slug is stable: use it to find the post again when an update_article arrives.

A receiver in Next.js

// app/api/blog-webhook/route.ts
export async function POST(req: Request) {
  if (req.headers.get("authorization") !== `Bearer ${process.env.BLOG_WEBHOOK_TOKEN}`) {
    return new Response("Unauthorized", { status: 401 });
  }
  const { event_type, data } = await req.json();
  const articles = event_type === "update_article" ? [data.article] : data.articles ?? [];
  for (const a of articles) {
    await savePost({ slug: a.slug, title: a.title, html: a.content_html, description: a.meta_description, image: a.image_url });
  }
  return Response.json({ ok: true });
}

savePost is yours: write to your database, your CMS, or a file. Make it an upsert on the slug so an update replaces the post and does not duplicate it.

Delivery

One request per event. If your endpoint answers with anything other than 2xx, the delivery is shown as failed in the article's publishing log with the status it returned. The article is still published on your hosted blog, so nothing is lost. A failed delivery is not sent again by itself: fix the endpoint, and the article's next refresh and every later article are delivered as usual.

Coming from Outrank

The event names and fields match Outrank's webhook, so a receiver written for it works here without changes.

More documentation