content_staged webhook. Your endpoint decides where
the content lands in your CMS and returns a review link. This page is the
exchange contract — everything you need to implement the receiving side
without access to Semji’s code.
content_staged is separate from content_published (the event that fires
when a content is marked as published). A content_staged send happens
before publication: the content stays a draft in Semji, and your CMS
holds it for review. The two can be configured independently on the same
workspace.Prerequisites
- An HTTPS endpoint, reachable from the public internet, that accepts a
POSTwith a JSON body and replies in JSON. - A
content_stagedintegration configured for your workspace (name + endpoint URL) by a workspace owner under Settings, and enabled. - The ability to call back to
api.semji.comif you answer asynchronously.
How it works
1
Semji POSTs the draft to your endpoint
A JSON payload carrying the content, an
idempotency_key, and a callback
block is sent to the URL you configured for the content_staged event.2
Your endpoint answers
Either synchronously — return the result in the HTTP response
(
completed or failed) — or, if you need more time, return accepted
and finish later through the callback.3
(async only) You POST the result to the callback URL
When the work is done,
POST the outcome to the callback.url from the
payload, authenticated with the callback.token.The request Semji sends
Semji issues a singlePOST (with Content-Type: application/json) to your
configured URL. The request times out after 10 seconds — see
responding.
Outbound payload
string
Always
content_staged for this webhook.string
Stable 12-char identifier of this send. If you receive the same key twice (a
retry), do not create a second draft — return the result you already produced.
integer
Version of the content at send time. A later send of the same content carries a
higher version — the latest send wins.
object
The content itself:
title, meta_description, sanitized html, plain text,
content_score, words_count, plus page, workspace and organization
context. IDs are 12-char public IDs.object
url to POST the async result to, a one-shot token (see
the callback), and expires_at — the callback is
rejected after this time (24 hours after the send).Responding to the request
Reply with HTTP 2xx as soon as you have received and understood the webhook, and describe the outcome in a JSON body. The HTTP status reflects transport, thestatus field reflects the business outcome — a non-2xx response is read as your
endpoint being unreachable, not as a business failure. There is one result
format everywhere (sync response, callback, and the integration test):
string
required
completed, failed, or accepted.object
Required when
status is completed. Must contain cms_id and at least one
of preview_url / back_office_url.string
Optional human-readable reason when
status is failed.Synchronous — you finish within 10 seconds
Return the terminal result directly in the HTTP response:200 — completed
200 — failed
Asynchronous — you need more time
If your pipeline can’t finish within the 10-second window (moderation, scheduled jobs, a no-code flow…), acknowledge immediately and finish later:200 — accepted
The async callback
Post the terminal result to thecallback.url from the payload (of the form
https://api.semji.com/webhooks/calls/{publicId}/callback), with the
callback.token as a Bearer token.
completed or failed; a body with accepted is rejected.
Response codes
Idempotency
Both directions are safe to retry:- Semji → you: the same
idempotency_keymeans the same send. Don’t create a second draft — return the result you already produced. - You → Semji: replaying the callback for an already-finalized call returns
200with no effect.
idempotency_key and a higher content_version — the latest send wins, and the
callback of a superseded send no longer changes the content’s state.
No-code example (Make / Zapier)
- Webhook module receives the
content_stagedPOST. Return{"status":"accepted"}immediately so the scenario doesn’t hit the 10-second timeout. - Map
data.title,data.html,data.meta_descriptionto your CMS “create entry” module. - HTTP module POSTs the callback: URL =
{{callback.url}}, headerAuthorization: Bearer {{callback.token}}, body{"status":"completed","result":{"cms_id":"{{cms_id}}","preview_url":"{{preview_url}}"}}.
Testing your endpoint
A workspace owner can test the integration from Settings before going live: Semji sends a samplecontent_staged payload to your URL and shows the parsed
verdict (received, sent, or error). A sent verdict means your endpoint
answered accepted — the test flips to received once your callback arrives.
Troubleshooting
The content stays stuck in “loading” in Semji
The content stays stuck in “loading” in Semji
You answered
accepted but no callback reached Semji. Post the result to the
callback.url; if nothing arrives within 24 hours the send is marked failed.My callback returns 401
My callback returns 401
The
Authorization header must be Bearer <token> using the exact
callback.token from the payload of that send. Tokens are per-send.My callback returns 410
My callback returns 410
The callback expired — more than 24 hours passed since the send. Trigger a new
send from the editor to get a fresh callback.
My `completed` response is rejected (400)
My `completed` response is rejected (400)
A
completed result must include cms_id and at least one of
preview_url / back_office_url. A body with accepted is not valid on the
callback — only completed or failed.Related
- Sync drafts to your CMS — the pull-based
alternative and the
content_publishedwebhook. - CMS integrations overview — connect a CMS through MCP or the REST API.
- Authentication — API keys for the REST API.