Payouts

A payout sends stablecoin out of your Settlane wallet to an address you name. It is the mirror of an invoice and it is deliberately not symmetrical: taking money in is fast and reversible-by-nobody, and sending money out goes through a policy, a screening and — often — a person.

Before your first payout

Creating one

POST /v1/payouts takes five required fields:

FieldNotes
customer_idYour customer, already created and (where required) verified.
asset_idFrom GET /v1/assets.
amount_centsUS cents, integer, 1 to 1,000,000,000.
destinationAn address for the asset's own chain. A malformed one is refused synchronously.
referenceYour id for this payout. The route is idempotent on reference — see below.

reference is the idempotency key, not the Idempotency-Key header. That is the one thing about this route that differs from POST /v1/invoices, and it is the safer default: sending the same money twice because a retry generated a new key is not a mistake worth leaving available. Use your own withdrawal id.

The response is the payout with a status. 201 does not mean the money has moved — it means the record exists and the policy has decided what happens next.

What can be refused, and when

These refusals happen synchronously, before any record moves:

CodeWhy
INVALID_REQUESTThe destination is malformed for that chain.
NOT_FOUNDcustomer_id names no customer of yours.
IDENTITY_NOT_VERIFIEDThe customer has not completed verification and your tenant requires it.
CUSTOMER_BLOCKEDAn operator blocked the customer.
PAYOUT_ADDRESS_SANCTIONED, PAYOUT_ADDRESS_BLOCKED, PAYOUT_ADDRESS_TETHER_FROZEN, PAYOUT_ADDRESS_CIRCLE_FROZEN, PAYOUT_ADDRESS_COUNTERPARTY_RISKScreening refused the destination, naming the check for your operator.

Show your customer one neutral sentence for every address refusal. A sanctioned party must not learn it is listed, and naming a block list teaches an attacker which address to swap in. The named codes appear on the money path (payout creation and execution, read by your own operator); the request stage (POST /v1/screenings) answers every one of them with the single code PAYOUT_ADDRESS_NOT_PERMITTED and one fixed sentence. See the errors reference. An address another customer of yours has used is not a refusal: the payout is created and waits for a person (pending_approval, hold_reason: shared_address), and approving it is the confirmation.

Four more are 503 and mean Settlane could not check, not that your customer was refused: SANCTIONS_LIST_STALE, TETHER_CHECK_UNAVAILABLE, CIRCLE_CHECK_UNAVAILABLE, COUNTERPARTY_RISK_CHECK_UNAVAILABLE. Retry them with the same body. Do not treat them as a refusal and do not route around them.

You can ask the question before you commit to it: POST /v1/screenings runs the request-stage address checks and records the answer. It takes no idempotency key and is deliberately not idempotent — each call records that the question was asked, possibly against a list that has changed since, and replaying a stored answer would hand you a judgement made against an older snapshot.

The lifecycle

StatusWhat it means
createdRecorded and not yet moving. The policy has not been applied.
pending_approvalA person has to approve this before it can be queued.
heldHeld by a time-based rule — a destination nobody has vouched for yet. An operator's approval is that vouching.
queuedApproved and waiting for the executor.
signingThe executor has it and the signer is checking it.
broadcastSigned and sent to the chain.
confirmingOn the chain and waiting for the confirmation rule.
completedSettled. The money has moved.
failedThe attempt failed. Nothing is assumed about whether the money moved — read the payout's events.
refusedScreening refused the destination. No transaction was ever built.
rejectedA person rejected it.
cancelledCancelled before it moved.

completed is the only status that means the money has moved. broadcast means a transaction is on the chain and not yet settled; failed means the attempt failed and says nothing about whether the money moved — read the payout's own event history (GET /v1/payouts/{id}/events) before you decide anything, and never re-send on a failed without looking.

Cancel is allowed until signing. POST /v1/payouts/{id}/cancel after that is refused with CONFLICT: once the signer has the transaction there is no longer a decision Settlane can make on your behalf.

Retrying a failed payout

Settlane never retries a payout by itself. An operator retries a failed one from the dashboard, and the retry is a new payout that names the old one:

If you key on reference, treat a payout whose retry_of is one of yours as the continuation of that one. The #r<n> suffix is reserved: POST /v1/payouts refuses a reference ending in it, so a retry's reference can never collide with one of yours. The dashboard refuses to retry a failed payout whose transaction Settlane cannot show did not pay — including a Tron payout whose every payload expired unincluded, which is dead but is not retried in this version; create a new payout under a new reference instead — and one whose family already has a retry in flight (a retry of a retry counts): a person reads the events, and if the money moved, records it with mark-settled instead.

Approvals, holds and why a payout waits

The policy runs on creation and decides queued, held or pending_approval. Four things put a payout in front of a person:

The policy is yours to set. Settings → Payout policy on the dashboard (the workspace owner, with a fresh authenticator code): whether a verified identity is required, the 24-hour auto limit, what makes a customer established, the new-address wait, the per-payout maximum, the daily cap, how many approvals a waiting payout needs, manual mode, the KYC minimum age (16–25), and whether — and within what caps — an API key may approve (see approving through the API). A money cap cannot exceed the signing cap the platform set for your workspace — and if the platform later lowers a cap beneath your policy, nothing is clamped: the signer refuses what the policy would auto-queue above it, and the Settings page says which field to lower. The minimum age is judged when a decision arrives, so an attempt in progress gets the value in force then and a customer already decided is never re-judged. Every change is recorded with its old and new value. Lowering approvals_required from two to one lets the person who already approved a waiting payout finalise it with a second press.

Approval is a person's, on the dashboard's Approvals queue — unless you switch on approval through the API. A scope is a credential your server holds, so an approval made with one is not "an approval by a person", and two required approvals cannot fix that on their own, because one server can hold two keys. That is why it is off by default, and why it is bounded when it is on.

Approving through the API

POST /v1/payouts/{id}/approve and POST /v1/payouts/{id}/reject approve or reject a payout that is waiting for a person, for an admin tool of your own — exactly as the dashboard's Approve and Reject do: the same events (payout.queued, or payout.cancelled with status: "rejected"), the same timeline, the same rules. Both take { "note": "...", "approved_by": "..." }: note is the reason, as on the dashboard; approved_by is your own name for the person who pressed the button (1–120 characters, recorded verbatim, e.g. "Admin console @alex").

Watching one

Two ways, and you want the first:

GET /v1/payouts/{id}/events is the payout's own history — what happened, in order, including every refusal. It is the first thing to read when a payout is not where you expected it.

Money is integers, everywhere

amount_cents is US cents as an integer on the way in and a decimal string on the way out; amount_base_units is the token amount in base units, also a string. There is no floating point anywhere in this API and no FX rate anywhere in Settlane — a stablecoin is 1:1 with the dollar by definition here. Parse amount_base_units with BigInt.

Next