Skip to main content
Instead of polling, subscribe a webhook so Craftkit pushes state changes to your server. Four event families are delivered: render.* (rendering lifecycle), document.* (engagement on a delivered document), signature.* (e-signature lifecycle), and form.* (collect-only embed fill submissions).
Looking to trigger a render from an external system instead? That’s the inbound webhook (POST /v1/hooks/:token) — see Inbound webhook.

Configure a subscription

Webhooks are configured per project in the dashboard (Project → Webhooks): a url, a signing secret, the list of events to subscribe to, and an active flag. There is no API to register subscriptions programmatically yet (roadmap) — use the dashboard. A subscription only receives the events it is subscribed to; run several subscriptions (each with its own secret) to split traffic by family.

Event triggers

Every event name is family.action.

render.*

document.*

Fired as recipients interact with a render you’ve shared or emailed (see Sharing). Recipient-facing events (viewed/downloaded/printed/email_opened) are de-duplicated within a 5-minute window per (share, event, source IP).

signature.*

Fired as a signature request progresses. Payloads are provider-neutral — Craftkit’s own status vocabulary only, never a third-party event type or id.

form.*

Emitted by embed fill sessions minted with form.captureMode: "collect" (see Embed sessions). In collect-only mode the form is pure data-collection infrastructure: on submit Craftkit validates the field data and delivers it to you without creating a render or storing the field data. You persist it and request the render as a separate follow-up call.
form.submitted is ephemeral: because collect-only mode retains nothing, the delivery payload (which carries the submitted field data) is held only until you 2xx it — through the normal retry/backoff — then it is purged. There is no pull fallback: the webhook is the only delivery path. Alert on a delivery that exhausts its retries (abandoned), since after that the payload is gone.
At-least-once, no replay. The collect submit returns 202 only after the form.submitted delivery is durably enqueued — but a 503/500 from the submit endpoint may still have enqueued (and delivered) it, so treat form.submitted as at-least-once. A submit retry mints a new delivery, so x-craftkit-delivery-id won’t match across a resubmit — dedupe on the payload’s sessionId (stable per fill), not on the delivery id alone. And because an abandoned delivery is purged it cannot be replayed: keep your endpoint reachable and rely on your own retry.

Delivery contract

Craftkit POSTs a JSON body to your url with these headers: Every body is a JSON object whose first field is event; the rest depends on the family.
string
render.succeeded or render.failed.
string
string
string
Terminal render status.
string | null
Public PDF URL on success; null when public delivery isn’t configured or on failure.
string | null
Set on render.failed.
string
string | null

Verify the signature

Compute the HMAC over the raw request body and compare against x-craftkit-signature (raw hex, no sha256= prefix) in constant time. Reject on mismatch. Optionally reject deliveries whose x-craftkit-timestamp is outside a tolerance window to blunt replays.

Retries

Failed deliveries (non-2xx, timeout, or error) retry up to 6 attempts; after that the delivery is marked abandoned. Respond 2xx quickly (the budget is ~15s per attempt) and do heavy work asynchronously. De-duplicate on x-craftkit-delivery-id since a delivery may arrive more than once.
downloadUrl in render.* payloads follows the same durability rules as elsewhere — a permanent public URL, or null if no public bucket is configured. See Render lifecycle.