Skip to Content
📘 New: connect any website to SEObox with a signed webhook — see Connect Your Site.
Connect Your SiteCustom Site (Webhook)

Custom Site (Webhook)

If your site isn’t WordPress, Webflow, or Shopify — a Next.js or Astro site, a Laravel/Django/Rails app, a headless CMS, a static site generator — connect it with a webhook. When you publish an article, SEObox sends it to a URL you control as a signed JSON POST. Your code checks the signature and saves the article wherever your site reads content from.

Create an endpoint on your site

Add a route that accepts POST requests, for example https://example.com/api/seobox. The examples below are ready to copy.

Choose a signing secret

Generate a long random string. SEObox uses it to sign every request, and your endpoint uses it to check the signature. For example:

openssl rand -hex 32

Store it on your server as an environment variable such as SEOBOX_WEBHOOK_SECRET.

Add the connection in SEObox

On your site’s page, open Content Management System and choose Headless (called Webhook in the Setup wizard). Fill in:

FieldValue
Webhook URLYour endpoint, e.g. https://example.com/api/seobox
Signing secretThe secret from the previous step
Payload formatJSON (recommended — includes Markdown and HTML), Markdown, or HTML

Click Connect.

Publish a test article

Approve an article and click Publish, then check your endpoint’s logs and your site.

The request

POST /api/seobox HTTP/1.1 Host: example.com Content-Type: application/json X-SEOBOX-Event: post.publish X-SEOBOX-Signature: 5f1c0e8b0c… (hex HMAC-SHA256 of the raw body)

Payload

With the JSON format:

{ "title": "How to Choose a Standing Desk in 2026", "slug": "how-to-choose-a-standing-desk", "meta_title": "How to Choose a Standing Desk (2026 Buyer's Guide)", "meta_description": "Height range, motor type, stability, and budget — what actually matters.", "excerpt": "Height range, motor type, stability, and budget — what actually matters.", "faq": [ { "question": "Are standing desks worth it?", "answer": "For most people who sit…" } ], "status": "publish", "content_markdown": "## Why height range matters\n\n…", "content_html": "<h2>Why height range matters</h2><p>…</p>", "content_format": "json" }
FieldTypeNotes
titlestringArticle title (H1).
slugstringURL-safe slug. Use it as your unique key.
meta_titlestringFor <title>. May be missing.
meta_descriptionstringFor <meta name="description">. May be missing.
excerptstringCurrently the same as meta_description.
faqarray{ question, answer } pairs. May be empty or missing. Not included in the body — render it yourself, ideally with FAQPage structured data.
statusstringAlways "publish" today.
content_formatstring"json", "markdown", or "html" — matches your chosen format.
content_markdown, content_htmlstringJSON format only. The body in both formats.
contentstringMarkdown or HTML format only. The body in the chosen format.
featured_imageobject{ url, alt }. Reserved — not sent yet.
tagsarrayReserved — not sent yet.

Fields without a value are left out of the JSON entirely, so treat every field except title, slug, status, content_format, and the content field(s) as optional.

Your response

Return any 2xx status within 30 seconds. You can include the new post’s ID and live URL, which SEObox shows on the article:

{ "id": "post_812", "url": "https://example.com/blog/how-to-choose-a-standing-desk" }

Both are optional. Without id, SEObox uses the slug; without url, the published URL stays empty.

Any non-2xx status, or no response within 30 seconds, counts as a failure. SEObox retries up to 3 times with exponential backoff, so make your endpoint idempotent: upsert by slug rather than inserting blindly.

Verifying the signature

X-SEOBOX-Signature is the lowercase hex HMAC-SHA256 of the exact raw request body, keyed with your signing secret. To verify:

  1. Read the raw body bytes before parsing JSON. If you re-serialize parsed JSON, the signature won’t match.
  2. Compute hex(HMAC_SHA256(secret, rawBody)).
  3. Compare it to the header with a constant-time comparison. Reject with 401 if it doesn’t match.

Always verify the signature. Without it, anyone who finds your endpoint URL can publish content to your site.

Receiver examples

app/api/seobox/route.ts
import { createHmac, timingSafeEqual } from "node:crypto"; export async function POST(req: Request) { const raw = await req.text(); // raw body, before JSON.parse const expected = createHmac("sha256", process.env.SEOBOX_WEBHOOK_SECRET!) .update(raw) .digest("hex"); const given = req.headers.get("x-seobox-signature") ?? ""; if ( given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected)) ) { return Response.json({ error: "invalid signature" }, { status: 401 }); } if (req.headers.get("x-seobox-event") !== "post.publish") { return Response.json({ ok: true }); // ignore events you don't handle } const post = JSON.parse(raw); // Upsert by slug into your CMS / database / MDX files: // await db.post.upsert({ where: { slug: post.slug }, create: {...}, update: {...} }); return Response.json({ id: post.slug, url: `https://example.com/blog/${post.slug}`, }); }

Testing locally

SEObox only sends to public addresses, so localhost won’t work. Expose your local server with a tunnel such as ngrok http 3000 or cloudflared tunnel --url http://localhost:3000, and use the public URL as your Webhook URL while testing.

You can also simulate a request yourself:

BODY='{"title":"Test","slug":"test","status":"publish","content_format":"html","content":"<p>Hi</p>"}' SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SEOBOX_WEBHOOK_SECRET" -hex | sed 's/^.* //') curl -X POST https://example.com/api/seobox \ -H "Content-Type: application/json" \ -H "X-SEOBOX-Event: post.publish" \ -H "X-SEOBOX-Signature: $SIG" \ -d "$BODY"

Good to know

  • post.publish is the only event today. Posts are not updated or deleted through the webhook.
  • Requests don’t include a timestamp, so don’t reject them for being “too old”. Rely on the signature and on upserting by slug.
  • Coming from an older integration? The headers used to be X-NeuroSEO-Signature and X-NeuroSEO-Event. They are now X-SEOBOX-Signature and X-SEOBOX-Event, and the signing scheme hasn’t changed.
  • Looking for notifications when articles are ready or published — not the article itself? See Event Webhooks.
Last updated on