WEBHOOKS
What we send your webhook
set_webhook registers a URL. Each delivery is a notification, not the article: it carries the event type and ids. For a publish, call get_published with the article_id. De-dup on the delivery id.
Every delivery
A POST with Content-Type: application/json and these headers:
content-typeAlways application/json.
x-letterstory-eventOne of the event names above — tells deliveries apart.
x-letterstory-delivery-idThis event's delivery id (shape documented per event above). Use it for idempotency.
x-letterstory-signaturesha256=<hex> — an HMAC-SHA256 of the raw JSON body using the webhook_secret set on set_webhook. Sent only when a secret is configured.
Delivery:Sent once, with a 10-second timeout. Your endpoint must answer 2xx — anything else (a non-2xx status, a timeout, an unreachable host) is logged on our side and NOT retried. Because there's no retry, pair a webhook with a periodic list_published check (e.g. hourly) as a safety net for the times your endpoint was down.
Events
article.published
Fires when: An article is published or republished (a higher published_version means a republish, not a new article).Sent to: Every API key in the organization with a webhook_url configured — not just the key that triggered the publish.x-letterstory-delivery-id: `<article_id>:<published_version>:<api_key_id>` — stable per publish, safe to use as an idempotency key.
eventstring
article_idstringPass this to get_published to fetch the full article — the payload itself carries no content.
org_idstring
titlestring
slugstring | null
published_versionnumber | nullRepublishing an article sends this event again with a higher version. Upsert on article_id, keep the highest version seen.
published_atstring
article.finished
Fires when: An article becomes a complete draft awaiting human review (lifecycle_phase = 'finished', not yet published) — never guaranteed exactly-once from a single call site, so treat the delivery id as the de-dup key.Sent to: Every API key in the organization with a webhook_url configured.x-letterstory-delivery-id: `<article_id>:<api_key_id>`.
eventstring
article_idstring
org_idstring
collection_idstring | null
titlestring
finished_atstringWhen the article entered lifecycle_phase = 'finished' — a complete draft, never published, ready for a human to review. Same fact notify-standing-reviewers and the Control Room's pending-review bar watch (lifecycle_phase = 'finished' AND published_at IS NULL).
arena_run.paused
Fires when: An arena run (any door: your run_arena, the app, Auto-Pen) pauses at its direction gate, waiting for submit_arena_direction. Unanswered for 72 hours, it is cancelled.Sent to: Every API key in the organization with a webhook_url configured.x-letterstory-delivery-id: `<run_id>:paused:<api_key_id>`.
eventstring
run_idstringPass this to get_arena_run for the whole run (outline, sources, verification).
org_idstring
arena_idstring | null
article_idstring | nullThe post a hosted run writes into; null for a standalone run.
topicstring
origin"direct" | "internal"direct = a caller asked the arena; internal = Auto-Pen, the planner…
callerstring | null
direction_optionsnumberHow many theses wait for an answer: submit_arena_direction (or POST …/runs/{run_id}/direction).
paused_atstring
arena_run.settled
Fires when: An arena run reaches a terminal state: done, error or cancelled (including an expired direction gate).Sent to: Every API key in the organization with a webhook_url configured.x-letterstory-delivery-id: `<run_id>:<status>:<api_key_id>`.
eventstring
run_idstringPass this to get_arena_run for the whole run (outline, sources, verification).
org_idstring
arena_idstring | null
article_idstring | nullThe post a hosted run writes into; null for a standalone run.
topicstring
origin"direct" | "internal"direct = a caller asked the arena; internal = Auto-Pen, the planner…
callerstring | null
status"done" | "error" | "cancelled"
errorstring | null
completed_atstring | null