payout.* events. Seven follow a payout from the moment it’s sent until the money is final. Three tell you the money didn’t arrive and why. Webhooks lists when each one is sent.
Agents: fetch this page as plain markdown at
https://docs.usecleff.com/api-reference/webhook-events-payout.md.Disbursed, delivered and settled mean different things.
- Disbursed: the money has been sent. How soon the Beneficiary sees it depends on the rail: within seconds on an instant rail such as RTP, FedNow or a card, and up to a few business days on ACH.
- Delivered: the money is in the Beneficiary’s account. Use this event to tell someone their money has arrived. It’s sent again if the delivery time is later corrected.
- Settled: the payment can no longer be reversed, so the money is final.
Content
Everypayout.* event has these fields:
Some events add fields:
The
status in an event’s data names the step the event reports. It is not the payout’s own status, which has its own values: payout.disbursed carries status: "disbursed" while the payout reads in_transit. Read the payout for its status.
rail_ref is null on payout.rejected, and on payout.failed when the payout failed before it was sent. It’s often null on payout.canceled too.
Events don’t carry provider or provider_payout_id. GET /v1/payouts/{id} returns both: provider names who carried the payout for Cleff, and provider_payout_id is their reference for it. Treat both as opaque. Quote them to Cleff support, but don’t build logic on them, because their values can change without notice.
Example:
Why a payout failed
payout.failed and payout.returned include four fields that explain what happened:
To replace a payout, create a new one with
POST /v1/payouts and set replaces_payout_id to the failed payout’s ID. Check replaceable first.
indeterminate means Cleff doesn’t know the outcome. The money may already have moved.
When the rail gives a reason code, it’s in failure_detail, in that rail’s own vocabulary. For example, a closed account is reported as account_closed whether the reason was an ACH R02, an RTP or FedNow AC04, or a card network’s 41. The same code can mean different things on different rails, so use failure_code in your logic, not failure_detail. If there’s no code, or Cleff doesn’t recognise it, the failure is indeterminate.
Some failures block future payouts. After account_frozen, the Beneficiary is suspended and new payouts to them fail with BENEFICIARY_SUSPENDED. After account_closed, account_invalid or account_unauthorized, new payouts to that destination fail with DESTINATION_REPLACEMENT_REQUIRED until the Beneficiary gives a new one. Other codes don’t block anything.
replaceable tells you whether it’s safe to create a replacement payout. It’s false when the failure is indeterminate, because the original payout may still have gone through, and false once a replacement already exists. It doesn’t account for a suspended Beneficiary: after account_frozen it can be true, but the replacement is refused with BENEFICIARY_SUSPENDED.
The same four fields are on GET /v1/payouts/{id}, so you can still read a failure you missed.
Why a payout was rejected
payout.rejected carries the decision’s reasons. It has no failure_code or fault, because the payout was never sent.
Each
Reason has a code: MANUAL_REJECTION, BENEFICIARY_NOT_COMPLIANT, RULE_AMOUNT_OVER_LIMIT, RULE_NOT_APPLICABLE, AGENT_REJECTED or CUSTOM. Any message or details you sent with a decision aren’t included in the webhook. GET /v1/payouts/{id} returns your decisions in full.
replaceable is always false on payout.rejected. To try again, create a new payout.