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); the 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 POST /merchant/settlements — reuse the same key to safely retry. The same key with a different body returns 409 idempotency_conflict. Not used on GET. | <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 splits the funds into a matured bucket (withdrawable now — totalAvailable*) and a maturing bucket (still inside your settlement waiting window — totalPending*):
{
"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"
}
]
}
}
| 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 |
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 a settlement.
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.
POST /merchant/settlements
Request a settlement of an exact amount to a destination address.
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 and ≤ your matured available for that asset. Decimal string — never a number. | "250.00" |
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.
Response 201
Returns the settlement in the slim shape below. It starts
REQUESTED (so txHash / blockchainUrl are empty until it settles on-chain)
— or PENDING_APPROVAL when the amount exceeds your approval cap,
in which case it waits for a team member to approve it before dispatch:
{
"success": true,
"data": {
"id": "d4447446-45a0-4603-b04a-db88c40fc7d8",
"address": "T...",
"assetName": "USDT",
"network": "TRON",
"source": "API",
"status": "REQUESTED",
"amount": "250.00",
"feeLocked": "4.82",
"netAmount": "245.18",
"receivedAmount": "",
"txHash": "",
"blockchainUrl": ""
}
}
The 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.
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. REJECTED
is a cancellation — an operator/support action, not something you trigger over
the API. A settlement id that isn't yours returns 404.
Turning on a cashout approval cap means POST /merchant/settlements can return a
fifth status, PENDING_APPROVAL, for requests above the cap. If your
integration switches on status, handle it (treat it as "pending, not yet
dispatched") before enabling the cap.
Response shape
Both the create (201) 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 / REQUESTED / PROCESSING / SETTLED / REJECTED. PENDING_APPROVAL appears only when a cashout cap is set. |
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. |
feeLocked | string (decimal) | No | Network fee reserved for the payout, in the asset's units. |
netAmount | string (decimal) | No | amount − feeLocked — what lands at the destination. |
receivedAmount | string (decimal) | No | Amount actually sent on settlement (empty string until SETTLED). |
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) — 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, 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 parked cashout or leave it pending; there is no merchant reject over the API. Cancelling a parked cashout is an operator/support action. A cashout you never approve stays reserved — its funds are unavailable to settle — until it is approved or an operator cancels it.
Errors
All cashout error codes, with how to resolve each, are on the Cashout Errors page.