GET /events - same
events, delivered instead of fetched.
Respond 2xx to acknowledge. Anything else (or a timeout) is treated as a
failure and the delivery is retried.
Events we deliver
Each event names itstype and carries the affected resource as data.object,
already in the shape that resource has on the API. Its object field names what
it is, so one handler can switch on type and read data.object without
guessing. The full catalog:
Delivery and retries
- Deliveries are retried with jittered exponential backoff over multiple
days until your endpoint answers
2xx. - The same event may arrive more than once - dedupe on
webhook-id(the eventid), which is stable across retries. - Events reach each endpoint in the order they occurred: a newer event waits
until every older one for that endpoint has been acknowledged (or given up on
after its retries). So
company.createdalways lands before therequirement.createdthat followed it. While your endpoint rejects an event, later events for that endpoint queue behind it - fix or acknowledge the first, and the rest flow. To keep that order honest even for events created at nearly the same instant, a fresh delivery starts a couple of seconds after its event. - A
Retry-Afterheader on a429or503pushes our next attempt back. - Answer
410 Goneto unsubscribe: the endpoint is disabled and its queued deliveries are cancelled. - An endpoint that fails continuously for days is disabled automatically,
and the workspace owner is emailed. No events are lost - replay what you missed
from
GET /events(see Reconciling by polling). - Redirects are not followed - a
3xxcounts as a failure. If your URL moved, update the endpoint.
Verifying a delivery
Deliveries are signed per the Standard Webhooks specification, and every delivery carries both signature schemes the spec defines: a symmetricv1 HMAC signature and an asymmetric v1a ed25519
signature, space-delimited in one header. Verify whichever suits you - the HMAC
with your endpoint secret, or the ed25519 signature with the endpoint’s public
key, which needs no shared secret at all. The shortest path is an off-the-shelf
standardwebhooks library:
hand it your key material, the three webhook-* headers, and the raw request
body, and it does everything below.
Every endpoint has its own signing secret (whsec_...), shown once when you
create the endpoint, and its own keypair, whose public half (whpk_...) is
readable on the endpoint resource at any time. A valid signature proves the
delivery came from Clemta and was meant for that endpoint - a delivery for one
endpoint cannot be verified as another’s.
Each delivery carries three headers:
webhook-id,
webhook-timestamp, webhook-signature) with the same value - that is what
off-the-shelf libraries look up, so both naming styles verify.
Clemta-Webhook-Idis the event id - stable across retries, your idempotency key.Clemta-Webhook-Timestampis the unix time of this attempt (a retry is re-signed fresh, so it stays inside your tolerance window).Clemta-Webhook-Signatureis a space-delimited list of signatures, each prefixed with its scheme. Skip entries whose prefix you do not verify and match any one that you do:v1,+ base64 ofHMAC-SHA256(key, "<id>.<timestamp>.<body>"), where the key is your secret base64-decoded after thewhsec_prefix, over the exact raw request body. During a secret rotation there are twov1entries - old and new secret both verify until the overlap ends.v1a,+ base64 of the ed25519 signature over the identical"<id>.<timestamp>.<body>"string, verifiable with the endpoint’spublic_key(base64-decoded after thewhpk_prefix). Secret rotations never touch it.
<id>.<timestamp>.<rawBody>, compare
in constant time against each v1, entry, and reject if the timestamp is not
recent (a few minutes’ tolerance stops a captured signature being replayed
later):
Verifying without a shared secret (v1a)
If you would rather not hold a symmetric secret - say the verifying service is
one you do not fully trust with material that can also sign - verify the
v1a entry instead. It is an ed25519 signature over the same
<id>.<timestamp>.<body> string, and the endpoint’s public_key can only
verify, never forge:
public_key is on the endpoint resource in your dashboard, readable any
time - unlike the secret it is not sensitive, so you can fetch it again whenever
you adopt this scheme. Endpoints created before asymmetric signing shipped carry
no keypair and sign v1 only - recreate the endpoint to get one.
Rotating a secret
Roll an endpoint’s secret from the Webhooks page of your partner dashboard. It is a sensitive action, so the dashboard asks you to confirm it with a one-time code emailed to you before the roll goes through. A roll runs with an overlap: for a window, deliveries are signed under both the old and the new secret, so the signature header carries two space-delimitedv1, values and either verifies. Move your endpoint to the new secret during the
overlap, and the old one retires on its own - no delivery is lost mid-rotation.
Payload versioning
A delivery is rendered at the webhook version set for your workspace, not necessarily the latest. Sodata.object keeps the shape you integrated against
even as newer API versions restructure the same resource. See
Versioning for how this relates to the version your API
calls resolve to.
Managing endpoints
Endpoints are managed from the Webhooks page of your partner dashboard:- Create an endpoint - the signing secret is shown once, so store it then.
- Roll its secret - confirmed by email, with the overlap above.
- Disable an endpoint - deliveries stop.
Reconciling by polling
Webhooks andGET /events read the same
stream: every webhook is fanned out from the event log, so an event can never
exist as a delivery only. Events never expire, which makes the endpoint good for
two jobs:
- A safety net beside webhooks - a periodic sweep that catches anything your endpoint missed (an outage longer than the retry horizon, an endpoint disabled for a while).
- A full replacement for webhooks - if you would rather not run a public endpoint, poll the stream on an interval and you receive everything, pulled instead of pushed.
- Remember when your last sweep ran.
- Ask for everything since a few minutes before that:
GET /events?created_at[gte]=<last_sweep - 5m>&sort=created_at, following the cursor (after=<end_cursor>) untilhas_next_pageis false. - Process each event idempotently, keyed by its
id- the same value thewebhook-idheader carries, so one dedupe set covers both channels.
created_at order, so a sweep that starts
exactly where the last one ended could permanently skip an event that landed
late. Re-reading a few minutes of already-seen events costs nothing once
processing is idempotent, and closes that gap completely.
Narrow the sweep when you only care about part of the stream:
?type=company.status.changed, several types at once with
?type[in]=company.created,company.incorporated, or one company with
?company_id=cmp_....
One thing polling is not for: detecting changes by re-listing resources.
List endpoints answer “what is the current state”, and an updated company does
not resurface in any list - the events stream is the only change feed. Poll
GET /events for what happened, then fetch the resource by id if you need its
latest shape.