Skip to main content
Follow a payout from the moment you create it to the moment its money is final. For developers who create payouts and track them to completion.
Agents: fetch this page as plain markdown at https://docs.usecleff.com/concepts/payouts.md.
Four resources cooperate to move money: the Cleff Account funds are drawn from, the Payout that instructs the movement, the Decision that approves it, and the webhook events that report its progress. This page is the narrative; each resource’s reference page has the field-level detail. To do it step by step, follow Send a payout.

Fund the payouts

Payouts draw from your Cleff Account, and a shortfall surfaces when a payout is sent, not when it is created. Read the account’s balance and the deposit instructions to fund it with GET /v1/funding/account. A 404 means your Business has no Cleff Account yet: in sandbox, submit your business profile in the dashboard; in production, finish going live.

Follow a payout through its states

A payout is a single instruction: pay this Beneficiary (the payee), at this destination, this amount. It moves through an explicit state machine: Each state means one thing, and says what you do about it:

Name where the payout goes

A payout is paid to one of the Beneficiary’s destinations: a bank account or a debit card. Its destination_id names which one, and destination_kind says whether it is a bank or a card. destination_id is optional when you create the payout. Leave it out and the payout goes to the Beneficiary’s declared destination, the one they chose through a collection link. If they have declared none, it goes to the only destination they hold, and a Beneficiary who holds several needs you to name one. Beneficiaries & compliance has the full table of what you send and what happens. The destination is fixed when the payout is created. If the Beneficiary declares a different destination afterwards, their next payout goes there; this one does not move. Creation checks the payout up front: the Beneficiary must be compliant and the destination payout_usable. In production, your Business must not be blocked (BUSINESS_BLOCKED). A payout that fails a check is refused and nothing is created, so it is safe to correct and resend. The idempotency key you send makes creation safe to retry for 24 hours; see Idempotency.

Know which rail carries the money

The destination decides the Payout Rail, the network that carries the money: The dashboard’s Settings → Plan & Billing page lists your payout rails, which of them are active, and how to request the others.
A card payout your Business is not approved for is created and approved without complaint, then fails when it is sent. Check that card payouts are active on Plan & Billing before you pay a Beneficiary whose destination is a card.

Pass the approval gate

Nothing is sent without a Decision. A Decision is an approve or reject verdict recorded by a validator: a person on your team, or your own risk system calling the API. approve and reject are conveniences over decisions and share its guarantees. Every Decision carries a decision_id you mint, a ULID, which makes it safe to retry: resending it unchanged returns the original Decision with HTTP 200, and resending it with a different verdict is refused with IDEMPOTENCY_CONFLICT. Approving or rejecting a payout that is no longer pending_approval is refused with INVALID_STATE_TRANSITION. A Decision with status: "pending_review" is recorded without moving the payout: it stays in its current state until an approve or reject follows.
Rejection is terminal. A rejected payout can’t be approved later or replaced; create a new payout instead.
In production, a Business Cleff has blocked can’t create or approve payouts (BUSINESS_BLOCKED), but can still reject them, so nothing waiting for approval is stranded.

Know what happens after approval

Approval queues the payout to be sent, and sending happens asynchronously, so the payout stays approved while the payment is in flight. In production your balance is checked at this point, not before: a payout your Cleff Account can’t cover fails with failure_code: insufficient_funds. On the way, the provider may acknowledge the payout, give an expected delivery date, and assign rail_ref, the payment’s reference on the rail. The acknowledgment and the expected date arrive only as webhook events; rail_ref appears on the payout and in each event’s data. When the provider confirms the money has left, the payout reaches disbursed. disbursed is not the end. The receiving bank can still return the money, so the payout stays provisional until the return window closes and it becomes settled. The money arriving in the Beneficiary’s account is reported by payout.delivered, not by a state: a delivered payout is still disbursed until it settles. In sandbox, every payout that is approved reaches disbursed and stops there.

Replace a payout that did not arrive

A payout that is failed or returned can be answered with a replacement: a new payout, created with replaces_payout_id set to the original’s ID. Check replaceable on the original first. It is false when:
  • a replacement already exists, so creating another is refused with PREDECESSOR_ALREADY_REPLACED;
  • the failure is indeterminate, because the money may already have moved. Contact Cleff support instead.
Fix what the failure_code names before you replace the payout, or the replacement fails the same way. A replacement goes through the approval gate like any other payout.

Track payouts without polling

Webhooks report each step as it happens. There are ten payout.* events: No event is sent for creating or approving a payout, including one approved by a teammate in the dashboard; payout.disbursement_submitted is the first event after approval. To catch up on changes you did not see, list payouts with updatedSince. Payout events describes what each one carries.

Read amounts in minor and major units

The API speaks minor units: whole numbers of the currency’s smallest unit, in fields that end in _minor. For USD that is cents, so amount_minor: 50000 is $500.00. Integers keep amounts exact; there is no rounding on the way in or out. Webhooks speak major units. The amount field on every payout.* event is a decimal string in the currency’s own unit, carrying exactly as many decimal places as the currency uses. Jane Doe’s payout arrives as:
Both describe the same $500.00. To reconcile a webhook with the payout you created, match on payout_id rather than on the amount. If you do compare amounts, convert the string to minor units with a decimal parser, or by dropping the decimal point, and compare integers. Parsing it as a floating-point number can round it.

Next steps

Send a payout

Pay Jane Doe $500.00 and follow it to disbursed.