Agents: fetch this page as plain markdown at
https://docs.usecleff.com/webhooks.md.Subscribe an endpoint
Each environment has one webhook endpoint. The environment comes from the API key you call with, so a sandbox key manages the sandbox endpoint and a production key manages the production one. The URL must use HTTPS and resolve to a public address.secret now. It’s the key you verify every delivery with, and Cleff
returns it only in this response. If you lose it,
rotate it.
Cleff sends only the events listed in
enabled_events, and nothing while
enabled is false. Events you aren’t subscribed to aren’t saved for later,
and neither are retries still pending when you disable the endpoint, delete it,
or remove their event from enabled_events: turning it back on doesn’t resend
them.
Register the URL that handles the request itself, not one that redirects to it.
Verify the signature
Every delivery is aPOST with a JSON body and an X-Cleff-Signature header:
t is the Unix time the delivery was signed. v1 is the hex HMAC-SHA256 of
<t>.<raw request body>, keyed with your signing secret.
2xx status when:
- the header is missing or malformed;
tis more than 300 seconds from your clock, which stops an old delivery being replayed;- the HMAC you compute doesn’t match
v1. Compare in constant time, astimingSafeEqualdoes.
t and is
signed with your current secret.
Respond, and let Cleff retry
Return any2xx status within 10 seconds to acknowledge a delivery. Anything
else counts as a failure and is retried: a 4xx or 5xx, a timeout, or a
connection error.
Store the event with its id as a unique key and return 2xx straight away,
then do the work asynchronously. Slow processing inside the request turns into
timeouts, and timeouts turn into duplicates.
Cleff makes up to 8 attempts per delivery, backing off exponentially:
The last attempt comes about an hour after the first. After it fails, Cleff
stops retrying that delivery. The subscription stays enabled and later events
are still sent. To catch up on payouts you missed, list the ones changed since
your last event with
GET /v1/payouts?updatedSince=<timestamp>, and
read a Beneficiary with
GET /v1/beneficiaries/{id}.
Deduplicate and order events
Deliveries can arrive more than once and out of order. Deduplicate on
id and
order events about the same payout or Beneficiary by (occurred_at, sequence).
If you apply events in arrival order, a duplicate can be counted twice or a
late event can move a payout back to an earlier state, and every request still
succeeds.data:
amount is "500.00"
because webhooks carry amounts in major units, while the API’s amount_minor
for the same payout is 50000. See
Read amounts in minor and major units.
To handle deliveries safely:
- Skip an
idyou’ve already stored. Record eachidwhen the event arrives, not when you finish processing it, so a repeat that arrives mid-processing is still caught. - Find the subject. It’s
data.payout_idfor apayout.*event anddata.beneficiary_idfor abeneficiary.*event. The two families aren’t ordered against each other. - Don’t let an older event overwrite a newer one. Keep the highest
(occurred_at, sequence)you’ve applied for each subject, and don’t let an event that sorts before it change the subject’s status.
payout.delivered is the one exception. Its occurred_at is when the money
arrived, which can be earlier than the payout.disbursed event for the same
payout. Always apply it: a delivered payout stays delivered. If you receive two
with different delivered_at values, the one with the higher sequence is the
correction.
Rotate the signing secret
CallPOST /v1/webhook-subscriptions again to rotate the secret. The response
carries the new one, and the old one stops working at once. Repeating the call
rotates it again.
The call also replaces the endpoint’s url and enabled_events with the ones
you send, and sets enabled to true. Read the current values with
GET /v1/webhook-subscriptions first and send them back unchanged, or a
rotation silently drops events you meant to keep.
PATCH never rotates the secret, so you can change the URL or the event list
without redeploying it.
Choose the events you receive
Subscribe to events by name inenabled_events. The event pages describe each
event’s data.
In sandbox, payouts always succeed and stop at
payout.disbursed. You won’t
see payout.delivered, payout.settled, payout.canceled or
payout.returned until production, so subscribe to them before you go live.