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 32Store 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:
| Field | Value |
|---|---|
| Webhook URL | Your endpoint, e.g. https://example.com/api/seobox |
| Signing secret | The secret from the previous step |
| Payload format | JSON (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"
}| Field | Type | Notes |
|---|---|---|
title | string | Article title (H1). |
slug | string | URL-safe slug. Use it as your unique key. |
meta_title | string | For <title>. May be missing. |
meta_description | string | For <meta name="description">. May be missing. |
excerpt | string | Currently the same as meta_description. |
faq | array | { question, answer } pairs. May be empty or missing. Not included in the body — render it yourself, ideally with FAQPage structured data. |
status | string | Always "publish" today. |
content_format | string | "json", "markdown", or "html" — matches your chosen format. |
content_markdown, content_html | string | JSON format only. The body in both formats. |
content | string | Markdown or HTML format only. The body in the chosen format. |
featured_image | object | { url, alt }. Reserved — not sent yet. |
tags | array | Reserved — 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:
- Read the raw body bytes before parsing JSON. If you re-serialize parsed JSON, the signature won’t match.
- Compute
hex(HMAC_SHA256(secret, rawBody)). - Compare it to the header with a constant-time comparison. Reject with
401if it doesn’t match.
Always verify the signature. Without it, anyone who finds your endpoint URL can publish content to your site.
Receiver examples
Next.js (App Router)
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.publishis 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-SignatureandX-NeuroSEO-Event. They are nowX-SEOBOX-SignatureandX-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.