Skip to main content
A webhook subscription registers one HTTPS endpoint per environment to receive domain events from Cleff covering the Payout in-flight lifecycle. Deliveries are:
  • Signed with HMAC-SHA256 over "<unix_ts>.<rawBody>", header X-Cleff-Signature: t=<ts>,v1=<hex>.
  • Idempotent: every retry of a logical delivery carries the same id.
  • Per-Payout ordered by (occurred_at, sequence); see Ordering and dedup.
One endpoint per (business, environment). A Business has one sandbox endpoint and one production endpoint. (Fan-out to multiple endpoints per environment is on the roadmap; the API contract is shaped to add it non-breaking.)

Endpoints

The environment (sandbox or production) comes from the credential; there is no path-level selector.

Signing secret

The signing secret is generated by Cleff (format whsec_<env>_<random>) and returned exactly once on POST. Cleff stores it encrypted at rest but cannot reveal it back to you after creation; if you lose it, POST again to rotate.
POST is idempotent on (business, environment). Posting again rotates the secret (hard swap: old secret stops verifying immediately; a Stripe-style overlap window is on the roadmap).

Verifying a delivery

Reject any request where:
  • the header is missing, malformed, or the timestamp is outside a ±300s replay window;
  • the recomputed HMAC does not equal the provided v1 hex (use a constant-time compare);
  • the body has been parsed by middleware that mutated bytes; verify against the raw request body.
This is the same signature scheme Cleff uses to verify inbound provider webhooks: one HMAC construction platform-wide.

Payload envelope

Flat envelope; the event payload lives under data. The envelope shape is stable; data is versioned independently via api_version.

Ordering and dedup

Strict wire ordering over a retrying HTTP queue is not achievable, and head-of-line blocking would let one stuck delivery freeze every later event for the same Payout. So the contract is:
Receivers order same-Payout events by (occurred_at, sequence) and dedup on id.
  • occurred_at is the domain-time epoch, stamped when the state changed, not when Cleff happened to enqueue the delivery. Stable across retries.
  • sequence only breaks ties when two transitions stamp the same occurred_at.
  • id is the idempotency key; storing the highest-(occurred_at, sequence) you’ve applied per Payout makes the integration safe against duplicate delivery and out-of-order arrival.

Retry and dead-lettering

Failed deliveries retry with exponential backoff, up to 8 attempts, stretching the total window to several hours. Anything 2xx counts as delivered; anything else (including network failure) retries. After the budget is exhausted, the delivery lands on Cleff’s internal dead-letter surface and is investigated by Ops; the subscription is not auto-disabled in v1.

Event types (v1)

All v1 events carry a shared core projection plus an event-specific tail:
rail_ref is the provider’s opaque reference for the disbursement and is null until the provider returns one, typically populated by payout.disbursed, sometimes earlier. Cleff’s relationship with the upstream provider is internal: subscribers never see the provider name, the provider’s payout ID, or the provider’s raw lifecycle strings. Lifecycle position is conveyed by type.