- List drafts with a custom workflow status (e.g. Ready to publish).
- Fetch the full content (title, sanitized HTML, meta description) for each draft.
- Push to the CMS and immediately transition the draft to a second custom status (e.g. Sent to CMS).
- Mark each content as published in Semji once the CMS confirms the article is live.
This main flow is pull-based: your worker polls Semji because no webhook fires on custom workflow status changes. Semji does fire a webhook when a content is marked as published — if your editors manage publication from Semji, see the push-based alternative below.
Why two custom statuses?
Between “the editor approved the draft” and “the article is live on the public site”, the article can sit in the CMS for hours or days — review, scheduled publication, legal moderation… You need a status that means “already handed over to the CMS, don’t push it again” while you wait.| Stage | Semji status | Set by | Purpose |
|---|---|---|---|
| Editorially approved, ready to push | Ready to publish (custom) | Editor, in the app | Signals the worker to pick it up |
| Pushed to the CMS, awaiting publication | Sent to CMS (custom) | Worker, right after the CMS accepts it | Prevents re-pushing on the next poll |
| Live on the CMS | published (system) | Worker, via POST /contents/{id}/publish | Records the final URL and publishedAt |
- Marking as published right after the push —
publishedAtis wrong (the CMS may schedule for next week) and the URL may still change (slug under review). - Leaving the draft in Ready to publish — the next poll re-pushes the same article, creating duplicates in the CMS.
Prerequisites
- An API key with access to the workspace. See Authentication.
- The workspace ID you want to sync from. Get it from
GET /v1/workspaces. - Two custom workflow statuses: Ready to publish and Sent to CMS.
1. Create the two custom statuses
Workflow statuses are configured per workspace in the Semji app under Settings > Workflow. Create two non-system statuses:- Ready to publish — editors move drafts here when they’re approved.
- Sent to CMS — the worker moves drafts here right after the CMS accepts them.
200 OK
to do, published) have isReadOnly: true and can’t be renamed or deleted. Cache both custom IDs — you’ll use them in the next steps.
2. Poll for drafts ready to publish
CallGET /v1/workspaces/{workspaceId}/contents filtered on the Ready to publish status:
limit caps at 100 (default 25), and the envelope carries a pagination object (total, page, limit, hasMore). If hasMore is true, request the next page with ?page=2, and so on — the full worker below does this. Don’t assume one page is enough: a backlog (worker downtime, a bulk editorial approval) can easily exceed 100 drafts.
Each API key is limited to 1,000 requests per hour with a burst of 20/s. Polling every 10 minutes (= 6/hour) leaves plenty of headroom even for hundreds of drafts per poll. See Rate limits.
3. Push to the CMS and transition to Sent to CMS
For each draft, fetch the full body withGET /v1/contents/{id}, push it to your CMS, then immediately transition the draft to Sent to CMS with PUT /v1/contents/{id}.
- Push
htmlSanitized, nothtml. Thehtmlfield is the body as authored in the Semji editor and may contain editor-only markers (comments, fact-check annotations).htmlSanitizedis the same body with all annotations stripped — that’s the one that is safe to publish externally. versionis required on every update, even when you only change the status. Reuse theversionfrom yourGET. If anything modified the content between yourGETand yourPUT(an editor saving, for instance), the API returns409 Conflict. A 409 doesn’t mean your transition is invalid — refetch to get the freshversionand retry the same update.- Transition only on CMS success. If the CMS call fails, leave the draft in Ready to publish and the next poll will retry it.
- Persist the CMS record ID (e.g. WordPress post ID) on your side, keyed by Semji
content.id. You’ll need it in step 4 to know when the article goes live.
GET /v1/contents/{id} (excerpt)
4. Mark as published when the CMS goes live
The CMS publication can happen seconds or days after the push. You need a separate trigger that fires when the article actually goes live. Pick the option that fits your CMS:- CMS webhook (recommended when available) — WordPress, Contentful, Webflow, and most modern CMSes can fire a webhook on publish. Wire it to a small endpoint that looks up the Semji
content.idfrom the CMS record ID and calls Semji. - Second poll loop — every 10 min, list all your Sent to CMS drafts, check each one against the CMS API to see if it’s now public, and mark the ones that are.
POST /v1/contents/{id}/publish.
url(optional) — the public URL where the article is now live. If omitted, Semji reuses the URL of the associated page. Pass it explicitly when you’re publishing a new page or when the CMS slug differs from the original page URL.publishedAt(optional, ISO 8601) — the publication date recorded in Semji. If omitted, the current server time is used. Pass it when the publication is detected with a delay — e.g. your poll loop discovers an article that went live earlier — so performance tracking starts from the real publication date.
Putting it all together
A minimal worker that polls every 10 minutes for both transitions:finalizePublished with a webhook handler that calls POST /v1/contents/{id}/publish directly when it receives a “post published” event.
Push-based alternative with webhooks
The main flow assumes your CMS decides when an article goes live. Sometimes it’s the other way around: your pages already exist in Semji with their URLs (you optimize existing content, or you create the page in the CMS first), and your editors want to drive publication from Semji by clicking Mark as published. In that scenario you don’t need polling or custom statuses at all — Semji notifies you.| Pull (polling worker) | Push (webhook) | |
|---|---|---|
| Source of truth for publication | Your CMS | Semji (Mark as published) |
| Publication URL | Discovered after the CMS publishes | Already known (existing pages) |
| Latency | Up to one polling interval | Seconds |
| Custom statuses needed | Two | None |
| You host | A scheduled worker | A public HTTPS endpoint |
How it works
- In the Semji app, go to your workspace Settings > General and click Add a Webhook — paste the URL of your endpoint. See Automate your content publishing with webhooks in the Help Center, which also provides ready-made Make and N8n templates for WordPress if you’d rather not write code.
- The editor finishes the draft and clicks Mark as published, filling in the publication URL and date (backdating works both in the app and via the API’s
publishedAtfield). - Semji sends a
POSTrequest to your endpoint with acontent_publishedevent. - Your endpoint creates or updates the article in the CMS.
content_published webhook payload (excerpt)
data.htmlis already sanitized — editor annotations are stripped, it’s equivalent to thehtmlSanitizedfield of the API. You can push it to your CMS as-is.data.idis the content ID, directly usable withGET /v1/contents/{id}.- Delivery is a single POST — no signature, no retry on failure. Respond with a 2xx immediately and process asynchronously.