Skip to main content
When an editor clicks Send to CMS in the Semji editor, Semji sends the draft to your endpoint through a 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 POST with a JSON body and replies in JSON.
  • A content_staged integration configured for your workspace (name + endpoint URL) by a workspace owner under Settings, and enabled.
  • The ability to call back to api.semji.com if 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 single POST (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, the status 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
A completed response without cms_id, or without any URL, is a contract violation — Semji records the send as 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 content stays in a loading state in Semji until you post the result to the callback. If no callback arrives within 24 hours, the send is marked failed.

The async callback

Post the terminal result to the callback.url from the payload (of the form https://api.semji.com/webhooks/calls/{publicId}/callback), with the callback.token as a Bearer token.
The body is the same result format as the sync response. Only terminal outcomes are accepted here — completed or failed; a body with accepted is rejected.

Response codes

If your callback POST times out without a response, re-post it unchanged. The token is single-use per outcome but replays are idempotent: a repeat of the same call returns 200 with no side effect, so you never risk a double handling.

Idempotency

Both directions are safe to retry:
  • Semji → you: the same idempotency_key means 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 200 with no effect.
A new send of the same content (a re-click of Send to CMS) carries a new 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)

  1. Webhook module receives the content_staged POST. Return {"status":"accepted"} immediately so the scenario doesn’t hit the 10-second timeout.
  2. Map data.title, data.html, data.meta_description to your CMS “create entry” module.
  3. HTTP module POSTs the callback: URL = {{callback.url}}, header Authorization: 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 sample content_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

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.
The Authorization header must be Bearer <token> using the exact callback.token from the payload of that send. Tokens are per-send.
The callback expired — more than 24 hours passed since the send. Trigger a new send from the editor to get a fresh callback.
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.