Keepable docs
Webhooks

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"]
  }'
201 Created: secret shown once
{
  "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 asCloudEvents typeFires whendata carries
content.arrivedco.keepable.content.arrivedA delivered item lands in an inboxtenant_id, content_id, recipient_id, content_type, arrived_at
agreement.createdco.keepable.agreement.createdAn agreement is createdtenant_id, agreement_id, status, occurred_at
agreement.signedco.keepable.agreement.signedA participant signs…plus signature_id
agreement.completedco.keepable.agreement.completedAll signers have signedtenant_id, agreement_id, status, occurred_at
agreement.revokedco.keepable.agreement.revokedAn agreement is revokedtenant_id, agreement_id, status, occurred_at
agreement.expiredco.keepable.agreement.expiredAn agreement expires unsignedtenant_id, agreement_id, status, occurred_at
recipient.registeredco.keepable.recipient.registeredA recipient you addressed registersrecipient_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:

POST to your endpoint
{
  "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:

HeaderCarries
X-Keepable-SignatureThe HMAC signature, t=<unix>,v1=<hex>. Verify it.
X-Keepable-Event-IdThe event id, the same value as the envelope id. Use it to de-duplicate.
X-Keepable-Event-TypeThe 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"
200 OK
{
  "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 statusMeaning
pendingNot yet acknowledged; retries may still be in flight.
deliveredYour endpoint returned 2xx.
deadRetries 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.