Why use webhooks instead of polling
Content generation is asynchronous — a POST /generate call returns immediately with a run_id, and the actual article can take anywhere from seconds to a couple of minutes depending on length and current queue load. You can poll GET /v1/api/blogs/:uid on an interval to check status, but a registered webhook pushes an event to your own server the moment something happens, with no polling delay and far fewer wasted requests.
Setting up a webhook
- Go to Account Settings → Webhooks and enter an HTTPS endpoint you control.
- Save it — Seobox generates a webhook secret at the same time, shown once. Store it securely; it's used to verify incoming payloads.
- Your endpoint must respond with a
2xxstatus within 15 seconds. Anything else is treated as a failed delivery.
Only one webhook URL is supported per account today — route to multiple internal systems from your own receiving endpoint if needed.
Events
| Event | Fires when |
|---|---|
generation.completed |
An article has finished generating and is ready (either for review or publish, depending on your account's auto-publish setting) |
blog.ready_for_review |
An article is generated and held for manual approval before publishing |
blog.published |
An article has been successfully pushed live to the connected CMS |
blog.failed |
Generation or publishing failed for a run |
opportunity.created |
The opportunity engine surfaces a new content gap, decay, or ranking-gap opportunity for a connected site |
Payload shape
Every event is delivered as a POST request with an identical envelope:
{
"event": "blog.published",
"account_id": "acct_4d1e...",
"data": {
"blog_uid": "blog_1a4f...",
"site_uid": "site_9f2a...",
"title": "Keyword Research That Finds Intent, Not Just Volume",
"published_url": "https://example.com/blog/keyword-research"
},
"timestamp": "2026-07-16T14:02:31.000Z"
}
data varies by event type, but event, account_id, and timestamp are always present.
Verifying the signature
Every delivery includes an X-SEOBOX-Signature header — an HMAC-SHA256 digest of the raw request body, signed with your webhook secret. Always verify this before trusting a payload; without verification, anyone who discovers your endpoint URL could send fabricated events.
const crypto = require('crypto');
function isValidSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Compute the HMAC over the raw, unparsed request body — if your framework has already parsed the JSON into an object by the time you check the signature, re-stringifying it can produce a different byte sequence (key order, whitespace) and fail verification even for a legitimate request. Capture the raw body before any JSON-parsing middleware runs.
Delivery and retries
- Delivery attempts time out after 15 seconds.
- A
4xxresponse is treated as a permanent client-side failure and is not retried — fix your endpoint and the next event will deliver normally. - A
5xxresponse or timeout is retried up to 3 times with exponential backoff. - Only publicly resolvable HTTPS URLs are accepted; internal, private, or loopback addresses are rejected when the webhook URL is registered.
FAQ
What happens if my endpoint is down when an event fires? Delivery is retried up to 3 times with exponential backoff for server errors and timeouts. If all retries are exhausted, the event is dropped — for critical workflows, periodically reconcile via the API rather than relying on webhooks as the sole source of truth.
Can I register more than one webhook endpoint? Not directly — one URL per account. Fan out to multiple internal consumers from your own receiving service if you need to notify several systems.
Do webhook failures block content generation or publishing? No. Webhook delivery is fire-and-forget and never blocks or fails the underlying generation or publish operation — it's purely a notification layer.
