Webhooks
React to events as they happen instead of polling. Register an endpoint, subscribe to event types, and receive signed CloudEvents when content arrives, agreements progress, or a recipient registers.
Webhooks let you react to Keepable events the moment they happen (a delivery landing, a signer signing, a long-retained item finally reaching a newly-registered recipient) instead of polling for changes. You register an HTTPS endpoint, subscribe it to the event types you care about, and Keepable POSTs a signed event to it.
Managing endpoints and reading deliveries needs the webhooks.write scope.
Register an endpoint
POST your receiving URL and the event types to subscribe to. The signing secret is returned exactly once, in this response: store it immediately; you cannot retrieve it again.
curl https://api.keepable.co/webhook_endpoints \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/keepable",
"event_types": ["content.arrived", "agreement.completed"]
}'{
"id": "whe_01HXP",
"url": "https://example.com/hooks/keepable",
"event_types": ["content.arrived", "agreement.completed"],
"active": true,
"created_at": "2026-05-24T10:00:00Z",
"secret": "whsec_abc123"
}secret appears only in this create response; list responses omit it. If
you lose it, delete the endpoint and create a new one. Use the secret to
verify every delivery's signature.
List and delete endpoints as needed (list responses never include the secret):
# List
curl "https://api.keepable.co/webhook_endpoints?limit=50" \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"
# Delete
curl -X DELETE https://api.keepable.co/webhook_endpoints/whe_01HXP \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"Event catalogue
Subscribe using the short event name in event_types. Each delivered event
carries the full CloudEvents type and a typed data payload:
| Subscribe as | CloudEvents type | Fires when | data carries |
|---|---|---|---|
content.arrived | co.keepable.content.arrived | A delivered item lands in an inbox | tenant_id, content_id, recipient_id, content_type, arrived_at |
agreement.created | co.keepable.agreement.created | An agreement is created | tenant_id, agreement_id, status, occurred_at |
agreement.signed | co.keepable.agreement.signed | A participant signs | …plus signature_id |
agreement.completed | co.keepable.agreement.completed | All signers have signed | tenant_id, agreement_id, status, occurred_at |
agreement.revoked | co.keepable.agreement.revoked | An agreement is revoked | tenant_id, agreement_id, status, occurred_at |
agreement.expired | co.keepable.agreement.expired | An agreement expires unsigned | tenant_id, agreement_id, status, occurred_at |
recipient.registered | co.keepable.recipient.registered | A recipient you addressed registers | recipient_id, registered_at, matched_identifier |
The recipient.registered event is the payoff of
retention: when
someone you sent retained mail to finally signs up, this fires, and the held
items are delivered (each one also firing content.arrived).
The CloudEvents envelope
Events are delivered as a CloudEvents 1.0 JSON envelope
over a POST with Content-Type: application/json. The envelope wraps your
typed data:
{
"specversion": "1.0",
"id": "evt_01HXP",
"source": "https://api.keepable.co/tenants/ten_01HXP",
"type": "co.keepable.content.arrived",
"time": "2026-05-24T10:00:00Z",
"datacontenttype": "application/json",
"subject": "cnt_01HXP",
"data": {
"tenant_id": "ten_01HXP",
"content_id": "cnt_01HXP",
"recipient_id": "rcp_01HXP",
"content_type": "letter",
"arrived_at": "2026-05-24T10:00:00Z"
}
}Branch your handler on the envelope type. The id is the unique event id;
keep it for idempotent processing.
Alongside the body, each delivery carries three headers:
| Header | Carries |
|---|---|
X-Keepable-Signature | The HMAC signature, t=<unix>,v1=<hex>. Verify it. |
X-Keepable-Event-Id | The event id, the same value as the envelope id. Use it to de-duplicate. |
X-Keepable-Event-Type | The event type, so you can route without parsing the body. |
Respond fast
Return 200 as soon as you have durably accepted the event, ideally after
just enqueuing it, before any heavy work. Keepable treats a non-2xx or a slow
response as a failure and retries. Do the real
processing asynchronously.
Delivery and retries
Keepable retries failed deliveries with backoff, so your endpoint should expect at-least-once delivery: the same event may arrive more than once. De-duplicate on the event id.
Inspect recent delivery attempts (the last 30 days) to debug a flaky endpoint:
curl "https://api.keepable.co/webhook_deliveries?limit=50" \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"{
"deliveries": [
{
"id": "whd_01HXP",
"endpoint_id": "whe_01HXP",
"event_id": "evt_01HXP",
"event_type": "content.arrived",
"status": "delivered",
"attempts": 1,
"last_error": "",
"created_at": "2026-05-24T10:00:00Z",
"delivered_at": "2026-05-24T10:00:01Z"
}
],
"next_token": null
}Delivery status | Meaning |
|---|---|
pending | Not yet acknowledged; retries may still be in flight. |
delivered | Your endpoint returned 2xx. |
dead | Retries exhausted. The event was not accepted; investigate last_error. |
A dead delivery means you missed an event. Use the delivery list plus the
relevant read endpoint (e.g. fetch the agreement, or
list inbox content) to reconcile.
Next
Verify webhook signatures
Every delivery is signed. Confirm it is genuinely from Keepable, reject replays, and process exactly once.
Access delegation
Let one tenant act on behalf of another. Request access to a tenant, and accept or reject requests targeting yours.
Verify signatures
Every webhook delivery is HMAC-SHA256 signed with your endpoint's secret. Verify the signature, enforce a timestamp window, and de-duplicate on the event id before you trust a payload.