Skip to main content

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.

HeaderTypeRequiredDescriptionExample
X-Api-KeystringYesGateway API key. Keep it server-side only.<your-api-key>
X-TimestampstringYesCurrent Unix time in seconds. Rejected (signature_stale) if more than 5 minutes from the server clock.1783369120
X-Signaturestring (hex)YesHMAC_SHA256(secret, X-Timestamp + "." + rawBody), hex-encoded. For GET requests the body is the empty string.a7b9…
Idempotency-KeystringConditionalRequired 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-TypestringConditionalapplication/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 in totalAvailable and are withdrawn by naming the pool on the cashout (route: "AML_HELD"). On gateways without AML screening every *Quarantined* figure is 0.

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
}
]
}
}
FieldTypeNullableDescriptionExample
data.items[]arrayNoOne entry per asset with a non-zero balance. Empty when nothing is settle-able.[ … ]
data.items[].assetIdstring (UUID)NoThe asset — pass it to POST /merchant/settlements.a1b2c3d4-…
data.items[].symbolstringNoAsset ticker.USDT
data.items[].namestringNoHuman-readable asset name.Tether
data.items[].typestring (enum)NoToken standard: NATIVE, ERC20, TRC20, or BEP20.TRC20
data.items[].networkIdstring (UUID)NoThe chain the asset settles on.9b1de2c8-…
data.items[].networkNamestringNoChain name.TRON
data.items[].networkSymbolstringNoNative-coin ticker of the chain.TRX
data.items[].totalAvailablestring (decimal)NoMatured, withdrawable now — the ceiling for a settlement of this asset. Decimal string — never a number.250.00
data.items[].totalAvailableUsdstring (decimal)NototalAvailable valued in USD. Decimal string.250.00
data.items[].paymentsintegerNoNumber of matured payments backing totalAvailable.12
data.items[].totalPendingAvailablestring (decimal)NoMaturing — settle-able once its waiting window elapses; not yet withdrawable. Decimal string.40.00
data.items[].totalPendingUsdstring (decimal)NototalPendingAvailable valued in USD. Decimal string.40.00
data.items[].pendingPaymentsintegerNoNumber of payments in the maturing bucket.2
data.items[].nextMaturityAtstring (RFC 3339)YesWhen the soonest maturing funds become withdrawable. null/omitted when nothing is maturing.2026-07-07T13:45:00Z
data.items[].totalQuarantinedAvailablestring (decimal)NoAML-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[].totalQuarantinedAvailableUsdstring (decimal)NototalQuarantinedAvailable valued in USD. Decimal string.120.00
data.items[].quarantinedPaymentsintegerNoNumber of held payments backing totalQuarantinedAvailable. Each reads amlState: "HELD" on verify.1
data.items[].totalQuarantinedReceivedstring (decimal)NoGross amount received into the held pool (before fees). Decimal string.120.00
data.items[].totalQuarantinedPendingAvailablestring (decimal)NoAML-held funds still inside the maturation window — not yet drainable. Decimal string.0
data.items[].quarantinedPendingPaymentsintegerNoNumber 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​

FieldTypeRequiredDescriptionExample
assetIdstring (UUID)YesThe asset to withdraw. Its network is used to price the fee and validate the address."<asset-uuid>"
amountstring (decimal)YesAmount you intend to withdraw, in asset units. Decimal string — never a number."20"
addressstringYesDestination 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
}
}
FieldTypeDescription
addressstringDestination echoed back.
assetNamestringHuman-readable asset symbol, e.g. USDT.
networkstringChain the payout would settle on, e.g. TRON.
amountstring (decimal)The amount you asked to quote.
feeLockedstring (decimal)Network fee that would be reserved, in the asset's units — the same figure a real cashout locks.
netAmountstring (decimal)amount − feeLocked — what would land at the destination.
sufficientbooleanfalse 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.
note

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.

Monthly-billed gateways

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​

FieldTypeRequiredDefaultDescriptionExample
assetIdstring (UUID)Yes—The asset to withdraw. Its network determines how the address is validated."<asset-uuid>"
amountstring (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"
routestring (enum)NoGREENWhich 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"
addressstringYes—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..."
labelstringNorandom api-…Names the auto-created address-book entry."payouts"
metadataobjectNo—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.

CodestatusWhat happened
200REQUESTEDThe amount was claimed from the routed pool and is queued for dispatch.
200PENDING_APPROVALClaimed, but held for sign-off — over your approval cap, or a destination screen that did not pass. Approve it to release.
202UNDERFUNDEDAccepted but parked: you asked for more than the routed pool holds. Nothing is claimed. It activates itself when the funds arrive. See Parked cashouts.
202WAITING_AML_CREDITAccepted 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:

routePays fromCeiling on balances
GREEN (default; also null/omitted)Your ordinary settle-able balance.totalAvailable
AML_HELDThe held pool — deposits our screening flagged, excluded from totalAvailable and visible as amlState: "HELD" on verify.totalQuarantinedAvailable
SMARTThe 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 in PENDING_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 the totalQuarantined* figures.
  • On a gateway without AML screening nothing is ever held, so totalQuarantinedAvailable is 0: route: "AML_HELD" is then an over-request like any other (409 insufficient_balance, or a park on an entitled gateway), and route: "SMART" has no verdict to route on and is refused with 409 screening_required — name the pool explicitly, or ask Finomesh to enable screening.
  • SMART never guesses. When no verdict is available it either refuses (409 screening_required) or, when the cause is an exhausted screening credit, parks as WAITING_AML_CREDIT — never a silent default to either pool.
  • route accepts only GREEN, AML_HELD, SMART (or nothing). Anything else is 422 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 202 and status: "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:

statusWaiting forClears when
UNDERFUNDEDFunds. shortfall says how much more must reach the routed pool.The balance covers it — checked about once a minute.
WAITING_AML_CREDITAML 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 (or PENDING_APPROVAL if it is over your cap) and dispatches normally. Amount, address and metadata are exactly what you sent.
  • shortfall / shortfallUsd are computed live on every read of an UNDERFUNDED cashout — GET /:id and GET / — and move with every credit that lands. They are absent (never 0) when we cannot read the balance at that moment, and absent on the 202 create response itself; read them back from the status endpoint.
  • POST /:id/retry re-checks the balance immediately instead of waiting for the next automatic pass. Optional — it is the same activation.
  • POST /:id/cancel withdraws 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 feeLocked and netAmount are 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-Key still returns the original settlement id on replay while the request is parked. Poll GET /merchant/settlements/:id or 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.

Handle every status you can receive

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:

FieldTypeNullableDescription
idstring (UUID)NoSettlement id — use it to poll this endpoint.
addressstringNoDestination address the payout is sent to.
assetNamestringNoHuman-readable asset symbol, e.g. USDT.
networkstringNoChain the payout settles on, e.g. TRON.
sourcestring (enum)NoAPI for cashout-API settlements.
statusstring (enum)NoPENDING_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.
routestring (enum)NoThe 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.
routedBystring (enum)YesSMART_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.
destinationRiskScoreintegerYesThe verdict SMART routed on. Present only alongside routedBy.
metadataobjectYesEcho of the metadata object you sent on create; omitted when none was supplied.
amountstring (decimal)NoGross amount requested, in the asset's units — exactly the figure you sent, on both routes.
feeLockedstring (decimal)NoNetwork 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.
netAmountstring (decimal)Noamount − feeLocked — what lands at the destination. Empty while parked.
receivedAmountstring (decimal)NoAmount actually sent on settlement (empty string until SETTLED).
shortfallstring (decimal)YesHow 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.
shortfallUsdstring (decimal)Yesshortfall valued in USD. Same presence rules.
expiresAtstring (RFC 3339)YesWhen a parked cashout is auto-rejected. Present only while parked.
txHashstringNoOn-chain transaction hash (empty string until SETTLED).
blockchainUrlstringNoReady-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​

ParamTypeDescription
statusstringOptional. One of PENDING_APPROVAL, UNDERFUNDED, WAITING_AML_CREDIT, REQUESTED, PROCESSING, SETTLED, REJECTED. An unknown value returns 422.
limitintOptional page size.
pageintOptional 1-based page number.
afterstringOptional 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​

FieldTypeRequiredDescription
totpCodestringWhen 2FA enrolledFresh 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:

CodeWhen
403 totp_not_enrolled2FA is not enabled on your account — it is required to approve over the API.
401 totp_required2FA is enabled but the code is missing or wrong.
409 conflictThe cashout is no longer awaiting approval (already approved, or cancelled by an operator).
404 not_foundNo such settlement under your account.
You approve — you cannot reject

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:

CodeWhen
409 conflictThe balance still falls short — it stays parked and keeps trying. The message says so.
409 conflictThe 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_foundNo 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:

CodeWhen
409 conflictThe cashout is no longer parked — it activated, expired, or was already cancelled.
404 not_foundNo such settlement under your account.

Errors​

All cashout error codes, with how to resolve each, are on the Cashout Errors page.