Skip to content
API Referencev1

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.

RESTJSONBearer authIdempotentBTCLTCETHBNBTRXUSDTUSDC
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

  1. 1Create an account and add a merchant (one per store). Copy the API key it shows you; it is shown only once.
  2. 2From your server, POST /v1/invoices with the chain, asset and price.
  3. 3Redirect the customer to the paymentUrl in the response. It shows the address, amount, a QR code and live status.
  4. 4Check the invoice with GET /v1/invoices/{id}. Fulfil the order once its status is paid, overpaid or settled.
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>.

http
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.

Keep it server-side. Never put your key in browser JavaScript, a mobile app or a public repository. Anyone holding it can create invoices and read your balances (and, if you enabled it, withdraw from an allowed IP).

Base URL & format

text
https://api.coinsflow.app

Requests 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.

chainassetsblock timepayment seenpaid at (≤ $500 / above)settled atdecimals
bitcoinBTC~10 minseconds (mempool)1 / 268
litecoinLTC~2.5 minseconds (mempool)2 / 6128
ethereumETH · USDT · USDC12 sfirst block1232ETH 18 · tokens 6
bscBNB · USDT · USDCunder 1 sfirst block153018 (all, incl. USDT/USDC)
tronTRX · USDT · USDC3 sfirst block20406

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. 1 · pending
    You create the invoice and send the customer to paymentUrl.
  2. 2 · detected
    The customer sends. On Bitcoin and Litecoin we see it in the mempool within seconds; on the other chains in its first block.
  3. 3 · paid
    It reaches confirmationsRequired and is credited to your pending balance. Fulfil here.
  4. 4 · settled
    It reaches final depth and becomes withdrawable.

Typical time from sending to paid:

chaininvoice ≤ $500invoice 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 · assetfeenotes
litecoin · LTCVery lowUsually a fraction of a cent.
bsc · BNB, USDT, USDCVery lowCents or less, paid in BNB.
ethereum · ETH, USDT, USDCVariesPaid in ETH; token transfers cost more than plain ETH.
bitcoin · BTCVariesDepends on how busy the network is; usually low today.
tron · TRXLowCovered by free bandwidth for most wallets.
tron · USDT, USDCNoticeableBurns 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

statusmeaning
pendingCreated, waiting for the customer. Nothing sent yet.
detectedA 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.
confirmingIn a block, gaining confirmations.
underpaidConfirmed, but less than amountDue arrived (0.5% tolerance). The customer can top up to the same address.
paidReached confirmationsRequired and credited to your pending balance. Safe to fulfil the order.
overpaidLike paid, but more than amountDue arrived. The full amount is credited to you.
settledReached full finality. The amount is withdrawable.
expiredDeadline passed with nothing sent. Every address we issue is watched permanently, so a payment that arrives late is still detected and credited (paidLate: true).
cancelledCancelled 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

fieldtypemeaning
iduuidThe invoice id.
chain, assetstringNetwork and asset the customer must pay with.
addressstringA fresh address used by this invoice only.
amountDuedecimal stringWhat the customer must send, exactly. "0" on a pay-what-you-want invoice.
amountPaiddecimal stringSum of payments that are in a block. Mempool payments are not counted.
openAmountbooleanTrue for a pay-what-you-want invoice: there is no price, and any confirmed payment makes it paid. Read amountPaid for how much arrived.
statusstringSee Invoice lifecycle.
confirmationsRequiredintegerConfirmations at which it becomes paid. Set when created, from the chain and the invoice value.
orderId, descriptionstring | nullYours. The description is shown to the customer.
metadataobjectYour key/value pairs, returned as given. Never shown to the customer.
priceAmount, priceCurrencystring | nullThe USD price, when you priced it in USD.
usdValuestring | nullUSD value when created, for reporting.
paidLatebooleanTrue if the first payment was sent after expiresAt.
paidWithstring | nullHow 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, createdAtISO 8601UTC.
paymentUrlurlThe hosted payment page. Send the customer here.
paymentsarrayOnly 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 statusmeaning
mempoolBroadcast, not in a block yet. Shown to you and the customer; never counted or credited.
confirmingIn a block, gaining confirmations.
confirmedReached full finality.
orphanedNo 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).

json
"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

  1. Create the invoice from your server, with an Idempotency-Key (your order id works) so a retry never makes a second invoice.
  2. Store the invoice id against your order. Put your own references in orderId or metadata.
  3. Never fulfil because the customer came back from the payment page. Fetch GET /v1/invoices/{id} from your server and check status.
  4. Fulfil at paid, overpaid or settled. For something large and irreversible, wait for settled.
  5. Treat detected as “payment on its way”, not as paid. It can still be replaced or double-spent until it is in a block.
  6. Handle underpaid (ask for the rest, same address) and paidLate (the money arrived after the deadline; you decide whether to honour the price).
  7. 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.

json
{
  "error": "unsupported_asset",
  "detail": "USDT is not available on bitcoin. Supported: BTC."
}
statuserrormeaning
400invalid_requestBody or query failed validation; detail lists each problem.
400unsupported_assetThat asset is not available on that chain (e.g. USDT on bitcoin).
400invalid_amountAmount is not a positive decimal string with at most the asset’s decimals.
400invalid_cursorPass nextCursor back exactly as returned.
401unauthorizedMissing, wrong or revoked API key.
403merchant_inactiveDashboard only: this merchant is suspended. Over the API a suspended merchant’s key answers 401 unauthorized.
404not_foundNo such invoice for this merchant.
409idempotency_key_reuseThe Idempotency-Key was already used with a different body.
409idempotency_in_progressA retry arrived while the first request with that key was still running. Retry in a moment; you will get the same invoice.
409not_cancellableInvoice: a payment was already detected. Withdrawal: it is already being sent.
400invalid_destinationWithdrawal address is not valid for that network (checksum included).
400amount_too_smallBelow the minimum withdrawal for that asset.
400insufficient_balanceNot enough withdrawable (settled) balance for amount + fee; detail says how much is available.
403scope_requiredThis API key is not allowed to withdraw. Enable it in the dashboard.
403ip_not_allowedThe key has an IP allowlist and this request came from elsewhere.
403withdrawals_disabledWithdrawals are disabled for this merchant. Contact support.
503withdrawals_pausedWithdrawals 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.
503price_unavailableNo fresh USD price for that asset. Retry shortly, or send a crypto amount.
503chain_unavailableThat 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.
500internal_errorOur fault. Safe to retry with the same Idempotency-Key.
API reference / Invoices

Create an invoice

POSThttps://api.coinsflow.app/v1/invoices

Reserves a fresh address from our HD wallet and returns the invoice. Returns 201.

Parameters

chainstringRequired
bitcoin, litecoin, ethereum, bsc or tron.
assetstringRequired
BTC, LTC, ETH, BNB, TRX, USDT or USDC. Must be available on the chain.
amountstring
Exact crypto amount, e.g. "0.015", at most the asset’s decimals. Give exactly one of amount, priceAmount or openAmount.
priceAmountstring
Price in USD, up to 8 decimals, e.g. "49.99". Converted at a locked rate, rounded up by at most one base unit.
openAmountboolean
Send true for pay what you want: no price, the customer chooses how much to send. See Pay what you want.
orderIdstring
Your reference, up to 255 characters. Returned on the invoice and shown to the customer on the payment page (as “Order”), so do not put anything private in it; use metadata for that.
descriptionstring
Shown to the customer on the payment page. Up to 500 characters.
ttlSecondsinteger
How long the customer has to pay: 259200 (3 days) to 604800 (7 days); default 259200. Shorter values are raised to 3 days.
metadataobject
Up to 20 of your own key/value pairs. See Metadata.
Idempotency-Keyheader
Makes retries safe. See Idempotency.

Example

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"
  }'
201 CreatedResponse
{
  "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"
}
API reference / Invoices

Get an invoice

GEThttps://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

iduuidRequired
The invoice id.

Example

curl https://api.coinsflow.app/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34 \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "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"
    }
  ]
}
API reference / Invoices

List invoices

GEThttps://api.coinsflow.app/v1/invoices

Newest 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

statusstring
One status, or several comma-separated: paid,overpaid,settled.
chainstring
Only this chain.
orderIdstring
Exact order id.
qstring
Search: an order id, the address paid, or the start of an invoice id.
limitinteger
1 to 100; default 25.
cursorstring
nextCursor from the previous page.

Example

curl "https://api.coinsflow.app/v1/invoices?status=paid,overpaid,settled&chain=bitcoin&limit=50" \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "data": [ { "id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34", "status": "paid", "...": "..." } ],
  "nextCursor": "MjAyNi0wOS0yNiAxMDowMDowMC4xMjM0NTYrMDB8NGYxYzllMmEt..."
}
API reference / Invoices

Cancel an invoice

POSThttps://api.coinsflow.app/v1/invoices/{id}/cancel

Only while nothing has been detected. After that it returns 409 not_cancellable: money is already moving and the invoice must play out.

Parameters

iduuidRequired
The invoice id.

Example

curl -X POST https://api.coinsflow.app/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34/cancel \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "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"
}
API reference / Account

Balances

GEThttps://api.coinsflow.app/v1/balances

What 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"
200 OKResponse
{
  "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
    }
  ]
}
API reference / Account

Merchant profile

GEThttps://api.coinsflow.app/v1/me

The 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"
200 OKResponse
{
  "id": "b0c4…",
  "name": "My Shop",
  "status": "active",
  "feePercent": "0.50",
  "createdAt": "2026-09-20T12:00:00.000Z"
}
API reference / Account

Stats

GEThttps://api.coinsflow.app/v1/stats

Headline 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"
200 OKResponse
{
  "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.

errorwhen
403 scope_requiredThe key is not allowed to withdraw. Enable it in the dashboard.
403 ip_not_allowedThe 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. 1 · requested
    You ask. Amount + fee are reserved from your withdrawable balance.
  2. 2 · approved
    Automatically, up to your daily limit (USD 5,000 by default). Larger amounts are reviewed by our team first.
  3. 3 · broadcast
    Signed and sent, usually within a minute of approval. txid is set.
  4. 4 · confirmed
    Confirmed on chain. You get a webhook and an email.
statusmeaning
requestedReceived. Checked against your balance; the amount plus fee is reserved.
approvedApproved automatically (within your daily limit) or by our team.
signingYour balance has been debited; the transaction is being built and signed.
broadcastSent to the network. txid is set. Counting confirmations.
confirmedDone: confirmationsRequired reached.
failedCould 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.
cancelledCancelled by you or by our review before anything was sent. Nothing debited.
stuckSent 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):

assetminimum
BTC0.0002
LTC0.01
ETH0.002
BNB0.005
TRX20
USDT5
USDC5

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.

API reference / Withdrawals

Quote a withdrawal

GEThttps://api.coinsflow.app/v1/withdrawals/quote

The 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

chainstringRequired
bitcoin, litecoin, ethereum, bsc or tron.
assetstringRequired
An asset available on that chain.
amountstring
Decimal string: the amount you want to send. Gives the exact fee for it.
destinationstring
Where it will go. On TRON a first-time receiver costs more to reach, so include it for an exact fee.

Example

curl "https://api.coinsflow.app/v1/withdrawals/quote?chain=tron&asset=USDT&amount=250&destination=TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7" \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "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
}
API reference / Withdrawals

Create a withdrawal

POSThttps://api.coinsflow.app/v1/withdrawals

Needs 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

chainstringRequired
The network to send on.
assetstringRequired
What to send.
amountstringRequired
Decimal string: exactly what the destination receives. The fee is taken on top.
destinationstringRequired
An address on that network. Validated with its checksum.
maxFeestring
The highest fee you accept, usually the fee from your quote. Recommended.
orderRefstring
Your reference, up to 255 characters. Returned on the withdrawal and in webhooks.
Idempotency-Keyheader
Strongly recommended. See Idempotency.

Example

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"
  }'
201 CreatedResponse
{
  "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
}
API reference / Withdrawals

Get a withdrawal

GEThttps://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

iduuidRequired
The withdrawal id.

Example

curl https://api.coinsflow.app/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40 \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "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
}
API reference / Withdrawals

List withdrawals

GEThttps://api.coinsflow.app/v1/withdrawals

Newest first, paginated like invoices with nextCursor.

Parameters

statusstring
One status, or several comma-separated.
chainstring
Only this chain.
limitinteger
1 to 100; default 25.
cursorstring
nextCursor from the previous page.

Example

curl "https://api.coinsflow.app/v1/withdrawals?status=requested,approved,signing,broadcast&limit=50" \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "data": [ { "id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40", "status": "broadcast", "...": "..." } ],
  "nextCursor": null
}
API reference / Withdrawals

Cancel a withdrawal

POSThttps://api.coinsflow.app/v1/withdrawals/{id}/cancel

Only while it is requested or approved. Once it is being signed the network decides, and this returns 409 not_cancellable.

Parameters

iduuidRequired
The withdrawal id.

Example

curl -X POST https://api.coinsflow.app/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40/cancel \
  -H "Authorization: Bearer $COINSFLOW_API_KEY"
200 OKResponse
{
  "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.

http
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" }
    ]
  }
}
eventwhen
invoice.detectedA payment was seen (mempool on BTC/LTC, first block elsewhere). Not paid yet.
invoice.confirmingA payment is in a block and gaining confirmations.
invoice.paidPaid in full at the required confirmations. Fulfil the order.
invoice.overpaidMore than due arrived. Also fulfil; the surplus is credited to you.
invoice.underpaidLess than due arrived by the deadline. Ask for the rest (same address).
invoice.settledReached final depth; the funds are withdrawable.
invoice.expiredNothing arrived in time.
invoice.cancelledYou cancelled it.
withdrawal.broadcastThe withdrawal transaction was sent to the network. data.txid is set.
withdrawal.confirmedThe withdrawal is confirmed on chain.
withdrawal.failedIt 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:

attemptsent
1immediately
23 seconds later
330 seconds
45 minutes
51 hour
66 hours
724 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.

endpointdoes
GET /v1/webhooksYour endpoints (the secret is shown only as a hint here).
POST /v1/webhooksAdd 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}/testQueue a ping event now.
GET /v1/webhooks/{id}/deliveriesRecent deliveries with status, attempts, response. ?status=failed for failures only.
POST /v1/webhooks/{id}/deliveries/{deliveryId}/retrySend one again now.
POST /v1/webhooks/{id}/rotate-secretNew 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"
  }'
API reference / Public

Network status (public)

GEThttps://api.coinsflow.app/chains

No 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
200 OKResponse
{
  "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"
    }
  ]
}
API reference / Public

Prices (public)

GEThttps://api.coinsflow.app/prices

No 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
200 OKResponse
{
  "currency": "USD",
  "rates": { "BTC": "84125.00", "LTC": "73.74", "USDT": "0.9998", "...": "..." },
  "updatedAt": "2026-09-26T10:51:00.000Z"
}
API reference / Public

Platform status (public)

GEThttps://api.coinsflow.app/status

No 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
200 OKResponse
{
  "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

datechange
2026-10-01Small 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-01Automatic 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-01Balances 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-01Pay-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-30Pay 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-30Invoices 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-30GET /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-30Withdrawal 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-28Withdrawals 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-26Bitcoin 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.