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
- Payouts are set up for your account by Settlane. This is switched on per account. Ask [email protected].
- The destination's customer must be identity-verified. This is required by default; the workspace owner can change it in the payout policy. Settlane pays out to a customer's address, not to an anonymous one. See the KYC guide.
- The tenant must be activated, and your key needs
payouts:write. - Your hot wallet must hold the stablecoin it pays out, and native currency for gas. A payout is sent from your own hot wallet's balance: deposit sweeps land there (
sweep_destination), and a float you fund tops it up. With too little USDT the payout sits inqueuedand the dashboard raises an alert; with no gas nothing can be sent at all — watch forwallet.low_gas.
Creating one
POST /v1/payouts takes five required fields:
| Field | Notes |
|---|---|
customer_id | Your customer, already created and (where required) verified. |
asset_id | From GET /v1/assets. |
amount_cents | US cents, integer, 1 to 1,000,000,000. |
destination | An address for the asset's own chain. A malformed one is refused synchronously. |
reference | Your 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:
| Code | Why |
|---|---|
INVALID_REQUEST | The destination is malformed for that chain. |
NOT_FOUND | customer_id names no customer of yours. |
IDENTITY_NOT_VERIFIED | The customer has not completed verification and your tenant requires it. |
CUSTOMER_BLOCKED | An operator blocked the customer. |
PAYOUT_ADDRESS_SANCTIONED, PAYOUT_ADDRESS_BLOCKED, PAYOUT_ADDRESS_TETHER_FROZEN, PAYOUT_ADDRESS_CIRCLE_FROZEN, PAYOUT_ADDRESS_COUNTERPARTY_RISK | Screening 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
| Status | What it means |
|---|---|
created | Recorded and not yet moving. The policy has not been applied. |
pending_approval | A person has to approve this before it can be queued. |
held | Held by a time-based rule — a destination nobody has vouched for yet. An operator's approval is that vouching. |
queued | Approved and waiting for the executor. |
signing | The executor has it and the signer is checking it. |
broadcast | Signed and sent to the chain. |
confirming | On the chain and waiting for the confirmation rule. |
completed | Settled. The money has moved. |
failed | The attempt failed. Nothing is assumed about whether the money moved — read the payout's events. |
refused | Screening refused the destination. No transaction was ever built. |
rejected | A person rejected it. |
cancelled | Cancelled 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:
- it has the same customer, asset, amount and destination, a reference of
<original>#r<n>, andretry_ofset to the failed payout's id — on everypayout.*event it emits; - it is screened again and the policy runs again from the start, so it can wait for an approval like a first payout would;
- the failed payout stays
failed, with the retry on its timeline.
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:
- A destination nobody has vouched for yet. A new address waits. An operator pressing Approve is that vouching, which is why approving a
heldpayout releases it rather than making it wait out the clock. - A destination another of your customers has used. It goes to the queue as
shared_address; approving records that the address is shared on purpose, and the executor does not ask again. - The per-payout maximum, and the daily cap.
- Manual mode —
require_approval_for_all, for a tenant that wants every payout seen.
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").
- Off until the workspace owner switches it on, in Settings → Payout policy, with a fresh authenticator code. Off, both routes answer
FORBIDDENwithparamapi_approval_enabled. - Its own scope. The key needs
payouts:approve;payouts:writedoes not include it. Give it to a second key, held only by your admin tool — never to the server that creates payouts. The key that created a payout cannot approve it. - Two caps. A payout above
api_approval_max_cents(US$500 unless the owner changes it), or one that would take the workspace's API approvals overapi_approval_daily_centsin a rolling 24 hours (US$2,000), is refusedFORBIDDENwithparamnaming the cap, and keeps waiting for a person.0on either cap stops every approval through the API. What an approval spends is not given back if the payout is later rejected or cancelled. - One API approval per payout. With two approvals required, an API approval is one of them; the other is a person's, in the dashboard.
pending_approvalonly. Aheldpayout (the new-address wait) is not released or rejected by a key —CONFLICT. Your server can still cancel it.- Identity checks stay with a person. A payout waiting because the customer's identity verification is required (
hold_reason: kyc_required) is not approved by a key —FORBIDDENwithparamhold_reason; approve it in the dashboard. A key may still reject it. - One line of plain text.
noteandapproved_bymay not contain line breaks, control characters or formatting characters (such as right-to-left overrides or zero-width spaces), andapproved_byneeds at least one visible character —INVALID_REQUESTnaming the field. - Safe to retry. Approving a payout that is already approved, or that already carries this key's approval, answers its current state; so does rejecting one that is already rejected. Rejecting a payout that was approved is refused with
CONFLICT, as the dashboard refuses it. - Every use is told. Each approval and rejection is emailed to the workspace's owners and to Settlane, appears on the Alerts screen as a notice, and is in the audit log with the key, the name, the amount and the caps. If one surprises you, revoke the key in Developers → API keys.
Watching one
Two ways, and you want the first:
- Webhooks.
payout.pending_approval,payout.held,payout.queued,payout.broadcast,payout.completed,payout.failed,payout.refused,payout.cancelled. See the webhook reference.payout.refusedis the one to refund your customer on: no transaction was ever built and none will be. A rejection by an operator arrives aspayout.cancelled. Every payload is the payout object,referenceandretry_ofincluded. - Polling
GET /v1/payouts/{id}, which is fine for a one-off and wasteful as a strategy.
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
- The KYC guide — what verification a payout needs.
- The errors reference
- The API reference