Skip to main content

Events

Every payload has the same shape, so one handler covers them all — branch on event.
For article.published, article.updated (from Re-publish) and article.unpublished, we wait for your answer. Anything other than a 2xx within 15 seconds fails the attempt; after three failed attempts (exponential backoff from 5 seconds) the publish, re-publish or unpublish fails, and the article keeps its previous state in SeoSorted. Answer 2xx only once you’ve stored — or removed — the post.article.unpublished still carries the publishedAt and publishedUrl of the post being removed, so you can match it against what you stored at publish time.

Headers

X-SeoSorted-Delivery is unique per attempt, so a retry of the same event arrives with a new value. It is useful for logging, but it is not an idempotency key — deduplicate on article.id + event. See verifying the signature.

Body

A real delivery, with the body fields truncated:

Fields

string
required
One of article.published, article.updated, article.unpublished, article.deleted, test.
string
required
ISO 8601 timestamp of when we built the payload.
string
required
Matches the X-SeoSorted-Delivery header. New on every attempt, including retries.
object
required
object
required
Take body.markdown if you store Markdown, body.html if you render directly. They are the same content — the HTML is rendered from the Markdown at delivery time.

A minimal handler

Upsert and delete by article.id, so a retried or repeated event is harmless, and reply with the live URL so SeoSorted can link to it. verify is the function from the webhook page; posts stands in for your own storage.
Express
Keep the work inside the 15-second timeout. If storing a post can take longer, queue it, but only for article.deleted and edit-time article.updated events — for publish, re-publish and unpublish, your 2xx is what tells SeoSorted the change landed.