Register an endpoint
CHECKFU_API_KEY and CHECKFU_WORKSPACE_ID come from Get access. The TypeScript tabs below use the TypeScript SDK with this client:
event_types entirely. Sending null is valid when patching an existing endpoint but is rejected at creation, and an empty array is rejected in both.
Beside the session-log lifecycle, the catalog carries resource-lifecycle events, delivered with a null data.session_id and the mutated resource’s identity in data.resource_id so one get-by-id completes the read:
memory_store.created/memory_store.archived/memory_store.deleted— a memory store changed, including the platform-created output store a completed dream leaves behind, which no customer call made.agent.created/agent.updated/agent.archived— an Agent changed. Every effective update automatically creates the next immutable Version; fetch the Agent to read its currentversion.connection.created/connection.revoked/connection.refresh_failed— a connection was created, enteredrevoked, or a credential refresh failed and left it needing reauthorization.vault_credential.refresh_failed— an attached Vault Credential’s OAuth access token expired and could not be renewed. The expired token is not released to the Session; usedata.vault_idanddata.resource_idas the nested Vault/Credential lookup pair to inspect its secret-free state before replacing or revalidating it.automation.created/automation.paused/automation.resumed/automation.deleted— an automation’s lifecycle changed. Automations have no archive state; deletion is the terminal transition.workspace.created/workspace.updated— a non-bootstrap Workspace was created from an existing Workspace context, or a Workspace’s versioned configuration/status changed. Creation is delivered through the request Workspace’s pre-existing custodian; bootstrap creates the first Workspace and has no possible Workspace-scoped subscriber, so it emits no fact. Workspaces have no delete/archive operation; disabling one is an update and its custodian remains addressable.sandbox_profile.created/sandbox_profile.updated— a SandboxProfile was created with immutable revision 1, or a later immutable revision advanced its aggregate head. Fetch the SandboxProfile bydata.resource_idto read the current template and revision number. SandboxProfiles expose no archive/delete operation.
secret. Create and rotation require an Idempotency-Key; an
exact retry can recover the same response if the first response was lost, but never mints a second
secret generation for that operation. Store the secret before using a different key or changing
the endpoint version.
Verify the signature
ASCII(t) || "." || raw_request_body, keyed by the whole whsec_… secret as UTF-8 bytes.
path=examples/support-desk/server/src/checkfu-webhook.ts
Checkfu-Signature header, and an array containing the current secret. Malformed or duplicate t/v1 fields, invalid hex, and timestamps more than five minutes away all fail closed.
Two details that cause most failures:
- Sign the raw bytes. The body is not normalized before verification. If your framework parses JSON and you re-serialize it to verify, the signature will not match. Capture the raw body.
- The key is the full secret string, including the
whsec_prefix. Do not key on the decoded suffix.
Handle the delivery
id, then fetch the Session or Run for detail. Respond 2xx quickly and do the work asynchronously. The delivery timeout is 10 seconds.
data.session_id is nullable: it is null on test deliveries, which also carry seq: 0. Handle that before dereferencing it, or the first press of the test button crashes your handler.
Retries and auto-disable
Delivery is at-least-once. A failed delivery retries at 30 seconds, 2 minutes, 10 minutes, 1 hour, 4 hours, and 12 hours, for seven attempts in total. After 20 consecutive terminal failures the endpoint disables itself with a reason ofdelivery_failures. Re-enable it by patching status back to active, which also resets the failure count. Check GET /v1/webhook-endpoints/{id}/deliveries to see what was failing before you re-enable.
That listing takes order=asc or order=desc, and desc is what you want here: it walks the newest deliveries first, so the failures that disabled the endpoint are on the first page. The cursor does not encode a direction, so send the same order with every page request of a walk, including the ones that carry a page cursor.
Redeliver a dead letter
After fixing the receiver, re-enable or update the endpoint first. Then queue the failed delivery again:202 queued receipt. An exact retry with the same idempotency
key returns that receipt without queueing twice.
Redelivery preserves the original delivery ID, event ID, sequence, timestamp,
and thin body. It adds one delivery attempt; it does not fabricate a second
event or reset the attempt count. Because webhook attempts are independent,
the redelivered notification can arrive after notifications for newer events.
Continue deduplicating on id and do not infer event order from arrival order.
Test and rotate
Read the endpoint’s currentversion from its create, get, list, or patch response. Testing and rotation require that reviewed version and reject the request if the endpoint changed before the action ran.
webhook.test and reports whether it arrived and with what status code. That type never appears in a real event log, so it is safe to special-case.
Plan a coordinated cutover, then rotate with a unique idempotency key:
{ "expected_version": <current version> } as the DELETE /v1/webhook-endpoints/{id} body.
Choosing webhooks or streaming
They are complementary: a dashboard usually streams the Session in front of the user and takes webhooks for everything else.
Next steps
Automations and operations
Inbound ingest uses the identical signature scheme in the other direction.
Resume a stream
The streaming half of the same problem.