Cashout Endpoints
Endpoint reference for the cashout / settlement API. For the guided walkthrough — enabling the API, the security model, and per-step task pages — see the Cashout / Settlements section.
All cashout endpoints live under /api/v1/merchant/settlements, require the X-Api-Key header plus the signing headers below, and must originate from an allowlisted IP. The API is off by default — enable it first.
Headers
Every cashout request carries the API key plus the two signing headers (full walkthrough: Signing Requests); every money-moving POST additionally requires Idempotency-Key.
| Header | Type | Required | Description | Example |
|---|---|---|---|---|
X-Api-Key | string | Yes | Gateway API key. Keep it server-side only. | <your-api-key> |
X-Timestamp | string | Yes | Current Unix time in seconds. Rejected (signature_stale) if more than 5 minutes from the server clock. | 1783369120 |
X-Signature | string (hex) | Yes | HMAC_SHA256(secret, X-Timestamp + "." + rawBody), hex-encoded. For GET requests the body is the empty string. | a7b9… |
Idempotency-Key | string | Conditional | Required on every state-changing POST — create, approve, retry, cancel. Reuse the same key to safely retry; the same key with a different body returns 409 idempotency_conflict. Each endpoint has its own key namespace, so one key cannot replay across two of them. Not used on GET or on the read-only estimate-fee. | <uuid> |
Content-Type | string | Conditional | application/json on POST /merchant/settlements. | application/json |
GET /merchant/settlements/balances
Your cashout-able balance per asset (matured funds only), plus a "maturing" bucket for funds still inside the settlement waiting window.
TS=$(date +%s); BODY=""
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl https://api-staging.finomesh.com/api/v1/merchant/settlements/balances \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG"
Request body: None. For GET, sign the empty body (see the signing walkthrough).
Response — 200 OK
Balances are grouped by asset under data.items[]. Each item reports two pools:
- the ordinary settle-able pool ("green"), split into a matured bucket (withdrawable now —
totalAvailable*) and a maturing bucket (still inside your settlement waiting window —totalPending*); - the AML-held pool ("red",
totalQuarantined*) — deposits our screening flagged. These are never included intotalAvailableand are withdrawn by naming the pool on the cashout (route: "AML_HELD"). On gateways without AML screening every*Quarantined*figure is0.
The two pools are exact complements: a payment is in one or the other, never both.
An asset can appear here that you never offered on a payment: when a payer
sends a different asset than they selected (e.g. native TRX to a USDT deposit
address), the platform credits the deposit in the asset that actually arrived —
so a TRX row can show up alongside your USDT balances. It cashes out
exactly like any other row.
{
"success": true,
"data": {
"items": [
{
"assetId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"symbol": "USDT",
"name": "Tether",
"type": "TRC20",
"networkId": "9b1de2c8-7a41-4c8e-b3f2-6d5e4a3b2c1d",
"networkName": "TRON",
"networkSymbol": "TRX",
"totalAvailable": "250.00",
"totalAvailableUsd": "250.00",
"payments": 12,
"totalPendingAvailable": "40.00",
"totalPendingUsd": "40.00",
"pendingPayments": 2,
"nextMaturityAt": "2026-07-07T13:45:00Z",
"totalQuarantinedReceived": "120.00",
"totalQuarantinedAvailable": "120.00",
"totalQuarantinedAvailableUsd": "120.00",
"quarantinedPayments": 1,
"totalQuarantinedPendingAvailable": "0",
"quarantinedPendingPayments": 0
}
]
}
}
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
data.items[] | array | No | One entry per asset with a non-zero balance. Empty when nothing is settle-able. | [ … ] |
data.items[].assetId | string (UUID) | No | The asset — pass it to POST /merchant/settlements. | a1b2c3d4-… |
data.items[].symbol | string | No | Asset ticker. | USDT |
data.items[].name | string | No | Human-readable asset name. | Tether |
data.items[].type | string (enum) | No | Token standard: NATIVE, ERC20, TRC20, or BEP20. | TRC20 |
data.items[].networkId | string (UUID) | No | The chain the asset settles on. | 9b1de2c8-… |
data.items[].networkName | string | No | Chain name. | TRON |
data.items[].networkSymbol | string | No | Native-coin ticker of the chain. | TRX |
data.items[].totalAvailable | string (decimal) | No | Matured, withdrawable now — the ceiling for a settlement of this asset. Decimal string — never a number. | 250.00 |
data.items[].totalAvailableUsd | string (decimal) | No | totalAvailable valued in USD. Decimal string. | 250.00 |
data.items[].payments | integer | No | Number of matured payments backing totalAvailable. | 12 |
data.items[].totalPendingAvailable | string (decimal) | No | Maturing — settle-able once its waiting window elapses; not yet withdrawable. Decimal string. | 40.00 |
data.items[].totalPendingUsd | string (decimal) | No | totalPendingAvailable valued in USD. Decimal string. | 40.00 |
data.items[].pendingPayments | integer | No | Number of payments in the maturing bucket. | 2 |
data.items[].nextMaturityAt | string (RFC 3339) | Yes | When the soonest maturing funds become withdrawable. null/omitted when nothing is maturing. | 2026-07-07T13:45:00Z |
data.items[].totalQuarantinedAvailable | string (decimal) | No | AML-held, matured — withdrawable only by a cashout that names the held pool (route: "AML_HELD"). It is that route's ceiling exactly as totalAvailable is the green route's. Decimal string. | 120.00 |
data.items[].totalQuarantinedAvailableUsd | string (decimal) | No | totalQuarantinedAvailable valued in USD. Decimal string. | 120.00 |
data.items[].quarantinedPayments | integer | No | Number of held payments backing totalQuarantinedAvailable. Each reads amlState: "HELD" on verify. | 1 |
data.items[].totalQuarantinedReceived | string (decimal) | No | Gross amount received into the held pool (before fees). Decimal string. | 120.00 |
data.items[].totalQuarantinedPendingAvailable | string (decimal) | No | AML-held funds still inside the maturation window — not yet drainable. Decimal string. | 0 |
data.items[].quarantinedPendingPayments | integer | No | Number of payments in the held-and-maturing bucket. | 0 |
Each item also carries an aggregate lifetime-ledger breakdown (totalReceived, totalTxFee, totalCommission, and totalPendingReceived / totalPendingTxFee / totalPendingCommission for the maturing bucket) used by the dashboard; a merchant integration only needs totalAvailable to size an ordinary settlement and totalQuarantinedAvailable to size one against the held pool.
POST /merchant/settlements/estimate-fee
Preview the network fee for a cashout before requesting it — a dry run. It
returns the exact feeLocked and netAmount a real create of the
same body would produce, but creates nothing: no settlement, no address-book
entry, no balance is claimed. Use it to show the payer the fee, or to check a
withdrawal clears its own network fee before committing.
The request body is the same as create minus label (nothing is
stored). This is a read-only POST (it carries a body to sign), so it does
not require an Idempotency-Key.
BODY='{"assetId":"<asset-uuid>","amount":"20","address":"T..."}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api-staging.finomesh.com/api/v1/merchant/settlements/estimate-fee \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
-d "$BODY"
Request body
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
assetId | string (UUID) | Yes | The asset to withdraw. Its network is used to price the fee and validate the address. | "<asset-uuid>" |
amount | string (decimal) | Yes | Amount you intend to withdraw, in asset units. Decimal string — never a number. | "20" |
address | string | Yes | Destination address. Validated for the asset's chain (EVM EIP-55 checksum, TRON/UTXO base58check) — a malformed address returns 422 validation_error before any fee is quoted, same as create. The fee is destination-aware (e.g. on TRON a first-time recipient costs more energy), so quote against the real address you'll pay. | "T..." |
Response — 200 OK
{
"success": true,
"data": {
"address": "T...",
"assetName": "USDT",
"network": "TRON",
"amount": "20",
"feeLocked": "2.59",
"netAmount": "17.41",
"sufficient": true
}
}
| Field | Type | Description |
|---|---|---|
address | string | Destination echoed back. |
assetName | string | Human-readable asset symbol, e.g. USDT. |
network | string | Chain the payout would settle on, e.g. TRON. |
amount | string (decimal) | The amount you asked to quote. |
feeLocked | string (decimal) | Network fee that would be reserved, in the asset's units — the same figure a real cashout locks. |
netAmount | string (decimal) | amount − feeLocked — what would land at the destination. |
sufficient | boolean | false when feeLocked ≥ amount (a real cashout would be rejected as below its own network fee). The numbers are still returned so you can see why. |
The quote is a point-in-time estimate. Network fees move with chain conditions,
so the fee locked when you actually create the settlement may differ
slightly. A 503 fee_estimation_unavailable means the chain RPC was momentarily
unreachable — retry shortly.
On a monthly-billed gateway,
a token cashout deducts nothing: the quote returns feeLocked: "0",
netAmount equal to amount, and sufficient: true — the destination
receives exactly the requested amount, and the settlement fee appears on your
monthly statement instead. Native-coin cashouts quote and deduct the network
fee as above.
POST /merchant/settlements
Request a settlement of an exact amount to a destination address. amount is
always required, route picks which balance pays, and the HTTP status code
tells you what happened: 200 claimed, 202 parked, 409 refused.
BODY='{"assetId":"<asset-uuid>","amount":"250.00","address":"T...","label":"payouts"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api-staging.finomesh.com/api/v1/merchant/settlements \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
-d "$BODY"
Request body
| Field | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
assetId | string (UUID) | Yes | — | The asset to withdraw. Its network determines how the address is validated. | "<asset-uuid>" |
amount | string (decimal) | Yes | — | Exact amount to withdraw, in asset units. Must be > 0. Decimal string — never a number. Always required — there is no "withdraw everything" shape on this API; to withdraw the lot, read the pool's figure from balances and pass it. An amount over the routed pool's available balance is not a request error: see When you ask for more than you have. | "250.00" |
route | string (enum) | No | GREEN | Which balance pays this cashout — GREEN, AML_HELD, or SMART. Omit it (or send null) for the ordinary settle-able balance. See Fund pools and route. | "GREEN" |
address | string | Yes | — | Destination address. Validated for the asset's chain (EVM EIP-55 checksum, TRON/UTXO base58check). Stored in your address book on first use, tagged API; a repeat address is reused, not rejected. For EVM chains the match is case-insensitive — the same address sent checksummed or all-lowercase reuses one entry (stored lowercase). Base58 chains (TRON/BTC) are matched exactly. | "T..." |
label | string | No | random api-… | Names the auto-created address-book entry. | "payouts" |
metadata | object | No | — | Free-form one-level JSON object for your own tracking, echoed back on read and shown to the approver (key by key) when a request parks at your approval cap. Keys are not validated; total size capped at 4 KB. A requester key is shown first. | {"requester":"ops-team-alice","orderId":"1234"} |
Required headers
Idempotency-Key is required — reuse the same key to safely retry a request
without creating a second settlement. The same key with a different body
returns 409 idempotency_conflict.
Responses — 200 claimed, 202 parked
The status code is the outcome. Both return the settlement in the slim shape below; branch on the code, not on prose.
| Code | status | What happened |
|---|---|---|
200 | REQUESTED | The amount was claimed from the routed pool and is queued for dispatch. |
200 | PENDING_APPROVAL | Claimed, but held for sign-off — over your approval cap, or a destination screen that did not pass. Approve it to release. |
202 | UNDERFUNDED | Accepted but parked: you asked for more than the routed pool holds. Nothing is claimed. It activates itself when the funds arrive. See Parked cashouts. |
202 | WAITING_AML_CREDIT | Accepted but parked: a SMART or AML_HELD cashout whose destination screen could not run because your AML screening credit is exhausted. Top up and it screens and continues on its own. See Parked cashouts. |
409 | — | Refused — insufficient_balance and the rest are on Cashout Errors. |
{
"success": true,
"data": {
"id": "d4447446-45a0-4603-b04a-db88c40fc7d8",
"address": "T...",
"assetName": "USDT",
"network": "TRON",
"source": "API",
"status": "REQUESTED",
"route": "GREEN",
"amount": "250.00",
"feeLocked": "4.82",
"netAmount": "245.18",
"receivedAmount": "",
"txHash": "",
"blockchainUrl": ""
}
}
A claimed settlement is dispatched on-chain after your gateway's configured delay. Poll the status endpoint or rely on the settlement webhook for the terminal result.
Fund pools and route
Your balance can sit in two pools, and route says which one pays:
route | Pays from | Ceiling on balances |
|---|---|---|
GREEN (default; also null/omitted) | Your ordinary settle-able balance. | totalAvailable |
AML_HELD | The held pool — deposits our screening flagged, excluded from totalAvailable and visible as amlState: "HELD" on verify. | totalQuarantinedAvailable |
SMART | The destination decides. We screen the address once at create: a clean verdict draws from green, an over-threshold one draws from the held pool. routedBy: "SMART_ROUTE" on the response says we chose, and destinationRiskScore is the verdict we chose on. | whichever pool the verdict picked |
One rulebook, both pools. amount is exact, required, and means the same
thing on either route — the held pool is withdrawn in partial amounts exactly
like the green one, and over-asking behaves identically on both
(see below). Fees, the approval cap, destination screening and
the dispatch delay all apply unchanged.
{"assetId":"<asset-uuid>","address":"T...","amount":"120.00","route":"AML_HELD","label":"held-payouts"}
Notes:
- No operator approval for the held pool. Like every API cashout it starts
REQUESTED; only your own gates — the approval cap and destination screening — can park it inPENDING_APPROVAL, released with the usual approve call. - Once a held-pool cashout settles, the payments it drew on read
amlState: "RELEASED"on verify and leave thetotalQuarantined*figures. - On a gateway without AML screening nothing is ever held, so
totalQuarantinedAvailableis0:route: "AML_HELD"is then an over-request like any other (409 insufficient_balance, or a park on an entitled gateway), androute: "SMART"has no verdict to route on and is refused with409 screening_required— name the pool explicitly, or ask Finomesh to enable screening. SMARTnever guesses. When no verdict is available it either refuses (409 screening_required) or, when the cause is an exhausted screening credit, parks asWAITING_AML_CREDIT— never a silent default to either pool.routeaccepts onlyGREEN,AML_HELD,SMART(or nothing). Anything else is422 validation_error.
When you ask for more than you have
An amount over the routed pool's available balance is not a malformed
request — what happens depends on a per-gateway setting Finomesh controls:
- Parking enabled — the cashout is accepted with
202andstatus: "UNDERFUNDED". Nothing is claimed and nothing is reserved; it sits waiting for the balance to arrive and then activates itself. See Parked cashouts. - Parking disabled (the default) —
409 insufficient_balance, with the requested and available figures in the message. Request≤the pool's figure from balances.
Ask Finomesh to enable parking for your gateway if you want the first
behavior. Your integration should handle 202 regardless: a 200-only
assumption breaks the day it is switched on.
Parked cashouts
A parked cashout is a request we have accepted but not claimed. No funds are reserved, no fee is locked, and no balance is touched — it is a standing instruction that carries out itself when its blocker clears. Two statuses:
status | Waiting for | Clears when |
|---|---|---|
UNDERFUNDED | Funds. shortfall says how much more must reach the routed pool. | The balance covers it — checked about once a minute. |
WAITING_AML_CREDIT | AML screening credit, on a SMART or AML_HELD cashout whose destination screen could not run. | You top up your screening credit — from the dashboard, on the gateway's AML Statement tab, or from the parked cashout itself. We retry the screen on the same schedule, so the cashout resumes on its own once the credit lands. |
Both carry expiresAt. A parked cashout that is still blocked at that moment is
auto-rejected (REJECTED) and you are emailed — it does not sit forever. The
window is 7 days, sized for a funding round-trip.
- It activates itself. You do not have to do anything: when the shortfall
closes, the same request moves to
REQUESTED(orPENDING_APPROVALif it is over your cap) and dispatches normally. Amount, address and metadata are exactly what you sent. shortfall/shortfallUsdare computed live on every read of anUNDERFUNDEDcashout —GET /:idandGET /— and move with every credit that lands. They are absent (never0) when we cannot read the balance at that moment, and absent on the202create response itself; read them back from the status endpoint.POST /:id/retryre-checks the balance immediately instead of waiting for the next automatic pass. Optional — it is the same activation.POST /:id/cancelwithdraws the request. Because a parked cashout claims nothing, cancelling releases nothing, which is why this is a merchant action where cancelling a claimed settlement is not.- A parked cashout is priced when it activates, not when you send it — the
network fee is quoted fresh at that moment, so
feeLockedandnetAmountare empty while it waits. - There is a cap on how many parked cashouts one gateway may hold at once;
over it, a new request is refused with
409 too_many_parked_cashouts. Fund or cancel one first. - A request that could never activate is refused up front rather than parked —
below your settlement minimum (
422 below_minimum_settlement) or below its own network fee (422 amount_below_network_fee).
Approval cap
You can set a cashout approval cap — a USD ceiling — per gateway in the
dashboard (next to the signing secret). When a cap is set, any API cashout whose
USD value is strictly greater than the cap is created as PENDING_APPROVAL
instead of REQUESTED: the funds are reserved, but the payout is not
dispatched until it is approved. Approving moves it to REQUESTED and starts the
normal dispatch delay.
You can approve either from the dashboard settlement list or entirely over the
API — GET /merchant/settlements?status=PENDING_APPROVAL to find what is
waiting, then POST /merchant/settlements/:id/approve to release it
(2FA required). Approval is the only merchant action: a parked cashout you don't
approve simply stays reserved until you do.
- The cap is a manual sign-off step for large programmatic withdrawals — it does not change auth, signing, or idempotency.
- The
Idempotency-Keystill returns the original settlement id on replay while the request is parked. PollGET /merchant/settlements/:idor the list endpoint rather than re-POSTing to check whether it has been approved. - With no cap configured, every API cashout dispatches without approval and
responses never include
PENDING_APPROVAL— behavior is unchanged.
GET /merchant/settlements/:id
Status of one of your settlements.
TS=$(date +%s); BODY=""
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl https://api-staging.finomesh.com/api/v1/merchant/settlements/<settlement-id> \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG"
{
"success": true,
"data": {
"id": "d4447446-45a0-4603-b04a-db88c40fc7d8",
"address": "TRUpnEA3xuVCuLDZyFVXhCjdvTZf1gB13r",
"assetName": "USDT",
"network": "TRON",
"source": "API",
"status": "SETTLED",
"amount": "19",
"feeLocked": "4.82",
"netAmount": "14.18",
"receivedAmount": "19",
"txHash": "f0ee051f4fae2585bceed7320818e222c913c2931fecd3265cbc6d1255767850",
"blockchainUrl": "https://nile.tronscan.org/#/transaction/f0ee051f4fae2585bceed7320818e222c913c2931fecd3265cbc6d1255767850"
}
}
Statuses: REQUESTED → PROCESSING → SETTLED (terminal) or REJECTED
(terminal). Cashouts above your approval cap start at
PENDING_APPROVAL and move to REQUESTED once approved.
Parked cashouts (UNDERFUNDED, WAITING_AML_CREDIT) also converge on
REQUESTED — on their own, once the blocker clears. REJECTED is a
cancellation: an operator action, your own cancel of a parked cashout,
or a parked cashout that hit expiresAt. A settlement id that isn't yours
returns 404.
PENDING_APPROVAL appears once you set a cashout approval cap
or turn on destination screening; UNDERFUNDED and WAITING_AML_CREDIT appear
when a cashout parks. If your integration switches on status, treat
all three as "accepted, not yet dispatched" rather than falling through to an
error branch.
Response shape
The create (200/202) and this endpoint return the same slim object under
data:
| Field | Type | Nullable | Description |
|---|---|---|---|
id | string (UUID) | No | Settlement id — use it to poll this endpoint. |
address | string | No | Destination address the payout is sent to. |
assetName | string | No | Human-readable asset symbol, e.g. USDT. |
network | string | No | Chain the payout settles on, e.g. TRON. |
source | string (enum) | No | API for cashout-API settlements. |
status | string (enum) | No | PENDING_APPROVAL / UNDERFUNDED / WAITING_AML_CREDIT / REQUESTED / PROCESSING / SETTLED / REJECTED. PENDING_APPROVAL appears when a cashout cap is set or a destination screen did not pass; the two parked statuses when the cashout parks. |
route | string (enum) | No | The pool this settlement pays from — GREEN or AML_HELD (see above). Always present. Reads SMART only while a SMART request is parked in WAITING_AML_CREDIT: until the screen runs there is no pool to name. |
routedBy | string (enum) | Yes | SMART_ROUTE when we picked the pool from the destination verdict. Omitted when you named the pool yourself — so you can tell the two apart without keeping your own record of what you sent. |
destinationRiskScore | integer | Yes | The verdict SMART routed on. Present only alongside routedBy. |
metadata | object | Yes | Echo of the metadata object you sent on create; omitted when none was supplied. |
amount | string (decimal) | No | Gross amount requested, in the asset's units — exactly the figure you sent, on both routes. |
feeLocked | string (decimal) | No | Network fee reserved for the payout, in the asset's units. Empty on a parked cashout: the fee is quoted when it activates, not when you send it. |
netAmount | string (decimal) | No | amount − feeLocked — what lands at the destination. Empty while parked. |
receivedAmount | string (decimal) | No | Amount actually sent on settlement (empty string until SETTLED). |
shortfall | string (decimal) | Yes | How much more must reach the routed pool before an UNDERFUNDED cashout activates. Computed live on every read; omitted on any other status, and omitted rather than 0 when the balance could not be read. |
shortfallUsd | string (decimal) | Yes | shortfall valued in USD. Same presence rules. |
expiresAt | string (RFC 3339) | Yes | When a parked cashout is auto-rejected. Present only while parked. |
txHash | string | No | On-chain transaction hash (empty string until SETTLED). |
blockchainUrl | string | No | Ready-to-open block-explorer link for txHash (empty string until SETTLED). |
GET /merchant/settlements
List your cashouts with an optional status filter. Use it to find the cashouts
waiting for approval (?status=PENDING_APPROVAL) or parked
(?status=UNDERFUNDED) — and, because the response carries pagination.total,
to count them without pulling every row (?status=PENDING_APPROVAL&limit=1).
TS=$(date +%s); BODY=""
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl "https://api-staging.finomesh.com/api/v1/merchant/settlements?status=PENDING_APPROVAL&limit=20" \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG"
Query parameters
| Param | Type | Description |
|---|---|---|
status | string | Optional. One of PENDING_APPROVAL, UNDERFUNDED, WAITING_AML_CREDIT, REQUESTED, PROCESSING, SETTLED, REJECTED. An unknown value returns 422. |
limit | int | Optional page size. |
page | int | Optional 1-based page number. |
after | string | Optional cursor; overrides page when set. |
Response — 200 OK
data.items is an array of the slim settlement shape;
data.pagination.total is the total number of matches.
{
"success": true,
"data": {
"items": [
{
"id": "d4447446-45a0-4603-b04a-db88c40fc7d8",
"address": "T...",
"assetName": "USDT",
"network": "TRON",
"source": "API",
"status": "PENDING_APPROVAL",
"metadata": { "requester": "ops-team-alice", "orderId": "1234" },
"amount": "250.00",
"feeLocked": "4.82",
"netAmount": "245.18",
"receivedAmount": "",
"txHash": "",
"blockchainUrl": ""
}
],
"pagination": { "total": 1 }
}
}
POST /merchant/settlements/:id/approve
Approve a cashout that parked at your approval cap. Approving
moves it to REQUESTED and starts the normal dispatch delay.
Two-factor authentication is required. Enable 2FA on your account, then send a
fresh 6-digit code in the body. A merchant with no 2FA enrolled is refused with
403 totp_not_enrolled — enable 2FA, or approve from the dashboard. This keeps a
large payout behind a human second factor even though the request is signed with
your cashout secret.
TS=$(date +%s); BODY='{"totpCode":"123456"}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api-staging.finomesh.com/api/v1/merchant/settlements/<settlement-id>/approve \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG" \
-d "$BODY"
Request body
| Field | Type | Required | Description |
|---|---|---|---|
totpCode | string | When 2FA enrolled | Fresh 6-digit code from your authenticator app. |
Idempotency-Key is supported (and recommended) — reuse the same key to safely
retry without a double-approve.
Response — 200 OK
Returns the settlement in the slim shape with status now
REQUESTED. Error cases:
| Code | When |
|---|---|
403 totp_not_enrolled | 2FA is not enabled on your account — it is required to approve over the API. |
401 totp_required | 2FA is enabled but the code is missing or wrong. |
409 conflict | The cashout is no longer awaiting approval (already approved, or cancelled by an operator). |
404 not_found | No such settlement under your account. |
A merchant can only approve a cashout waiting at the approval cap, or leave
it pending; there is no merchant reject over the API. Cancelling an
approval-parked cashout is an operator/support action, because its funds are
already claimed. (A cashout parked as UNDERFUNDED or
WAITING_AML_CREDIT is different — it claims nothing, so you cancel it
yourself with POST /:id/cancel.) A cashout you never approve stays
reserved — its funds unavailable to settle — until it is approved or an operator
cancels it.
POST /merchant/settlements/:id/retry
Ask a parked UNDERFUNDED cashout to check its balance now,
instead of waiting for the next automatic pass. Nothing else differs: it is the
same activation the platform runs for you about once a minute, so calling this is
optional — an impatience shortcut after you have topped up, not a required step.
No 2FA and no body. Idempotency-Key is required (as on every money-moving
POST) — a replayed key returns the original response instead of a 409.
TS=$(date +%s); BODY=""
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api-staging.finomesh.com/api/v1/merchant/settlements/<settlement-id>/retry \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG"
Response — 200 OK
Returns the settlement in the slim shape, now REQUESTED (or
PENDING_APPROVAL if it is over your cap, or if the destination
re-screen did not pass). Error cases:
| Code | When |
|---|---|
409 conflict | The balance still falls short — it stays parked and keeps trying. The message says so. |
409 conflict | The cashout is no longer parked (already activated, cancelled, or expired), or it is waiting on AML credit rather than funds — top up your screening credit and it resolves itself. |
404 not_found | No such settlement under your account. |
POST /merchant/settlements/:id/cancel
Withdraw a parked cashout — either UNDERFUNDED or
WAITING_AML_CREDIT. The settlement becomes REJECTED.
This is a merchant action (unlike cancelling an approval-parked cashout) for one
reason: a parked cashout has claimed nothing and reserved nothing, so cancelling
it releases no money and there is no operator decision to protect. The moment it
activates, this stops applying to it — a cancel that races an activation loses
cleanly with a 409.
No 2FA and no body; Idempotency-Key is required.
TS=$(date +%s); BODY=""
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
curl -X POST https://api-staging.finomesh.com/api/v1/merchant/settlements/<settlement-id>/cancel \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Timestamp: $TS" -H "X-Signature: $SIG"
Response — 200 OK
Returns the settlement in the slim shape with status
REJECTED. Error cases:
| Code | When |
|---|---|
409 conflict | The cashout is no longer parked — it activated, expired, or was already cancelled. |
404 not_found | No such settlement under your account. |
Errors
All cashout error codes, with how to resolve each, are on the Cashout Errors page.