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.coinspath.net
Auth: header Authorization: Bearer cf_live_…. Keys come from https://coinspath.net/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.coinspath.net/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.coinspath.netRequests 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. |
amountPaid | decimal string | Sum of payments that are in a block. Mempool payments are not counted. |
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. |
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.
| 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.
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 | This merchant is suspended. |
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. |
/v1/invoicesCreate an invoice
201.Parameters
| Name | Type | Description |
|---|---|---|
chainreq | string | bitcoin, litecoin, ethereum, bsc or tron. |
assetreq | string | BTC, LTC, ETH, BNB, TRX, USDT or USDC. Must be available on the chain. |
amount | string | Exact crypto amount, e.g. "0.015", at most the asset’s decimals. Give this or priceAmount. |
priceAmount | string | Price in USD, up to 8 decimals, e.g. "49.99". Converted at a locked rate, rounded up by at most one base unit. |
orderId | string | 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. |
description | string | Shown to the customer on the payment page. Up to 500 characters. |
ttlSeconds | integer | How long the customer has to pay. 60 to 604800; default 3600. |
metadata | object | Up to 20 of your own key/value pairs. See Metadata. |
Idempotency-Key | header | Makes retries safe. See Idempotency. |
Request
curl https://api.coinspath.net/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"
}'Response
{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "0",
"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,
"expiresAt": "2026-09-26T11:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinspath.net/invoice/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34"
}/v1/invoices/{id}Get an invoice
amountPaid sums payments that are in a block (unconfirmed mempool payments are not counted); compare status, not amounts, to decide when to fulfil.Parameters
| Name | Type | Description |
|---|---|---|
idreq | uuid | The invoice id. |
Request
curl https://api.coinspath.net/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34 \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "49.99",
"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,
"expiresAt": "2026-09-26T11:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinspath.net/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"
}
]
}/v1/invoicesList invoices
nextCursor is set; pass it back as cursor for the next page. Cursors are stable even while new invoices are being created.Parameters
| Name | Type | Description |
|---|---|---|
status | string | One status, or several comma-separated: paid,overpaid,settled. |
chain | string | Only this chain. |
orderId | string | Exact order id. |
q | string | Search: an order id, the address paid, or the start of an invoice id. |
limit | integer | 1 to 100; default 25. |
cursor | string | nextCursor from the previous page. |
Request
curl "https://api.coinspath.net/v1/invoices?status=paid,overpaid,settled&chain=bitcoin&limit=50" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"data": [ { "id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34", "status": "paid", "...": "..." } ],
"nextCursor": "MjAyNi0wOS0yNiAxMDowMDowMC4xMjM0NTYrMDB8NGYxYzllMmEt..."
}/v1/invoices/{id}/cancelCancel an invoice
409 not_cancellable: money is already moving and the invoice must play out.Parameters
| Name | Type | Description |
|---|---|---|
idreq | uuid | The invoice id. |
Request
curl -X POST https://api.coinspath.net/v1/invoices/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34/cancel \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"id": "4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34",
"chain": "tron",
"asset": "USDT",
"address": "TDCzDbXt9qSJrH9ZDyg4TBMjdTteKhQm5N",
"amountDue": "49.99",
"amountPaid": "0",
"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,
"expiresAt": "2026-09-26T11:00:00.000Z",
"createdAt": "2026-09-26T10:00:00.000Z",
"usdValue": "49.99",
"paymentUrl": "https://coinspath.net/invoice/4f1c9e2a-7b3d-4c1e-9a55-2d8e6f0b1c34"
}/v1/balancesBalances
pending is credited but not yet final; payable is final and withdrawable. Every supported pair is listed, including zeros.Request
curl https://api.coinspath.net/v1/balances \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"currency": "USD",
"totalUsd": "1234.56",
"assets": [
{
"chain": "tron", "chainName": "TRON",
"asset": "USDT", "assetName": "Tether", "decimals": 6,
"pending": "49.74", "payable": "1180.02", "total": "1229.76",
"priceUsd": 1, "valueUsd": "1229.76", "allocation": 99.6
}
]
}/v1/meMerchant profile
Request
curl https://api.coinspath.net/v1/me \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"id": "b0c4…",
"name": "My Shop",
"status": "active",
"feePercent": "0.50",
"createdAt": "2026-09-20T12:00:00.000Z"
}/v1/statsStats
Request
curl https://api.coinspath.net/v1/stats \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"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.
/v1/withdrawals/quoteQuote a withdrawal
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
| Name | Type | Description |
|---|---|---|
chainreq | string | bitcoin, litecoin, ethereum, bsc or tron. |
assetreq | string | An asset available on that chain. |
amount | string | Decimal string: the amount you want to send. Gives the exact fee for it. |
destination | string | Where it will go. On TRON a first-time receiver costs more to reach, so include it for an exact fee. |
Request
curl "https://api.coinspath.net/v1/withdrawals/quote?chain=tron&asset=USDT&amount=250&destination=TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"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
}/v1/withdrawalsCreate a withdrawal
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
| Name | Type | Description |
|---|---|---|
chainreq | string | The network to send on. |
assetreq | string | What to send. |
amountreq | string | Decimal string: exactly what the destination receives. The fee is taken on top. |
destinationreq | string | An address on that network. Validated with its checksum. |
maxFee | string | The highest fee you accept, usually the fee from your quote. Recommended. |
orderRef | string | Your reference, up to 255 characters. Returned on the withdrawal and in webhooks. |
Idempotency-Key | header | Strongly recommended. See Idempotency. |
Request
curl https://api.coinspath.net/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"
}'Response
{
"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,
"requestedAt": "2026-09-28T10:00:00.000Z",
"approvedAt": null,
"broadcastAt": null,
"confirmedAt": null,
"failedAt": null
}/v1/withdrawals/{id}Get a withdrawal
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.Parameters
| Name | Type | Description |
|---|---|---|
idreq | uuid | The withdrawal id. |
Request
curl https://api.coinspath.net/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40 \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"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,
"requestedAt": "2026-09-28T10:00:00.000Z",
"approvedAt": null,
"broadcastAt": null,
"confirmedAt": null,
"failedAt": null
}/v1/withdrawalsList withdrawals
nextCursor.Parameters
| Name | Type | Description |
|---|---|---|
status | string | One status, or several comma-separated. |
chain | string | Only this chain. |
limit | integer | 1 to 100; default 25. |
cursor | string | nextCursor from the previous page. |
Request
curl "https://api.coinspath.net/v1/withdrawals?status=requested,approved,signing,broadcast&limit=50" \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"data": [ { "id": "9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40", "status": "broadcast", "...": "..." } ],
"nextCursor": null
}/v1/withdrawals/{id}/cancelCancel a withdrawal
requested or approved. Once it is being signed the network decides, and this returns 409 not_cancellable.Parameters
| Name | Type | Description |
|---|---|---|
idreq | uuid | The withdrawal id. |
Request
curl -X POST https://api.coinspath.net/v1/withdrawals/9b2e7c10-5d4a-4f3b-8e21-6a0c1d2e3f40/cancel \
-H "Authorization: Bearer $COINSFLOW_API_KEY"Response
{
"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,
"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-26T11: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, with the same fields as the API. 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.coinspath.net/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"
}'/chainsNetwork status (public)
503 chain_unavailable on a network whose status is not ok, so a customer is never given an address nobody is watching.Request
curl https://api.coinspath.net/chainsResponse
{
"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"
}
]
}/pricesPrices (public)
Request
curl https://api.coinspath.net/pricesResponse
{
"currency": "USD",
"rates": { "BTC": "84125.00", "LTC": "73.74", "USDT": "0.9998", "...": "..." },
"updatedAt": "2026-09-26T10:51:00.000Z"
}/statusPlatform status (public)
Request
curl https://api.coinspath.net/statusResponse
{
"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" }
]
}Changelog
| date | change |
|---|---|
| 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.