CoinsFlow Payments API
Create invoices, send customers to a hosted payment page, and track payments on five blockchains: Bitcoin, Litecoin, Ethereum, BNB Smart Chain and TRON, including USDT and USDC. JSON over HTTPS; no SDK required.
For AI agents & LLM integrations
Base URL: https://api.coinsflow.app
Auth: header Authorization: Bearer cf_live_…. Keys come from https://coinsflow.app/apis/dashboard.
Amounts: always decimal strings, e.g. "0.015". Never JSON numbers.
Endpoints: POST /v1/invoices · GET /v1/invoices/{id} · GET /v1/invoices · POST /v1/invoices/{id}/cancel · GET /v1/balances · GET /v1/me · GET /v1/stats · GET /v1/withdrawals/quote · POST /v1/withdrawals · GET /v1/withdrawals/{id} · GET /v1/withdrawals · POST /v1/withdrawals/{id}/cancel · /v1/webhooks · GET /chains · GET /prices · GET /status
Webhooks: header CoinsFlow-Signature: t=…,v1=…; v1 = hex HMAC-SHA256(secret, "<t>.<raw body>"). Dedupe on the event id.
Statuses: new invoices are pending; fulfil at paid, overpaid or settled; detected is not paid yet.
Machine-readable: /openapi.json (OpenAPI 3.1) · /llms.txt
Getting started
- 1Create an account and add a merchant (one per store). Copy the API key it shows you; it is shown only once.
- 2From your server,
POST /v1/invoiceswith the chain, asset and price. - 3Redirect the customer to the
paymentUrlin the response. It shows the address, amount, a QR code and live status. - 4Check the invoice with
GET /v1/invoices/{id}. Fulfil the order once its status ispaid,overpaidorsettled.
curl https://api.coinsflow.app/v1/invoices \
-H "Authorization: Bearer $COINSFLOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"chain": "tron",
"asset": "USDT",
"priceAmount": "49.99",
"orderId": "order-1042",
"description": "Pro plan, 1 month"
}'Authentication
Send your key as a Bearer token on every request. Keys look like cf_live_<prefix>_<secret>.
Authorization: Bearer cf_live_3f9a0c1d2e4b5a6f_9sQ…We store only a hash of the secret, so a lost key cannot be shown again: rotate it in the dashboard, and the old key stops working immediately. Suspended merchants' keys are refused with 401. Keys cannot withdraw unless you allow it, and can be locked to your server's IP addresses: see Keys, permissions and IP allowlists.
Base URL & format
https://api.coinsflow.appRequests with a body must send Content-Type: application/json. Responses are JSON. Times are ISO 8601 in UTC.
Amounts
Every amount, in requests and responses, is a decimal string in whole units of the asset: "0.015" BTC, "49.99" USDT. Never send or parse them as JSON numbers; floating point cannot represent them exactly.
Give either amount (exact crypto) or priceAmount (USD). With a USD price we lock the current rate when the invoice is created and round the crypto amount up by at most one base unit, so you are never short.
Chains & assets
Use the chain id exactly as written. An invoice is paid at its confirmationsRequired, and settled (withdrawable) at the chain's final depth.
| chain | assets | block time | payment seen | paid at (≤ $500 / above) | settled at | decimals |
|---|---|---|---|---|---|---|
bitcoin | BTC | ~10 min | seconds (mempool) | 1 / 2 | 6 | 8 |
litecoin | LTC | ~2.5 min | seconds (mempool) | 2 / 6 | 12 | 8 |
ethereum | ETH · USDT · USDC | 12 s | first block | 12 | 32 | ETH 18 · tokens 6 |
bsc | BNB · USDT · USDC | under 1 s | first block | 15 | 30 | 18 (all, incl. USDT/USDC) |
tron | TRX · USDT · USDC | 3 s | first block | 20 | 40 | 6 |
USDT and USDC exist on three networks. They are different tokens with different contracts: a customer paying a tron USDT invoice must send TRC-20 USDT. The hosted page warns them about this.
How a payment flows
- 1 · pendingYou create the invoice and send the customer to paymentUrl.
- 2 · detectedThe customer sends. On Bitcoin and Litecoin we see it in the mempool within seconds; on the other chains in its first block.
- 3 · paidIt reaches confirmationsRequired and is credited to your pending balance. Fulfil here.
- 4 · settledIt reaches final depth and becomes withdrawable.
Typical time from sending to paid:
| chain | invoice ≤ $500 | invoice above $500 |
|---|---|---|
bitcoin | ~10 min (1 conf.) | ~20 min (2 conf.) |
litecoin | ~5 min (2 conf.) | ~15 min (6 conf.) |
ethereum | ~2.5 min | ~2.5 min |
bsc | ~15 s | ~15 s |
tron | ~1 min | ~1 min |
Small invoices on the two slow chains need fewer confirmations because reorganising even one block to steal them would cost far more than they are worth. Withdrawal always waits for the chain's full final depth, whatever the invoice.
What the customer pays in network fees (the fee is theirs, on top of the invoice amount):
| network · asset | fee | notes |
|---|---|---|
| litecoin · LTC | Very low | Usually a fraction of a cent. |
| bsc · BNB, USDT, USDC | Very low | Cents or less, paid in BNB. |
| ethereum · ETH, USDT, USDC | Varies | Paid in ETH; token transfers cost more than plain ETH. |
| bitcoin · BTC | Varies | Depends on how busy the network is; usually low today. |
| tron · TRX | Low | Covered by free bandwidth for most wallets. |
| tron · USDT, USDC | Noticeable | Burns energy: about 6.5 to 13 TRX per transfer unless the sender holds staked energy. |
For the lowest customer fees, offer Litecoin or BNB Smart Chain. Our addresses are native SegWit on Bitcoin and Litecoin, the cheapest kind to send to.
Invoice lifecycle
| status | meaning |
|---|---|
pending | Created, waiting for the customer. Nothing sent yet. |
detected | A payment is on the network. On Bitcoin and Litecoin that is seconds after the customer sends it (still in the mempool); elsewhere, in its first block. Not money yet. |
confirming | In a block, gaining confirmations. |
underpaid | Confirmed, but less than amountDue arrived (0.5% tolerance). The customer can top up to the same address. |
paid | Reached confirmationsRequired and credited to your pending balance. Safe to fulfil the order. |
overpaid | Like paid, but more than amountDue arrived. The full amount is credited to you. |
settled | Reached full finality. The amount is withdrawable. |
expired | Deadline passed with nothing sent. Every address we issue is watched permanently, so a payment that arrives late is still detected and credited (paidLate: true). |
cancelled | Cancelled by you before any payment. Anything sent to its address later is credited to you as a deposit. |
A payment that is still in the mempool keeps an invoice open past its deadline: it was sent in time. If it is dropped instead of mined (replaced by fee or double-spent), the invoice goes back to pending, or to expired if its time is up.
Block reorganisations are handled for you: if a confirmed payment is reorganised out of the chain, the invoice steps back to confirming and the credit is reversed. That is why you should treat paid as final only for goods you can take back, and wait for settled for anything irreversible and large.
The invoice object
| field | type | meaning |
|---|---|---|
id | uuid | The invoice id. |
chain, asset | string | Network and asset the customer must pay with. |
address | string | A fresh address used by this invoice only. |
amountDue | decimal string | What the customer must send, exactly. "0" on a pay-what-you-want invoice. |
amountPaid | decimal string | Sum of payments that are in a block. Mempool payments are not counted. |
openAmount | boolean | True for a pay-what-you-want invoice: there is no price, and any confirmed payment makes it paid. Read amountPaid for how much arrived. |
status | string | See Invoice lifecycle. |
confirmationsRequired | integer | Confirmations at which it becomes paid. Set when created, from the chain and the invoice value. |
orderId, description | string | null | Yours. The description is shown to the customer. |
metadata | object | Your key/value pairs, returned as given. Never shown to the customer. |
priceAmount, priceCurrency | string | null | The USD price, when you priced it in USD. |
usdValue | string | null | USD value when created, for reporting. |
paidLate | boolean | True if the first payment was sent after expiresAt. |
paidWith | string | null | How it was paid: "chain" (a transfer on the network), "coinsflow" (Pay with CoinsFlow, from a CoinsFlow balance), or null while nothing has been paid. |
expiresAt, createdAt | ISO 8601 | UTC. |
paymentUrl | url | The hosted payment page. Send the customer here. |
payments | array | Only on GET /v1/invoices/{id}. See Payments. |
Payments
GET /v1/invoices/{id} includes payments: each on-chain transfer to the invoice address, oldest first, with txid, amount, confirmations, status, blockHeight and seenAt.
ETH, BNB or TRX sent from inside a smart contract (a contract wallet, an exchange’s batch payout) is credited like any other payment. Such a transfer has no transaction of its own addressed to you, so its txid reads internal:… instead of a transaction hash: treat txid as an opaque id, not always something to look up in an explorer.
| payment status | meaning |
|---|---|
mempool | Broadcast, not in a block yet. Shown to you and the customer; never counted or credited. |
confirming | In a block, gaining confirmations. |
confirmed | Reached full finality. |
orphaned | No longer valid. With a blockHeight, a block reorganisation removed it (it is usually re-mined within minutes). With blockHeight null, it left the mempool without being mined: replaced by fee or double-spent. Either way it is not counted. |
Several payments to one invoice add up (a customer topping up after an underpayment). Anything sent to an invoice address in a different asset is still credited to you, as a deposit.
Pay what you want. For donations, tips and top-ups, create the invoice with openAmount: true and no amount or priceAmount. The payment page asks for any amount. The invoice goes to detected when a payment is seen, paid once it has its confirmations and settled at final depth, whatever the amount: it is never underpaid or overpaid. amountDue stays "0"; read amountPaid (and payments) to know how much arrived, and credit your customer for that. A later payment to the same invoice is credited to you and added to amountPaid, but the status does not change again, so no further webhook is sent for it. It must be asked for explicitly: a request with no amount and no openAmount is a 400, so a bug that drops your amount can never create an invoice anyone can settle with a cent. confirmationsRequired and usdValue are set from the amount once a payment is seen.
Pay with CoinsFlow. Customers who hold a CoinsFlow balance in the same asset on the same network can pay from it on the hosted page, confirmed with their password (and two-factor code, when on). There is nothing to confirm on-chain: the invoice goes through detected, paid and settled at once, with a webhook for each, and the amount (less your usual fee) is withdrawable immediately. Such an invoice has paidWith: "coinsflow" and an empty payments list. Handle it like any settled invoice; no change is needed in your integration.
Metadata
Attach up to 20 key/value pairs to an invoice with metadata. Keys are 1 to 40 characters; values are strings (up to 500 characters), numbers, booleans or null. They come back on every read of the invoice and are never shown to the customer (unlike orderId and description, which the payment page displays).
"metadata": { "customerId": "c_8812", "plan": "pro", "seats": 3 }Idempotency
Send an Idempotency-Key header (1 to 200 characters, e.g. your order id) when creating an invoice. Retrying with the same key and the same body returns the original invoice instead of creating a second one, even when retries run at the same moment: one of them creates it, the others get 409 idempotency_in_progress until it exists, then the same invoice. The same key with a different body is refused with 409 idempotency_key_reuse. Keys are kept for 30 days.
Fulfilling orders safely
- Create the invoice from your server, with an
Idempotency-Key(your order id works) so a retry never makes a second invoice. - Store the invoice
idagainst your order. Put your own references inorderIdormetadata. - Never fulfil because the customer came back from the payment page. Fetch
GET /v1/invoices/{id}from your server and checkstatus. - Fulfil at
paid,overpaidorsettled. For something large and irreversible, wait forsettled. - Treat
detectedas “payment on its way”, not as paid. It can still be replaced or double-spent until it is in a block. - Handle
underpaid(ask for the rest, same address) andpaidLate(the money arrived after the deadline; you decide whether to honour the price). - Use webhooks to hear about payments at once, verify the signature, and still confirm with
GET /v1/invoices/{id}before shipping anything valuable.
Errors
Errors return a JSON body with a stable machine-readable error code and a human detail.
{
"error": "unsupported_asset",
"detail": "USDT is not available on bitcoin. Supported: BTC."
}| status | error | meaning |
|---|---|---|
400 | invalid_request | Body or query failed validation; detail lists each problem. |
400 | unsupported_asset | That asset is not available on that chain (e.g. USDT on bitcoin). |
400 | invalid_amount | Amount is not a positive decimal string with at most the asset’s decimals. |
400 | invalid_cursor | Pass nextCursor back exactly as returned. |
401 | unauthorized | Missing, wrong or revoked API key. |
403 | merchant_inactive | Dashboard only: this merchant is suspended. Over the API a suspended merchant’s key answers 401 unauthorized. |
404 | not_found | No such invoice for this merchant. |
409 | idempotency_key_reuse | The Idempotency-Key was already used with a different body. |
409 | idempotency_in_progress | A retry arrived while the first request with that key was still running. Retry in a moment; you will get the same invoice. |
409 | not_cancellable | Invoice: a payment was already detected. Withdrawal: it is already being sent. |
400 | invalid_destination | Withdrawal address is not valid for that network (checksum included). |
400 | amount_too_small | Below the minimum withdrawal for that asset. |
400 | insufficient_balance | Not enough withdrawable (settled) balance for amount + fee; detail says how much is available. |
403 | scope_required | This API key is not allowed to withdraw. Enable it in the dashboard. |
403 | ip_not_allowed | The key has an IP allowlist and this request came from elsewhere. |
403 | withdrawals_disabled | Withdrawals are disabled for this merchant. Contact support. |
503 | withdrawals_paused | Withdrawals are paused platform-wide for maintenance. Your balance is safe; retry later. |
429 | (rate limited) | More than 50 requests per second from one IP (bursts up to 200 are absorbed). Back off and retry. |
503 | price_unavailable | No fresh USD price for that asset. Retry shortly, or send a crypto amount. |
503 | chain_unavailable | That network is paused (our watcher or the network itself is delayed), so we do not hand out an address nobody is watching. Try another network or retry shortly. |
500 | internal_error | Our fault. Safe to retry with the same Idempotency-Key. |
Create an invoice
https://api.coinsflow.app/v1/invoicesReserves a fresh address from our HD wallet and returns the invoice. Returns 201.
Parameters
chainstringRequiredassetstringRequiredamountstringpriceAmountstringopenAmountbooleanorderIdstringdescriptionstringttlSecondsintegermetadataobjectIdempotency-KeyheaderExample
curl https://api.coinsflow.app/v1/invoices \
-H "Authorization: Bearer $COINSFLOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"chain": "tron",
"asset": "USDT",
"priceAmount": "49.99",
"orderId": "order-1042",
"description": "Pro plan, 1 month"
}'{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "0",
"openAmount": false,
"status": "pending",
"orderId": "order-1042",
"description": "Pro plan, 1 month",
"metadata": { "customerId": "c_8812", "plan": "pro" },
"priceAmount": "49.99",
"priceCurrency": "USD",
"confirmationsRequired": 20,
"paidLate": false,
"paidWith": null,
"expiresAt": "2026-09-29T10:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinsflow.app/invoice/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34"
}Get an invoice
https://api.coinsflow.app/v1/invoices/{id}The current state. amountPaid sums payments that are in a block (unconfirmed mempool payments are not counted); compare status, not amounts, to decide when to fulfil.
Parameters
iduuidRequiredExample
curl https://api.coinsflow.app/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34 \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "49.99",
"openAmount": false,
"status": "paid",
"orderId": "order-1042",
"description": "Pro plan, 1 month",
"metadata": { "customerId": "c_8812", "plan": "pro" },
"priceAmount": "49.99",
"priceCurrency": "USD",
"confirmationsRequired": 20,
"paidLate": false,
"paidWith": "chain",
"expiresAt": "2026-09-29T10:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinsflow.app/invoice/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"payments": [
{
"txid": "8f3c1a…e9d2",
"amount": "49.99",
"confirmations": 21,
"status": "confirming",
"blockHeight": 76543210,
"seenAt": "2026-09-26T10:03:12.000Z"
}
]
}List invoices
https://api.coinsflow.app/v1/invoicesNewest first. When there are more, nextCursor is set; pass it back as cursor for the next page. Cursors are stable even while new invoices are being created.
Parameters
statusstringchainstringorderIdstringqstringlimitintegercursorstringExample
curl "https://api.coinsflow.app/v1/invoices?status=paid,overpaid,settled&chain=bitcoin&limit=50" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"data": [ { "id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34", "status": "paid", "...": "..." } ],
"nextCursor": "MjAyNi0wOS0yNiAxMDowMDowMC4xMjM0NTYrMDB8NGYxYzllMmEt..."
}Cancel an invoice
https://api.coinsflow.app/v1/invoices/{id}/cancelOnly while nothing has been detected. After that it returns 409 not_cancellable: money is already moving and the invoice must play out.
Parameters
iduuidRequiredExample
curl -X POST https://api.coinsflow.app/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34/cancel \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "0",
"openAmount": false,
"status": "cancelled",
"orderId": "order-1042",
"description": "Pro plan, 1 month",
"metadata": { "customerId": "c_8812", "plan": "pro" },
"priceAmount": "49.99",
"priceCurrency": "USD",
"confirmationsRequired": 20,
"paidLate": false,
"paidWith": null,
"expiresAt": "2026-09-29T10:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinsflow.app/invoice/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34"
}Balances
https://api.coinsflow.app/v1/balancesWhat you hold, per chain and asset, derived from the ledger. pending is credited but not yet final; payable is final. reserved is the part of payable already promised to a withdrawal being sent, and withdrawable (payable minus reserved) is what a new withdrawal can use. Every supported pair is listed, including zeros.
Example
curl https://api.coinsflow.app/v1/balances \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"currency": "USD",
"totalUsd": "1234.56",
"assets": [
{
"chain": "tron", "chainName": "TRON",
"asset": "USDT", "assetName": "Tether", "decimals": 6,
"pending": "49.74", "payable": "1180.02", "reserved": "0", "withdrawable": "1180.02", "total": "1229.76",
"priceUsd": 1, "valueUsd": "1229.76", "allocation": 99.6
}
]
}Merchant profile
https://api.coinsflow.app/v1/meThe merchant this key belongs to. Handy for checking a key works.
Example
curl https://api.coinsflow.app/v1/me \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"id": "b0c4…",
"name": "My Shop",
"status": "active",
"feePercent": "0.50",
"createdAt": "2026-09-20T12:00:00.000Z"
}Stats
https://api.coinsflow.app/v1/statsHeadline numbers for invoices that have been paid (paid, overpaid or settled), valued in USD when they were created.
Example
curl https://api.coinsflow.app/v1/stats \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"totalTurnover": "5120.00",
"incomeToday": "149.97",
"averageCheck": "42.67",
"conversionPct": 81.3,
"settledCount": 120,
"totalCount": 148
}Keys, permissions and IP allowlists
A new key can create and read invoices and read balances. It cannot move money. To withdraw over the API, open Developers in the dashboard, turn on Allow withdrawals with this key and list the IP addresses your server calls from. This needs two-factor authentication on your account, and your password.
With an allowlist set, the key is refused from any other address on every endpoint with 403 ip_not_allowed. Single addresses and CIDR blocks work, IPv4 and IPv6. Rotating the key keeps its permissions and allowlist.
| error | when |
|---|---|
403 scope_required | The key is not allowed to withdraw. Enable it in the dashboard. |
403 ip_not_allowed | The request came from an IP outside the key’s allowlist. |
Withdrawals
Send your settled (withdrawable) balance to any address on the same network. The amount you ask for is what arrives; a fee, quoted up front in the same asset, is taken from your balance on top.
The fee covers every network cost of the withdrawal. Usually that is one transaction. When the funds first have to be brought together on the network (for example a token balance spread over several deposit addresses), those extra transactions are priced in too, and the quote says how many (networkTransactions). Quote with the amount and destination you will use to get the exact fee, and pass it as maxFee when you create the withdrawal: you are never charged more than that. If network fees jump after the quote so that the fee no longer covers sending, the withdrawal is not sent and the amount and fee go back to your balance; quote again. If a withdrawal fails after network fees were already spent on it (for example, a transaction that failed on chain), the amount comes back and the fee pays for those network fees.
- 1 · requestedYou ask. Amount + fee are reserved from your withdrawable balance.
- 2 · approvedAutomatically, up to your daily limit (USD 5,000 by default). Larger amounts are reviewed by our team first.
- 3 · broadcastSigned and sent, usually within a minute of approval. txid is set.
- 4 · confirmedConfirmed on chain. You get a webhook and an email.
| status | meaning |
|---|---|
requested | Received. Checked against your balance; the amount plus fee is reserved. |
approved | Approved automatically (within your daily limit) or by our team. |
signing | Your balance has been debited; the transaction is being built and signed. |
broadcast | Sent to the network. txid is set. Counting confirmations. |
confirmed | Done: confirmationsRequired reached. |
failed | Could not be completed (rejected by the network, failed on chain, or network fees rose above the quoted fee before it was sent). The amount is refunded; so is the fee, unless network fees were already spent on it. failureReason says why. |
cancelled | Cancelled by you or by our review before anything was sent. Nothing debited. |
stuck | Sent but not confirming as expected. Our team is alerted and resolves it; it is never refunded while it could still confirm. |
Minimums (below these the fee would eat most of the amount):
| asset | minimum |
|---|---|
| BTC | 0.0002 |
| LTC | 0.01 |
| ETH | 0.002 |
| BNB | 0.005 |
| TRX | 20 |
| USDT | 5 |
| USDC | 5 |
Destination addresses are checked strictly, checksum included: a mistyped Bitcoin or TRON address, a Litecoin address on Bitcoin, or a mixed-case Ethereum address with a wrong EIP-55 checksum is refused with 400 invalid_destination before anything happens. You cannot withdraw to a CoinsFlow deposit address.
Quote a withdrawal
https://api.coinsflow.app/v1/withdrawals/quoteThe fee for a withdrawal made now, the minimum, and the most you can send (maxAmount: everything withdrawable, less the fee for sending that much). With amount the fee is exact for that amount; without it, it is the lowest fee (one transaction). Works with any key.
Parameters
chainstringRequiredassetstringRequiredamountstringdestinationstringExample
curl "https://api.coinsflow.app/v1/withdrawals/quote?chain=tron&asset=USDT&amount=250&destination=TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"chain": "tron",
"asset": "USDT",
"available": true,
"amount": "250",
"fee": "1.2",
"feeUsd": "1.20",
"feeSource": "live",
"networkTransactions": 1,
"minAmount": "5",
"withdrawable": "1180.02",
"maxAmount": "1178.82",
"confirmationsRequired": 20
}Create a withdrawal
https://api.coinsflow.app/v1/withdrawalsNeeds a key with withdrawals enabled, called from an allowlisted IP. Returns 201 with status requested. Send an Idempotency-Key: a retry then returns the same withdrawal instead of sending twice. With maxFee, a fee above it is refused with 409 fee_changed and nothing is created.
Parameters
chainstringRequiredassetstringRequiredamountstringRequireddestinationstringRequiredmaxFeestringorderRefstringIdempotency-KeyheaderExample
curl https://api.coinsflow.app/v1/withdrawals \
-H "Authorization: Bearer $COINSFLOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: payout-2026-09-28" \
-d '{
"chain": "tron",
"asset": "USDT",
"amount": "250",
"destination": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"maxFee": "1.2",
"orderRef": "payout-2026-09-28"
}'{
"id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40",
"chain": "tron",
"asset": "USDT",
"destination": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount": "250",
"fee": "1.2",
"total": "251.2",
"status": "requested",
"txid": null,
"networkFee": null,
"networkFeeAsset": null,
"confirmations": 0,
"confirmationsRequired": 20,
"usdValue": "249.95",
"orderRef": "payout-2026-09-28",
"failureReason": null,
"statusNote": null,
"automatic": false,
"requestedAt": "2026-09-28T10:00:00.000Z",
"approvedAt": null,
"broadcastAt": null,
"confirmedAt": null,
"failedAt": null
}Get a withdrawal
https://api.coinsflow.app/v1/withdrawals/{id}Current status, txid once sent, confirmations, and the network fee we paid. failureReason explains a failed or stuck withdrawal in plain words; statusNote explains one that is taking longer than usual. automatic is true when an automatic withdrawal set up in the dashboard made it.
Parameters
iduuidRequiredExample
curl https://api.coinsflow.app/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40 \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40",
"chain": "tron",
"asset": "USDT",
"destination": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount": "250",
"fee": "1.2",
"total": "251.2",
"status": "confirmed",
"txid": "c4d7e1…0a9b",
"networkFee": "13.8",
"networkFeeAsset": "TRX",
"confirmations": 20,
"confirmationsRequired": 20,
"usdValue": "249.95",
"orderRef": "payout-2026-09-28",
"failureReason": null,
"statusNote": null,
"automatic": false,
"requestedAt": "2026-09-28T10:00:00.000Z",
"approvedAt": null,
"broadcastAt": null,
"confirmedAt": null,
"failedAt": null
}List withdrawals
https://api.coinsflow.app/v1/withdrawalsNewest first, paginated like invoices with nextCursor.
Parameters
statusstringchainstringlimitintegercursorstringExample
curl "https://api.coinsflow.app/v1/withdrawals?status=requested,approved,signing,broadcast&limit=50" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"data": [ { "id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40", "status": "broadcast", "...": "..." } ],
"nextCursor": null
}Cancel a withdrawal
https://api.coinsflow.app/v1/withdrawals/{id}/cancelOnly while it is requested or approved. Once it is being signed the network decides, and this returns 409 not_cancellable.
Parameters
iduuidRequiredExample
curl -X POST https://api.coinsflow.app/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40/cancel \
-H "Authorization: Bearer $COINSFLOW_API_KEY"{
"id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40",
"chain": "tron",
"asset": "USDT",
"destination": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7",
"amount": "250",
"fee": "1.2",
"total": "251.2",
"status": "cancelled",
"txid": null,
"networkFee": null,
"networkFeeAsset": null,
"confirmations": 0,
"confirmationsRequired": 20,
"usdValue": "249.95",
"orderRef": "payout-2026-09-28",
"failureReason": "Cancelled by you.",
"statusNote": null,
"automatic": false,
"requestedAt": "2026-09-28T10:00:00.000Z",
"approvedAt": null,
"broadcastAt": null,
"confirmedAt": null,
"failedAt": null
}Webhooks
Add an endpoint in the dashboard (Developers) or with POST /v1/webhooks, and we POST a signed JSON event to it every time an invoice or withdrawal changes status. The event is queued in the same database transaction as the change, so none is ever lost.
POST /webhooks/coinsflow HTTP/1.1
Content-Type: application/json
User-Agent: CoinsFlow-Webhooks/1.0
CoinsFlow-Event: invoice.paid
CoinsFlow-Delivery: 5d0b8a52-6c1e-4f7a-9b3d-2e8f0a1c4b6d
CoinsFlow-Signature: t=1790590000,v1=5f3c9a0e...c21d
{
"id": "5d0b8a52-6c1e-4f7a-9b3d-2e8f0a1c4b6d",
"type": "invoice.paid",
"createdAt": "2026-09-26T10:05:40.000Z",
"previousStatus": "confirming",
"data": {
"object": "invoice",
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"status": "paid",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "49.99",
"orderId": "order-1042",
"metadata": { "customerId": "c_8812" },
"priceAmount": "49.99",
"priceCurrency": "USD",
"usdValue": "49.99",
"expiresAt": "2026-09-29T10:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"settledAt": null,
"payments": [
{ "txid": "8f3c1a…e9d2", "amount": "49.99", "confirmations": 20, "status": "confirming" }
]
}
}| event | when |
|---|---|
invoice.detected | A payment was seen (mempool on BTC/LTC, first block elsewhere). Not paid yet. |
invoice.confirming | A payment is in a block and gaining confirmations. |
invoice.paid | Paid in full at the required confirmations. Fulfil the order. |
invoice.overpaid | More than due arrived. Also fulfil; the surplus is credited to you. |
invoice.underpaid | Less than due arrived by the deadline. Ask for the rest (same address). |
invoice.settled | Reached final depth; the funds are withdrawable. |
invoice.expired | Nothing arrived in time. |
invoice.cancelled | You cancelled it. |
withdrawal.broadcast | The withdrawal transaction was sent to the network. data.txid is set. |
withdrawal.confirmed | The withdrawal is confirmed on chain. |
withdrawal.failed | It could not be completed. The amount and fee are back in your balance. |
data is the invoice (or withdrawal) as it is when we send: for an invoice the same fields as the API, plus settledAt; for a withdrawal the core fields (id, status, chain, asset, destination, amount, fee, txid, networkFee, confirmations, orderRef, failureReason and the timestamps). Fetch the object from the API when you need everything. Events can arrive out of order and more than once: use the event id to ignore repeats, and trust data.status over the event name. A ping event (from Send test) has {"type":"ping"} as its data.
Verifying signatures
Every request carries CoinsFlow-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> keyed with your endpoint's secret (whsec_…). Recompute it over the raw bytes, compare in constant time, and reject anything older than five minutes so a captured request cannot be replayed.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.COINSFLOW_WEBHOOK_SECRET; // whsec_...
// Verify over the RAW body: parsing and re-serialising JSON changes the bytes.
app.post("/webhooks/coinsflow", express.raw({ type: "application/json" }), async (req, res) => {
const parts = Object.fromEntries(
(req.get("CoinsFlow-Signature") ?? "").split(",").map((p) => p.split("=", 2)));
const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const ok = fresh && typeof parts.v1 === "string" && parts.v1.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body);
if (await alreadyHandled(event.id)) return res.sendStatus(200); // retries reuse the id
if (event.type === "invoice.paid") await fulfilOrder(event.data.orderId, event.data);
res.sendStatus(200); // answer quickly; do slow work in a queue
});Delivery and retries
Answer with any 2xx within 10 seconds. Anything else (an error status, a redirect, a timeout, a refused connection) is retried:
| attempt | sent |
|---|---|
| 1 | immediately |
| 2 | 3 seconds later |
| 3 | 30 seconds |
| 4 | 5 minutes |
| 5 | 1 hour |
| 6 | 6 hours |
| 7 | 24 hours, then it is marked failed |
You can see every delivery, its response and its next retry in the dashboard, and send any of them again. An endpoint that has failed without a single success for five days is paused (turn it back on in the dashboard). Endpoints must use https:// on the public internet; we do not follow redirects and never call private or internal addresses.
| endpoint | does |
|---|---|
GET /v1/webhooks | Your endpoints (the secret is shown only as a hint here). |
POST /v1/webhooks | Add one: url, events (default: paid, settled, expired, withdrawal confirmed and failed), description. Returns the secret. |
PATCH /v1/webhooks/{id} | Change url, events, description, or enabled. |
DELETE /v1/webhooks/{id} | Remove it. |
POST /v1/webhooks/{id}/test | Queue a ping event now. |
GET /v1/webhooks/{id}/deliveries | Recent deliveries with status, attempts, response. ?status=failed for failures only. |
POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry | Send one again now. |
POST /v1/webhooks/{id}/rotate-secret | New secret; the old one stops working immediately. |
curl https://api.coinsflow.app/v1/webhooks \
-H "Authorization: Bearer $COINSFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://shop.example.com/webhooks/coinsflow",
"events": ["invoice.paid", "invoice.settled", "invoice.expired", "withdrawal.confirmed", "withdrawal.failed"],
"description": "Order fulfilment"
}'Network status (public)
https://api.coinsflow.app/chainsNo key needed. Which networks are taking payments right now. New invoices are refused with 503 chain_unavailable on a network whose status is not ok, so a customer is never given an address nobody is watching.
Example
curl https://api.coinsflow.app/chains{
"chains": [
{
"chain": "litecoin", "name": "Litecoin", "assets": ["LTC"],
"status": "ok", "acceptingPayments": true,
"height": 3184650, "lastBlockAt": "2026-09-26T10:50:46.000Z",
"checkedAt": "2026-09-26T10:51:02.000Z"
}
]
}Prices (public)
https://api.coinsflow.app/pricesNo key needed. The latest USD price per asset, for showing estimates. Invoices priced in USD lock their own rate when created; do not compute amounts from this.
Example
curl https://api.coinsflow.app/prices{
"currency": "USD",
"rates": { "BTC": "84125.00", "LTC": "73.74", "USDT": "0.9998", "...": "..." },
"updatedAt": "2026-09-26T10:51:00.000Z"
}Platform status (public)
https://api.coinsflow.app/statusNo key needed. The same data as the status page: overall state, payments per network, withdrawals and webhooks, and history: 90 days per component, one entry per UTC day from start, each [share up, minutes degraded, minutes down] or null before recording began (shortened in the example).
Example
curl https://api.coinsflow.app/status{
"status": "operational",
"updatedAt": "2026-09-28T10:00:00.000Z",
"components": [
{ "id": "api", "name": "Payments API", "status": "operational" },
{ "id": "withdrawals", "name": "Withdrawals", "status": "operational" },
{ "id": "webhooks", "name": "Webhooks", "status": "operational" }
],
"chains": [
{ "chain": "litecoin", "name": "Litecoin", "status": "ok", "height": 3184650, "lastBlockAt": "2026-09-28T09:58:31.000Z" }
],
"history": {
"start": "2026-07-03",
"days": 90,
"uptime": { "api": 1, "withdrawals": 0.99965, "litecoin": 1 },
"series": {
"withdrawals": [null, null, [0.99931, 12, 1], [1, 0, 0]]
}
}
}Changelog
| date | change |
|---|---|
| 2026-10-01 | Small Bitcoin and Litecoin payments become withdrawable sooner. The depth at which an invoice goes from paid to settled now depends on what the payment is worth (up to $50 / up to $500 / up to $5,000 / above): Bitcoin 1, 2, 3 or 6 confirmations, Litecoin 2, 4, 6 or 12, and never before it is paid. A small payment can be paid and settled in the same block; invoice.paid and invoice.settled are both sent, paid first. Ethereum, BNB Smart Chain and TRON are unchanged. |
| 2026-10-01 | Automatic withdrawals: in the dashboard (Withdrawals), choose an asset, your own wallet address and when to send: as soon as the balance reaches an amount, or once a day or week. Each one is an ordinary withdrawal with the same fee, limits and webhooks; withdrawals gain automatic (true when a rule made it). A rule waits while the fee would be more than the share you set, and switches itself off, with an email, if a withdrawal is cancelled or three in a row fail. The hosted payment page no longer shows the merchant name or logo. |
| 2026-10-01 | Balances gain reserved and withdrawable (payable minus what a withdrawal in progress has already claimed). Webhook invoice objects now carry description, confirmationsRequired, paidLate and paymentUrl, and priceAmount in the same form as the API. An invoice that passes through several statuses at once (for example confirming, paid, settled) now sends an event for each, so invoice.paid is never skipped. A paid invoice that receives a second payment before it settles becomes overpaid and then settled. A short block reorganisation that leaves the payment in place no longer changes the invoice or repeats its events. |
| 2026-10-01 | Pay-what-you-want invoices: create one with openAmount: true instead of amount or priceAmount. The customer sends any amount; the invoice is detected, then paid as soon as that payment is confirmed (it is never underpaid or overpaid), and amountPaid says how much arrived. Invoices and webhook objects gain openAmount; webhook objects also gain paidWith. In the dashboard, leave the amount empty. |
| 2026-09-30 | Pay with CoinsFlow: on the hosted payment page, a customer with a CoinsFlow account can pay from their balance instead of from a wallet. The invoice goes straight to settled (the usual detected, paid and settled webhooks fire), final at once, and carries paidWith: "coinsflow". Invoices gain paidWith ("chain", "coinsflow" or null). |
| 2026-09-30 | Invoices stay open at least 3 days: ttlSeconds defaults to 259200 and anything shorter is raised to it (maximum still 7 days). With a USD priceAmount the rate stays locked for the whole time. |
| 2026-09-30 | GET /status adds history: a 90-day, per-day record of every service and network, recorded once a minute. The status page shows it as uptime bars. |
| 2026-09-30 | Withdrawal fees now cover every network cost of the withdrawal, including any transactions needed to bring the funds together before sending. GET /v1/withdrawals/quote takes amount and destination and returns the exact fee (and networkTransactions). POST /v1/withdrawals accepts maxFee: if the fee is higher now, nothing is created (409 fee_changed). Withdrawals carry statusNote, a plain explanation while one takes longer than usual. |
| 2026-09-28 | Withdrawals on all five networks (API and dashboard) with fees quoted up front, automatic approval up to a daily limit, and strict address validation. Signed webhooks for every invoice and withdrawal status, with retries for 24 hours and a delivery log. API keys can be limited to IP addresses; withdrawing over the API needs a key permission, an allowlist and two-factor. Two-factor authentication, password reset and email confirmation in the dashboard. Public GET /status. |
| 2026-09-26 | Bitcoin and Litecoin payments are detected in the mempool, seconds after the customer sends them (status detected; never counted until mined). Invoices worth at most $500 are paid after 1 confirmation on Bitcoin and 2 on Litecoin. New: metadata, several statuses in one list filter, idempotency_in_progress, and a 400 for Idempotency-Keys over 200 characters instead of silently truncating them. |
Coming next
Bitcoin Lightning: instant, very low-fee Bitcoin payments alongside the normal on-chain address.
Questions? Use the chat bubble at the bottom right of any page.
