Webhooks
Webhooks push events to your server so you don’t have to poll. Register an HTTPS endpoint, subscribe it to event names, and Scripe POSTs a signed JSON payload every time a matching event fires in the workspace. The most common use: subscribe tojob.completed instead of polling
GET /v1/jobs/{jobId}, and to post.created / post.scheduled to
mirror content state into your own system.
Endpoint management is REST, and all six operations require the
webhooks:manage scope — reads included. Full request/response
schemas are in the OpenAPI reference tab under Webhooks.
Event catalogue
Job side effects fire both the lifecycle event and the
resource-specific one — a finished generation emits
post.created and
job.completed; subscribe to either. Subscribing to an unknown event
name fails with 400 invalid_request, so a typo can’t create an
endpoint that never receives anything.
The catalogue grows additively — write your receiver to ignore type
values it doesn’t recognise.
Payload
Every delivery POSTs one event envelope:idis unique per event. Deliveries are at-least-once — dedupe onidif a double-delivery would hurt you.typeis the discriminator fordata’s shape.projectIdisnullfor events without a project scope.workspaceIdis unprefixed and its shape depends on the caller. It carries whichever id the originating caller was keyed by: for an API-key write, the bare internal workspace id (a 16-character hex string, as above); for an OAuth write, the Clerk org id (org_…). It therefore does not reliably equal theworkspace.idfrom/v1/workspaces/me, which is always the org id. Key your own records off the endpoint you registered rather than joining on this field.
Verifying signatures
Every delivery is signed with the endpoint’s secret (shown once at create and once at rotate): HMAC-SHA-256 over"<t>.<raw body>", hex
encoded, in the Webhook-Signature header as t=<unix seconds>,v1=<hex>.
Delivery and retries
- Timeout: your receiver has 10 seconds to respond. Return a
2xxquickly and process async — anything else counts as a failure. - Retry schedule: five attempts per delivery — the initial call plus four retries at 30 s, 5 min, 30 min, and 4 h after the preceding failure. If the fifth attempt fails, the delivery is marked permanently failed roughly 4 h 35 min after the first attempt.
- Auto-disable: five consecutive permanently-failed deliveries
(not one delivery’s retry chain) disable the endpoint. The
disabledReasonis free-form prose that embeds the streak threshold, today"repeated_failures (>= 5 consecutive deliveries)"— match on therepeated_failuresprefix, never on the whole string, since the threshold is interpolated from the retry schedule. Re-enable it from the dashboard or viaPATCHonce your receiver is healthy. Events routed to a disabled endpoint are recorded as permanently failed with an “Endpoint inactive” reason rather than being retried. Theendpoint_disablederror code is reserved for this condition but is not currently returned by any endpoint. - Redirects count as failures. The worker sends
redirect: "manual"and treats any3xxas a failed attempt, because the SSRF check pinned only the first hop — following aLocationheader would re-resolve it unchecked. A receiver that302s to its canonical URL therefore burns all five attempts and, after five such deliveries, gets auto-disabled. Register the final URL, not one that redirects to it. - Ordering is not guaranteed. Use
createdAt(and your own state) rather than arrival order. - Test-mode keys do not suppress deliveries — yet. A write made
with a
scripe_sk_test_*key enqueues an ordinary delivery and the worker calls your receiver for real. Suppression (recording the delivery as “would have called” without the HTTP call) is planned but not shipped, so point CI at a receiver you own, not at production.
Managing endpoints
webhooks:manageis not grantable to an API key — it is the one REST scope the key wizard does not offer (see auth.md §1.2) — so this call needs an OAuth token. Ascripe_sk_*key always gets403 scope_missinghere.- The
200response carries the plaintext signing secret exactly once. Store it immediately; every later read exposes onlysecretLast4. If you lose it, rotate. projectIdscopes the endpoint to one project;null(the default) delivers events for every project in the workspace.- URL constraints: HTTPS only, and the hostname must resolve to a
public IP — loopback, link-local, private, and CGNAT ranges are
rejected with
ssrf_blocked. The resolved IP is pinned for ~24 h, and every delivery attempt re-resolves the hostname: a changed but still routable address re-pins and the delivery proceeds (load balancers and CDNs cycle IPs), while a private or non-routable result fails that attempt into the ordinary retry chain. Detection alone does not disable the endpoint — only the five-consecutive-failure streak above does. - Rotation (
/rotate-secret) mints the new secret and returns its plaintext once. There is no dual-secret grace window: the endpoint stores exactly one secret, and every delivery after the call is signed with the new one only. Sequence the cutover as: teach your receiver to accept either the current secret or a second one from config, call/rotate-secret, deploy the returned secret into that second slot immediately, then drop the old one.
Receiver checklist
- Return
2xxwithin 10 seconds; do the work async. - Verify
Webhook-Signatureagainst the raw body, with a timestamp staleness check. - Dedupe on
id(at-least-once delivery). - Ignore unknown
typevalues and unknown fields indata. - Alert on your own 4xx/5xx responses — each delivery gives up after ~4.6 hours, and five consecutive give-ups disable the endpoint.