Skip to main content
The SEOPilot webhook lets you receive articles automatically as soon as they’re generated. SEOPilot sends a POST request to your endpoint with the full article payload — no polling required. Every article lands in your system the moment it’s ready, so your site stays fresh without any manual intervention.

Setting up your webhook

1

Open Integrations settings

Go to Settings → Integrations in your SEOPilot dashboard.
2

Enter your webhook URL

Paste the full HTTPS URL of your endpoint (e.g., https://yoursite.com/api/seopilot). SEOPilot will POST article payloads to this address. The URL must use https.
3

Set a secret

Add a secret token. SEOPilot uses it to sign every request so you can verify payloads on your end before processing them. A secret is required.
4

Save and test

Save the integration, then click Test webhook to send a sample payload and confirm your endpoint is reachable and responding with a 2xx.

Webhook payload

When an article is generated, SEOPilot sends a POST request with a JSON body structured as follows. The payload includes everything you need to render and serve the article from your own domain.
payload-example.json
{
  "event": "article.generated",
  "delivery_id": "d3f1c2a0-5b6e-4a7c-8d9e-0f1a2b3c4d5e",
  "created_at": "2026-06-01T09:00:00.000Z",
  "data": {
    "site": {
      "id": "8d3482e5-0874-41e1-90b7-43f3fffbac05",
      "url": "https://yoursite.com",
      "name": "Your Site"
    },
    "article": {
      "id": "a1b2c3d4-0000-0000-0000-000000000000",
      "title": "How to Choose a CRM for a Solo Realtor",
      "slug": "crm-for-solo-realtors",
      "meta_title": "How to Choose a CRM for a Solo Realtor",
      "meta_description": "A practical guide to picking the right CRM...",
      "body_md": "# How to Choose a CRM...",
      "internal_links": [
        { "slug": "best-crm-features", "anchor": "CRM features that matter" }
      ],
      "generated_at": "2026-06-01T09:00:00.000Z",
      "hero_image": {
        "url": "https://images.example.com/hero.jpg",
        "alt": "A realtor reviewing leads on a laptop",
        "photographer": { "name": "Jane Doe", "url": "https://unsplash.com/@janedoe" },
        "source_url": "https://unsplash.com/photos/abc123"
      }
    },
    "keyword": {
      "id": "f0e1d2c3-0000-0000-0000-000000000001",
      "keyword": "crm for solo realtors"
    }
  }
}
FieldTypeDescription
eventstringAlways article.generated.
delivery_idstringUnique identifier for this delivery attempt.
created_atstringISO 8601 timestamp of when the envelope was created.
data.siteobjectid, url, and name of the site the article belongs to.
data.article.idstringUnique identifier for the article in SEOPilot.
data.article.titlestringThe article’s full title.
data.article.slugstringURL-safe slug for use in your routing.
data.article.meta_titlestringSEO title for the <title> tag.
data.article.meta_descriptionstringSEO meta description for the <meta> tag.
data.article.body_mdstringThe article body in Markdown.
data.article.internal_linksarraySuggested internal links — each item has a slug and anchor.
data.article.generated_atstringISO 8601 timestamp of when the article was generated.
data.article.hero_imageobject | nullHero image with url, alt, photographer (name, url), and source_url. May be null.
data.keywordobjectThe target keyword — id and keyword.
The article body is delivered as Markdown in body_md. There is no separate HTML or MDX field — render the Markdown with your own pipeline, or write it straight to a Markdown/MDX file (see Static Sites).

Verifying webhook signatures

SEOPilot signs each request with an HMAC-SHA256 signature and sends it in the X-SEOPilot-Signature header. The header has the form t=<timestamp>,v1=<signature>, where timestamp is a Unix timestamp in seconds and signature is the hex HMAC-SHA256 of the string `${timestamp}.${rawBody}` using your secret. Verify it before doing anything with the payload — it guarantees the request genuinely came from SEOPilot and hasn’t been tampered with. SEOPilot also sends X-SEOPilot-Event (the event name) and X-SEOPilot-Delivery (the delivery ID) headers, and a User-Agent of SEOPilot-Webhook/1.0.
webhook-handler.js
const crypto = require('crypto');

/**
 * Verify a SEOPilot webhook request.
 *
 * @param {Buffer|string} rawBody  - The raw, unparsed request body bytes.
 * @param {string}        header   - Value of the X-SEOPilot-Signature header (e.g. "t=1717230000,v1=abc123...").
 * @param {string}        secret   - Your webhook secret from SEOPilot settings.
 * @returns {boolean}
 */
function verifyWebhook(rawBody, header, secret) {
  // Parse "t=<timestamp>,v1=<signature>"
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('='))
  );
  const timestamp = parts.t;
  const signature = parts.v1;
  if (!timestamp || !signature) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const sigBuf = Buffer.from(signature, 'hex');
  const expectedBuf = Buffer.from(expected, 'hex');

  // timingSafeEqual requires both buffers to be the same length.
  if (sigBuf.length !== expectedBuf.length) return false;

  return crypto.timingSafeEqual(sigBuf, expectedBuf);
}
Pass the raw (unparsed) request body bytes as rawBody — do not pass a pre-parsed JSON object, because re-serializing it may produce a different byte sequence and break the signature. If verifyWebhook returns false, reject the request with a 401 and do not process the article.
Always verify the webhook signature before processing the payload. Do not publish content from unverified requests.

Handling failed deliveries

Your endpoint must respond with a 2xx status code for SEOPilot to consider a delivery successful. Respond promptly — a non-2xx response or a network/timeout error is recorded as a failed delivery. When a delivery fails, SEOPilot automatically retries it with exponential backoff — up to 5 attempts total, doubling the wait each time (starting at roughly 2 seconds and capped at 60 seconds between attempts). If every attempt fails, the delivery is marked failed. The most recent delivery status is shown on the webhook in Settings → Integrations — for example, Last delivery: … — 200 OK or Last delivery: … — failed: <error>. You can re-send a sample payload at any time with the Test webhook button to confirm your endpoint is healthy again.