Amounts and base units#
Send amounts as a person writes them, as a string: "amountDecimal": "49.99". Unheld scales the figure by the asset's own decimals.
Unheld returns amounts in base units, as strings: wei for ETH (18 decimals), satoshis for BTC (8), token units for USDC (6). "10000000000000000" wei is 0.01 ETH. Divide by 10^decimals to display them — GET /invoices/:id carries assetDecimals and assetSymbol for exactly that.
⚠️ Decimals differ per network for the same token. USDT has 6 decimals on Ethereum and 18 on BNB Chain. Never hard-code them; read them from
GET /chains/:chainId/assetsor from the invoice.
Networks and chain ids#
Every network has a chain_id, which every endpoint takes as chainId / chain_id:
| Family | chain_id examples |
|---|---|
| EVM | numeric: "1" Ethereum, "56" BNB Chain, "8453" Base, "137" Polygon, "11155111" Sepolia |
| Bitcoin | "bitcoin-mainnet", "bitcoin-testnet" |
| Tron | "tron-mainnet", "tron-nile" |
| XRP Ledger | "xrpl-mainnet", "xrpl-testnet" |
Read the live list with GET /chains rather than hard-coding it — which networks your account can use also depends on your plan.
Live and Test mode#
Every request acts in exactly one mode. In Live mode only main networks can be invoiced; in Test mode only test networks. An invoice on the wrong side is refused 422 ENVIRONMENT_MISMATCH. Lists (GET /invoices, balances) show that mode's records only — test money is never added to live money.
An API key carries its own mode — a Live key (cv2_live_…) or a Test key (cv2_test_…), chosen when you create it. A request made with a key acts in the key's mode, whatever the dashboard's Live/Test switch says, so a test key can never touch a main network. The dashboard switch decides only what you see in the dashboard. Keys created before test keys existed are Live keys (their text reads cv2_<prefix>_…).
The invoice lifecycle#
status |
What it means | Webhook you receive |
|---|---|---|
CREATED |
Open, waiting for payment. A partial payment keeps it open so the payer can top up (on Bitcoin a part-paid invoice reads PAID_UNCONFIRMED instead) |
invoice.created, then invoice.receiving / invoice.confirming as payments arrive |
PAID_UNCONFIRMED |
A payment has been seen and is still confirming — or, on Bitcoin, only part of the amount has arrived | invoice.receiving (first money) or invoice.confirming (more money) |
PAID_CONFIRMED |
Paid. The payment reached the confirmation depth and covers the amount | invoice.paid (or invoice.overpaid) |
PAID_LATE |
Paid, after expiry, inside the late-payment window | invoice.paid (or invoice.overpaid) |
EXPIRED |
The clock ran out before the invoice was covered — or you or the payer cancelled it | invoice.expired |
CLOSED |
An expired Bitcoin invoice that received nothing | — |
Fulfil the order on invoice.paid / invoice.overpaid — never on invoice.receiving, which only means money was seen.
displayStatus is the same state in plain words for showing a customer: pending, confirming, paid, overpaid, expired (underpaid appears only on old invoices settled before short payments stopped settling). To tell a part-paid invoice from an untouched one, read amountStatus.
Confirmations: paid, and safe to ship#
Two depths matter:
- The credit depth (
minConfirmationsin the create response;min_confirmationsonGET /chains) — the number of blocks after which the payment is credited andinvoice.paidfires. It is set per network: 12 blocks on Ethereum, BNB Chain, Base, Polygon and Lisk, 2 on Bitcoin, 19 on Tron, 1 on the XRP Ledger, 2 on every test network.GET /chainsgives the current figure. - The ship-safe depth (
finalConfirmations) — a deeper point beyond which a chain reorganisation is not a realistic risk. When the payment reaches it you receiveinvoice.finaland the invoice'sfinalizedAtis set. The default is the platform's reorg assumption for the network (20 blocks on EVM chains, 50 on Bitcoin); you can change it per network in Account → Payment currencies. On Tron and the XRP Ledger a credited payment is already final, soinvoice.finalfollowsinvoice.paidat once.
For digital goods, deliver on invoice.paid. If you ship physical goods and want to wait for depth, ship on invoice.final.
Short, extra and late payments#
- A short payment never settles an invoice. It stays open (
amountStatus: "UNDERPAID") so the payer can send the rest to the same address. If the clock runs out first you receiveinvoice.expiredwithpaidAmountTotalandamountStatus: "UNDERPAID". - An overpayment settles it and sends
invoice.overpaid;overpaidAmountholds the extra. - A late payment that arrives within the late-payment window after expiry (currently 4 hours) is still credited: once it reaches the credit depth and covers the invoice you receive
invoice.paidand the invoice readsPAID_LATE. A part-paid invoice keeps its address for the whole window so the payer can top up. - Refunds are yours to make, from your own wallet. Unheld never holds your funds.
amountStatus is always one of UNPAID, UNDERPAID, PAID, OVERPAID, measured against expectedAmount.
Where the payer sends money#
receiveType says what kind of address an invoice was given:
receiveType |
Address |
|---|---|
POOL |
One of your own receiving addresses on that network, held for this invoice alone until it is paid or expires |
BTC_DERIVED |
A fresh Bitcoin address derived from your Bitcoin key for this invoice only |
XRPL_TAGGED |
Your XRP Ledger account plus a destination tag unique to this invoice (receiveTag) |
CUSTOMER_STATIC |
A customer's permanent deposit address (see Customers) |
⚠️ On the XRP Ledger in tag mode, the address alone is an incomplete destination. Always show
receiveTagbesidereceiveAddress. A payment sent without it, or with a wrong tag, cannot be matched to the invoice: it is not credited, and is held for review — contact support with the transaction hash.