Invoices
This document describes invoices: how to create one with POST /v1/invoices, read its state with GET /v1/invoices/{id}, and interpret its statuses, amounts and fees. Read it before accepting the first payment.
An invoice is the expectation of a payment of a specific amount in a specific asset before a deadline. The merchant, called a project in the cabinet (Dashboard), creates an invoice for an order and either shows the customer the amount and the address or sends the customer to the checkout page. The customer is the person who pays. All times in requests and responses are ISO 8601 in UTC, for example 2026-09-11T21:03:05.531Z.
Assets
What the service accepts. The contract addresses were read from the network, not taken from articles. Which of these assets are available to your project right now is what GET /v1/assets says: the operator can switch an asset or a network off, for maintenance for example, and then they are not in the answer.
Assets on TRON
| Asset | Contract | Decimals | Minimum deposit | Confirmations | Payouts |
|---|---|---|---|---|---|
USDT_TRON |
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
6 | 1 USDT | 20 | yes |
USDC_TRON |
TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8 |
6 | 1 USDC | 20 | switched off by the operator |
TRX_TRON |
native coin | 6 | 1 TRX | 20 | no |
USDC_TRON is switched off by the operator entirely: Circle has stopped minting USDC on TRON. An invoice in USDC_TRON and an invoice with USDC_TRON in currencies are refused with 422 asset_payments_disabled; the checkout page does not offer it, and it is not in the assets of a permanent address; a payout in it is refused with 422 asset_payouts_disabled. Invoices in it that are already open are still paid, and a USDC transfer to a per_user permanent address is credited as before — see what arrives at an address.
Assets on the EVM networks
| Network | Asset | Contract | Decimals | Minimum deposit | Confirmations |
|---|---|---|---|---|---|
ethereum |
USDT_ETHEREUM |
0xdac17f958d2ee523a2206206994597c13d831ec7 |
6 | 1 USDT | 96 |
ethereum |
USDC_ETHEREUM |
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 |
6 | 1 USDC | 96 |
ethereum |
ETH_ETHEREUM |
native coin | 18 | 0.0001 ETH | 96 |
bsc |
USDT_BSC |
0x55d398326f99059ff775485246999027b3197955 |
18 | 1 USDT | 400 |
bsc |
USDC_BSC |
0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d |
18 | 1 USDC | 400 |
bsc |
BNB_BSC |
native coin | 18 | 0.0001 BNB | 400 |
polygon |
USDT0_POLYGON |
0xc2132d05d31c914a87c6611c10748aeb04b58e8f |
6 | 1 USDT0 | 4 |
polygon |
USDC_POLYGON |
0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 |
6 | 1 USDC | 4 |
polygon |
POL_POLYGON |
native coin | 18 | 0.0001 POL | 4 |
base |
USDC_BASE |
0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 |
6 | 1 USDC | 600 |
base |
ETH_BASE |
native coin | 18 | 0.0001 ETH | 600 |
arbitrum |
USDT0_ARBITRUM |
0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 |
6 | 1 USDT0 | 4800 |
arbitrum |
USDC_ARBITRUM |
0xaf88d065e77c8cc2239327c5edb3a432268e5831 |
6 | 1 USDC | 4800 |
arbitrum |
ETH_ARBITRUM |
native coin | 18 | 0.0001 ETH | 4800 |
optimism |
USDT0_OPTIMISM |
0x01bff41798a0bcf287b996046ca68b395dbc1071 |
6 | 1 USDT0 | 900 |
optimism |
USDC_OPTIMISM |
0x0b2c639c533813f4aa9d7837caf62653d097ff85 |
6 | 1 USDC | 900 |
optimism |
ETH_OPTIMISM |
native coin | 18 | 0.0001 ETH | 900 |
avalanche |
USDT_AVALANCHE |
0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7 |
6 | 1 USDT | 1 |
avalanche |
USDC_AVALANCHE |
0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e |
6 | 1 USDC | 1 |
avalanche |
AVAX_AVALANCHE |
native coin | 18 | 0.0001 AVAX | 1 |
- There are no payouts on the EVM networks: a payout in any of these assets is refused with
422 asset_not_payable. - USDT on Polygon, Arbitrum and OP Mainnet is USDT0, a separate contract with an asset identifier of its own. To the customer it is the same USDT; to your code it is a different
asset. - The decimals differ: USDT and USDC on BNB Smart Chain have 18, on the other networks 6. Do not hard-code this number.
- The confirmation count is the depth at which the service treats a payment on that network as final. It differs: from 1 on Avalanche to 4800 on Arbitrum, where a block comes about every 0.25 seconds.
Assets on TON
The contract is the jetton master of the token, in the raw form the catalog holds; the same contract in the user-friendly form is EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs:
| Asset | Contract | Decimals | Minimum deposit | Confirmations | Payouts |
|---|---|---|---|---|---|
USDT_TON |
0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe |
6 | 1 USDT | 1 | no |
GRAM_TON |
native coin | 9 | 0.05 GRAM | 1 | no |
A payment on TON needs one confirmation: TON produces a masterchain block about every 0.4 seconds, and the service counts only transactions the network has finalized, so a payment is confirmed within seconds. The address of a TON invoice is in the user-friendly form — see addresses on TON.
Assets on Solana
The contract is the mint of the token, in base58 and case-sensitive. Both mints belong to the classic SPL Token program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA:
| Asset | Contract | Decimals | Minimum deposit | Confirmations | Payouts |
|---|---|---|---|---|---|
USDT_SOLANA |
Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB |
6 | 1 USDT | 1 | no |
USDC_SOLANA |
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
6 | 1 USDC | 1 | no |
SOL_SOLANA |
native coin | 9 | 0.01 SOL | 1 | no |
- There are no payouts on Solana: a payout in any of these assets is refused with
422 asset_not_payable. - A payment on Solana needs one confirmation, because the service counts only what the network has settled at the commitment level the operator configured. By default that is
finalized, the only level that cannot be rolled back. - Solana has no chain id. A network of this family is told apart by its genesis hash, not by a number. The identifiers your code sees are the network ids of the registry:
solanafor the mainnet. - An address and a transaction signature on Solana are base58 and case-sensitive. The signature the service returns in
tx_hashis 86 to 88 base58 characters, not 64 hexadecimal digits. Never lower-case either of them — see addresses on Solana. - A token on Solana is held not at the owner's address but in its associated token account, a separate account for the pair (owner, mint). The service creates that account when it issues the address, so a customer's wallet pays to the address as usual and needs to know nothing about it.
Native coins
TRX_TRON, GRAM_TON, SOL_SOLANA and the native coins of the EVM networks pay invoices just as tokens do: a price in US dollars is converted into the coin at an exchange rate, like any asset with a rate — see price and exchange rate. There are no payouts in them: a payout is refused with 422 asset_not_payable.
A native coin of an EVM network is credited whether a wallet or a smart contract sent it: the value of the transaction itself, or a transfer a smart contract made within it — a smart-contract wallet, a multisig, an exchange's withdrawal. The service finds a transfer from a contract by tracing the block and credits it only when the trace accounts for the whole rise of the address's balance; one it cannot account for is not credited automatically: the invoice stays unpaid, no webhook is sent, and the operator resolves it by hand. A token is credited by its transfer event, however it was sent.
A transfer of a coin to the address of an invoice in another asset is recorded as unexpected_asset: it is not credited to the merchant and does not close the invoice — see what arrives at an address.
Test networks
An installation on a TRON test network (Nile or Shasta) accepts only USDT_TRON, with that network's contract. Its TON network is the TON testnet, with GRAM_TON only: USDT is issued on the TON mainnet only. Its Solana network is solanadevnet, the Solana devnet, with USDC and the native coin only; the asset identifiers and the mint there are the devnet's own — USDC on the devnet is 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU.
Rules that follow from the tables:
- A transfer below the minimum deposit of its asset is dust: the service records it but does not credit it to the merchant and does not count it toward any invoice. The exception is the rest of a
per_invoiceinvoice: once a payment to the invoice has arrived and the amount is not reached yet, a transfer in the invoice's asset to its address is a payment of any amount above zero — see partial payment. - An invoice priced in US dollars (created without
asset) can be paid in any asset the project accepts and the service has an exchange rate for;currenciesnarrows that list. Which assets those are at a given moment is whatGET /v1/assetssays. - Decimals, minimum deposits and confirmation counts come from the service's asset catalog. The code has none of these numbers built in.
Amounts
Amounts are passed as strings in units of the asset, for example "49.900000". JSON numbers are not used for amounts: parsing into a floating-point number loses cents.
- In a request, an amount is digits with an optional fractional part:
49,49.9,49.90,0.5. It is refused with400 validation_failedif it has a sign, an exponent, a space, a comma, a leading zero (049), a point without digits on both sides (49.,.9), or more fractional digits than the asset has. - In a response, every amount of an asset has exactly as many fractional digits as the asset has:
"49.900000"for an asset with 6 decimals. A price in US dollars (price.amount) always has 6. - Decimals. Do not build the number 6 into your code: count the digits after the point in
amount_expected, which always carries all the decimals of the invoice's asset. - Comparison. Compare amounts as decimal numbers, not as strings:
"49.9"in a request and"49.900000"in a response are the same amount.
List the assets
GET /v1/assets returns the assets the merchant can create an invoice in at the moment of the request, with the network, the contract, the decimals and the minimum deposit of each. Use it to decide which tokens to offer instead of building the list into your code.
Request
The request has no parameters and no body. The answer carries an ETag, so a product that keeps its list of coins up to date pays for one 304 while the catalog is unchanged — see conditional reads.
Response
200 with the array assets, in the order of the networks set by the operator, then by identifier. An asset is listed when all of these hold:
- the service accepts it for payments, and the operator has not disabled payments in it;
- it is not disabled in the project settings;
- its network is enabled, and the service issues addresses in that network now.
| Field | Type | Description |
|---|---|---|
asset |
string | Asset identifier: the value of asset in POST /v1/invoices |
symbol |
string | Ticker of the token, for example USDT |
network |
string | Network identifier, for example tron |
network_name |
string | Name of the network for people, for example TRON |
token_standard |
string | How wallets name the tokens of the network, for example TRC-20 |
contract |
string or null | Token contract as the service's catalog holds it: on TON, the jetton master in the raw form 0: and 64 hexadecimal digits; null for the native coin of the network |
decimals |
integer | Decimals of the asset: every amount in it has exactly this many fractional digits |
min_amount |
string | Minimum deposit with all the decimals of the asset: a smaller transfer is dust, other than the rest of a payment already started (see partial payment), and an invoice in the asset cannot be smaller |
confirmations |
integer | Confirmations a transfer of this asset needs before it is credited: it is what «2 of 20» on your own payment screen is drawn from |
icon |
string or null | The mark of the asset in the catalog, for your own list of coins; null when it has none |
- The list shows what can be offered now, not what is credited: a transfer in an asset that has dropped out of the list is handled by the rules of its address — see what arrives at an address.
- The list does not depend on the review of the merchant or on a paused address mode: those refuse
POST /v1/invoicesitself, with their own codes.
Errors
| HTTP | error.code |
When |
|---|---|---|
| 401 | unauthorized |
The API key is missing or invalid — see authentication |
| 403 | merchant_disabled |
The merchant is disabled |
| 429 | rate_limited |
The request limit is exceeded |
| 500 | internal_error |
An unhandled error of the service |
Example
curl -s https://api.stons.io/v1/assets \
-H "X-Api-Key: $API_KEY"
200 on the TRON mainnet:
{
"assets": [
{
"asset": "USDT_TRON",
"symbol": "USDT",
"network": "tron",
"network_name": "TRON",
"token_standard": "TRC-20",
"contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"decimals": 6,
"min_amount": "1.000000",
"confirmations": 20,
"icon": "usdt"
}
]
}
Create an invoice
POST /v1/invoices creates an invoice for an order, or returns the invoice that already exists for the same order_id — see idempotency.
Request
The body is a JSON object with Content-Type: application/json, no larger than 16 KiB. A field that is not in the table is refused with 400 validation_failed.
The request takes one query parameter, include, whose only value is qr: the response then carries the QR code of the address. Any other query parameter, and any other value, is refused with 400 validation_failed.
| Field | Type | Required | Description |
|---|---|---|---|
order_id |
string | yes | The merchant's order identifier, unique within the merchant: 1–200 visible ASCII characters (0x21–0x7E), no spaces. Idempotency key |
asset |
string | no | Asset identifier from the table, for example USDT_TRON. Without asset, the customer chooses the token on the checkout page, and amount is a price in US dollars — see the customer chooses the token |
price_currency |
string | no | Only together with asset; the one value is USD. With it, amount is a price in dollars rather than a number of coins: the service quotes it into the named asset at its own rate and answers with an invoice ready to be paid — see a price in dollars in the asset you name |
amount |
string | yes | With asset: the amount in units of the asset, not below its minimum deposit, with no more fractional digits than the asset has. With asset and price_currency: a price in US dollars. Without asset: a price in US dollars too. A price is above zero, with at most 6 fractional digits. Format — see amounts |
currencies |
array of string | no | Only without asset: the assets the customer chooses from, 1 to 50 identifiers; order and repeats do not matter. Without the field, the customer chooses from every asset the merchant and the service accept at the moment of the choice |
expires_in |
integer | no | Seconds from creation to the deadline: from 300 (5 minutes) to 259200 (72 hours). Default 3600 (1 hour). A JSON number, not a string |
external_user_id |
string | in per_user mode |
The merchant's user, in the same format as order_id. In per_user mode the invoice is paid to this user's permanent address. In per_invoice mode the field is only stored and returned |
return_url |
string | no | Where the checkout page sends the customer after payment, instead of the merchant's return URL. https:// only, at most 2000 characters, no @, no spaces, no invoice_id, order_id, status, ts or signature parameters, and on a site of the merchant — see return URLs |
cancel_url |
string | no | Where the "Return to the shop" button on the checkout page leads while the invoice is unpaid, and where the page sends the customer if the deadline passed without the full amount, instead of the merchant's cancel URL. The same rules as for return_url |
An optional field is omitted when it has no value: null is refused with 400 validation_failed.
Response
201 means the invoice was created, 200 means an invoice for this order_id had already been created earlier. The body is the same in both cases. Every field is always present; a field that does not apply is null.
| Field | Type | Description |
|---|---|---|
invoice_id |
string (UUID) | Invoice identifier |
order_id |
string | The order_id of the request |
status |
string | created, seen, partially_paid, confirmed, underpaid, expired, paid_late or settled — see statuses |
asset |
string or null | Asset identifier; null until the customer has chosen the token |
network |
string or null | Identifier of the network of the asset, for example tron; null until the customer has chosen the token |
network_name |
string or null | Name of the network for people, from the service's network registry, for example TRON; null until the customer has chosen the token |
address |
string or null | Payment address, in full: base58 on TRON; 0x and 40 hexadecimal digits in lower case on EVM networks; on TON, the user-friendly form starting with UQ (0Q on a test network) — see addresses on TON. null until the customer has chosen the token |
address_explorer_url |
string or null | The page of address on the explorer of the network; null until the customer has chosen the token, or if the network has no explorer link |
payment_uri |
string or null | A link that opens a wallet with the transfer filled in, for your own payment screen. null where the network has no link this service trusts, and null while the exact amount is not asked for: the invoice waits for money (created, or partially_paid with a rest), its deadline has not passed and its quote still holds. Today only TON has one |
qr |
object | Only with include=qr: address is the QR code of the payment address, an SVG image in a data: URL, or null while the customer has not chosen the token — see QR code. Without include=qr the field is absent |
price |
object or null | For an invoice priced in dollars — created without asset, or with price_currency: amount is the price in US dollars with 6 fractional digits, currency is USD. null for an invoice whose amount was given in coins |
currencies |
array of string or null | The currencies of the request, sorted and without repeats; null if the request had none |
asset_selected_at |
string or null | When the invoice got its token: the customer chose it, or the service chose the one you named when creating with price_currency. null for an invoice whose amount was given in coins, and until there is a choice |
quote |
object or null | For an invoice priced in dollars, once the token is settled: the exchange rate the amounts in the token were computed at and until when they hold — see the quote. With price_currency it comes in the answer to the creation itself. null for an invoice whose amount was given in coins |
amount_requested |
string or null | The merchant's price in units of the asset: the amount of the request, or the price converted into the chosen token at the exchange rate; null until the customer has chosen the token |
amount_expected |
string or null | The amount the customer must pay: amount_requested plus the service fee if the customer pays it — see fees. Only this amount closes the invoice. For an invoice created without asset, it can change after the quote window — see quote window. null until the customer has chosen the token |
amount_received |
string or null | Total of confirmed payments, on time and late; null until the customer has chosen the token |
amount_remaining |
string or null | amount_expected minus amount_received, but not below zero: how much more to send to the address. Unconfirmed transfers are not subtracted. After the deadline, a transfer of the rest does not make the invoice confirmed — see partial payment. null until the customer has chosen the token |
amount_credited |
string or null | amount_received minus fees.service.amount: how much is credited to the merchant; null until the customer has chosen the token |
fees |
object | The service fee and the network fee line — see fees |
external_user_id |
string or null | The external_user_id of the request; null if the request had none |
return_url |
string or null | The return_url of the request, as sent; null means the merchant's return URL applies |
cancel_url |
string or null | The cancel_url of the request, as sent; null means the merchant's cancel URL applies |
checkout_url |
string or null | The checkout page of this invoice; null if the installation has no checkout page |
expires_at |
string | Deadline: creation time plus expires_in |
created_at |
string | Creation time |
deposits |
array | Payments to the invoice — see retrieve an invoice |
Idempotency
order_id is the idempotency key. When a request arrives with an order_id that already has an invoice, the service compares the request with that invoice:
- The same request gets
200and the current state of the invoice. The request is the same when all of these match:- for an invoice created with
asset: the sameassetand the sameamountas a number, compared withamount_requested, not withamount_expected; - for an invoice created without
asset: noasset, the same price as a number, compared withprice.amount, and the same set ofcurrencies, ignoring order and repeats. This holds after the customer has chosen the token too; - in both cases the same
external_user_id,return_urlandcancel_url; a field that is absent matches only an absent field.
- for an invoice created with
- Any difference gets
409 order_id_conflict: it is a different payment. A request withassetfor an invoice created without it, and the reverse, is a difference. expires_inis not compared. A retry with another value gets the invoice with its originalexpires_at.
Concurrent requests with the same order_id create one invoice; the others get 200 or 409 by the same comparison. The response to a retry shows the terms recorded when the invoice was created, even if the fee rate or the merchant's settings have changed since.
Some checks run before the service looks for an existing invoice, so a retry that fails them gets the error, not the invoice:
- the request format, including the bounds of
expires_in(400 validation_failed); external_user_idinper_usermode (400 validation_failed);- with
asset: the asset is accepted for payments (422 asset_not_supported), the amount format and decimals (400 validation_failed), the minimum deposit (422 amount_below_minimum); - without
asset: the price format and that the price is above zero (400 validation_failed).
All other checks run only when no invoice exists for the order_id yet, so they never refuse a retry of a created invoice. These checks are: merchant approval, the platform's pause of the address mode, the asset switches of the operator and of the merchant, the checks of currencies and of the exchange rates, the return URLs, and the fee settings.
Errors
| HTTP | error.code |
When |
|---|---|---|
| 400 | validation_failed |
The body does not match the request table: a field is missing, unknown, null, of the wrong type or format; expires_in is out of bounds; currencies is sent together with asset. Or: amount has more fractional digits than the asset; the price is zero or has more than 6 fractional digits; external_user_id is missing in per_user mode; the amount together with the service fee exceeds 78 digits in minimal units |
| 400, 413, 415 | bad_request |
The body is not valid JSON or is empty (400), is larger than 16 KiB (413), or has an unsupported Content-Type (415) |
| 401 | unauthorized |
The API key is missing or invalid — see authentication |
| 403 | merchant_disabled |
The merchant is disabled |
| 403 | merchant_not_approved |
The merchant has not been approved after review yet, or was rejected |
| 409 | order_id_conflict |
An invoice with this order_id exists and describes a different payment |
| 422 | address_mode_disabled |
The platform has temporarily paused the address mode of the merchant — see a paused mode |
| 422 | asset_not_supported |
asset, or an asset from currencies, is unknown or not accepted for payments |
| 422 | amount_below_minimum |
With asset: amount is below the minimum deposit of the asset. Without asset: the price, converted at the current exchange rate, is below the minimum deposit of an allowed token |
| 422 | asset_payments_disabled |
The operator has temporarily disabled payments in asset or in an asset from currencies |
| 422 | asset_disabled_by_merchant |
Payments in asset or in an asset from currencies are disabled in the project settings |
| 422 | asset_required |
There is no asset, and the installation has no checkout page where the customer could choose the token |
| 422 | no_payable_assets |
There is neither asset nor currencies, and the merchant currently accepts no asset that has an exchange rate |
| 422 | asset_not_offered |
An asset from currencies has no exchange rate, so it cannot be offered for a price in US dollars |
| 422 | redirect_url_not_allowed |
return_url or cancel_url is not on a site of the merchant — see return URLs |
| 429 | rate_limited |
The request limit is exceeded |
| 500 | internal_error |
An unhandled error of the service |
| 503 | addresses_exhausted |
The address derivation indexes have run out: all 2^31 are used. Practically unreachable |
| 503 | network_fee_quote_unavailable |
The project still has the retired setting "the customer pays the network fee". The owner must save "Who pays the fees" in the project settings in the cabinet |
| 503 | rate_unavailable |
Without asset: none of the allowed tokens has a fresh exchange rate right now, so nobody could pay the invoice. No invoice is created |
| 503 | service_unavailable |
The service cannot derive a new address in its current configuration; the message says why |
What the client does for each code — see errors.
Example
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_1001", "asset": "USDT_TRON", "amount": "49.9", "expires_in": 3600}'
201:
{
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"order_id": "ord_1001",
"status": "created",
"asset": "USDT_TRON",
"network": "tron",
"network_name": "TRON",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"payment_uri": null,
"price": null,
"currencies": null,
"asset_selected_at": null,
"quote": null,
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "0.000000",
"amount_remaining": "49.900000",
"amount_credited": "0.000000",
"fees": {
"service": { "payer": "merchant", "percent": "1.00", "fixed": "0.000000", "amount": "0.000000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"external_user_id": null,
"return_url": null,
"cancel_url": null,
"checkout_url": "https://pay.example.com/pay/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"expires_at": "2026-09-11T21:03:05.531Z",
"created_at": "2026-09-11T20:03:05.531Z",
"deposits": []
}
A price in dollars in the asset you name
The shortest path when you keep prices in dollars and already know what you want to be paid in: one request, and the answer carries everything needed to pay. The service does the rate and the conversion.
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_2001", "asset": "SOL_SOLANA", "amount": "99", "price_currency": "USD", "expires_in": 3600}'
price_currency: "USD" means amount is a price in dollars rather than a number of coins. The field is optional: without it amount is still an amount in units of the asset, and nothing in your integration changes.
The answer carries what you show the customer right away:
address— where to pay;amount_expected— how many coins to send;price— the price in dollars, as you sent it;quote.rate— the rate the amount was computed at, andquote.expires_at— how long it holds;expires_at— the invoice deadline;qrwithinclude=qr.
Such an invoice needs no checkout page: you chose the token.
What happens to the rate afterwards
The amount is fixed by a quote. In a token pegged to the US dollar, such as USDT_TRON, it holds until the invoice's deadline and is never requoted. In any other token, when the quote's window runs out while the invoice still waits for money, the service requotes the remainder at the new rate: what arrived stays as it arrived, and what is still owed is counted in dollars at the invoice's rate. See quote window.
If there is no rate, or it is stale, the invoice is not created: the answer is 503 rate_unavailable and nothing is left in the database. An honest refusal beats an invoice with no address and no amount.
A repeat with the same order_id returns the same invoice with the same amount, as for any other invoice.
Showing «≈ 0.45 SOL» before you create anything
GET /v1/rates answers, for every asset of your project, with the rate the invoice will actually use: the market rate with the service's markdown already taken off. So the number you show in advance is the number the invoice carries.
curl -s https://api.stons.io/v1/rates -H "X-Api-Key: $API_KEY"
{
"rates": [
{
"asset": "SOL_SOLANA",
"symbol": "SOL",
"rate": "213.450000000000000000",
"market_rate": "220.050000000000000000",
"markdown_percent": "3.00",
"observed_at": "2026-10-06T12:31:07.218Z",
"fresh": true,
"currency": "USD"
}
]
}
| Field | What it means |
|---|---|
rate |
Dollars for one coin at the rate an invoice would use: the market rate less the service's markdown. null if the service has never accepted a rate for this coin |
market_rate |
The same rate before the markdown |
markdown_percent |
What is taken off a volatile coin; 0.00 for a coin pegged to the dollar |
observed_at |
When the rate was observed |
fresh |
false — the rate is stale: it is shown, but an invoice in this asset is refused right now with 503 rate_unavailable |
Change feed
GET /v1/invoices returns the invoices of the project in the order their state changed, oldest change first. It is the catch-up path: a webhook arrives in seconds and stays the main one, while the feed is for when the receiver was down longer than the seven deliveries cover.
Request
| Parameter | Meaning |
|---|---|
updated_since |
A moment in ISO 8601; invoices whose state changed at that moment or later are returned. Without it the feed starts at the project's first invoice |
status |
Return only invoices in this status |
limit |
Invoices per page: 1 to 100, 50 by default |
cursor |
The next_cursor of the previous page |
Any other query parameter is refused with 400 validation_failed; include is among them, so the feed never carries QR codes. The answer carries an ETag: a walk that finds nothing new costs one 304 without a body — see conditional reads.
Response
| Field | Type | Meaning |
|---|---|---|
invoices |
array | Invoices of the same shape GET /v1/invoices/{id} returns, without the deposits field and without qr |
next_cursor |
string or null | Pass it as cursor for the next page; null means this page is the last |
Transfers are left out on purpose: a page of fifty invoices would carry hundreds of rows nobody asked for, and the amounts of the invoice already say how much arrived. When transfers are wanted, read the invoice by its id.
A page ends at its last row and the next page starts strictly after it, so an invoice that changes during the walk never shifts a page — unlike an offset, where a row can be skipped or read twice. A cursor this service did not give out is refused with 400 validation_failed.
How to keep your orders in step:
- Handle webhooks — that is the fast path.
- Keep the moment of the last change you processed.
- If webhooks stopped arriving, walk the feed from that moment, page after page, until
next_cursorisnull, and move the moment forward.
The customer chooses the token
A request without asset creates an invoice without a token: the customer chooses the coin and the network on the checkout page and sees the amount and the address only after the choice.
- Price.
amountis a price in US dollars. At the choice the service converts it into the chosen token at an exchange rate — see price and exchange rate. - Checkout page. It is required. If the installation has none, the request is refused with
422 asset_required. - Allowed tokens with
currencies. At creation, each listed asset must be accepted for payments (422 asset_not_supported), enabled by the operator (422 asset_payments_disabled) and in the project settings (422 asset_disabled_by_merchant), and have an exchange rate (422 asset_not_offered). The price converted at the current rate must not be below its minimum deposit (422 amount_below_minimum). - Allowed tokens without
currencies. At creation, the service takes every asset that is accepted for payments, enabled by the operator and in the project settings, and has an exchange rate. If there is none, the response is422 no_payable_assets. Each asset taken must take the price, converted at the current rate, as at least its minimum deposit, with the same error as above. - No fresh rate at creation. An allowed asset whose rate is not fresh at this moment is not checked against its minimum deposit: the checkout page checks it when it offers the choice. If none of the allowed assets has a fresh rate, the request is refused with
503 rate_unavailableand no invoice is created: nobody could pay it right now. - What the page offers. Of the allowed tokens, the checkout page offers those that pass the same checks at the moment the page is shown, each with its amount at the current rate.
- Before the choice,
asset,network,address,quote,amount_requested,amount_expected,amount_received,amount_remaining,amount_credited,fees.service.fixedandfees.service.amountarenull, andpriceholds the price. The fee rate and the payers are recorded at creation, as for any invoice. - The choice is possible while the status is
createdand the service's clock has not reachedexpires_at. The choice is final: the token cannot be changed. At the choice the service records the asset, issues the address, converts the price into the token and records the first quote. It computesamount_expectedfrom the converted price the same way as for an invoice created in that asset: with the fee rate and the payers recorded at creation and the fixed part of the fee of that asset at the moment of the choice.asset_selected_atis the moment of the choice. From then on the invoice is paid like an invoice created with that asset, except that what is still owed is quoted again after the quote window. - No choice by the deadline. When the service's clock reaches
expires_at, the invoice becomesexpired, andinvoice.expiredarrives withassetand the amounts set tonull. Such an invoice cannot be paid: it has no address. per_usermode.external_user_idis required, as always, and after the choice the invoice gets the user's permanent address. A transfer to that address that appeared in a block before the choice does not pay the invoice: it is a top-up, credited to the merchant's balance with the top-up fee, anddeposit.confirmedarrives withinvoice_idset tonull. The checkout page shows the address only after the choice, so do not give the customer the permanent address to pay such an invoice outside the page.
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_1002", "amount": "49.9", "currencies": ["USDT_TRON"]}'
201:
{
"invoice_id": "8d2c7a4e-1b3f-4c5d-9e6f-7a8b9c0d1e2f",
"order_id": "ord_1002",
"status": "created",
"asset": null,
"network": null,
"network_name": null,
"address": null,
"address_explorer_url": null,
"payment_uri": null,
"price": { "amount": "49.900000", "currency": "USD" },
"currencies": ["USDT_TRON"],
"asset_selected_at": null,
"quote": null,
"amount_requested": null,
"amount_expected": null,
"amount_received": null,
"amount_remaining": null,
"amount_credited": null,
"fees": {
"service": { "payer": "merchant", "percent": "1.00", "fixed": null, "amount": null },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"external_user_id": null,
"return_url": null,
"cancel_url": null,
"checkout_url": "https://pay.example.com/pay/8d2c7a4e-1b3f-4c5d-9e6f-7a8b9c0d1e2f",
"expires_at": "2026-09-11T21:03:05.531Z",
"created_at": "2026-09-11T20:03:05.531Z",
"deposits": []
}
Price and exchange rate
The service converts a price in US dollars into a token at an exchange rate: the price of one whole unit of the token's coin in US dollars. The service collects the rates itself from a market data source and uses a rate only while it is fresh.
- Stablecoins too. A token pegged to the US dollar is converted at its own market rate, never one to one: at 0.9991 USD per USDT, a price of 100 USD is 100.090082 USDT.
- A token not pegged to the US dollar is converted at its market rate lowered by the platform's markdown, so the customer sends more coins. A token pegged to the US dollar has no markdown.
- Rounding up. The amount is rounded up to the step of the token, so the customer pays at least the price. The step follows what the token is pegged to. A token pegged to the dollar steps by its cent: a price of $229.00 at a rate of 0.9999 asks for 229.03, not 229.024488. A token pegged to nothing steps by 0.00000001 of its unit: a cent of ether would be 26 dollars, so it must not be rounded to one. The step is never finer than the token's own minimal unit.
- Any price with up to 6 decimals can be converted: there is no refusal because a price cannot be written exactly in a token.
- Which rate. The rate the checkout page showed the customer, if it is still fresh at the choice; otherwise the latest fresh rate. The amount recorded at the choice is the amount shown while the shown rate is fresh.
- The service fee is computed from the converted price, as for an invoice created in that token — see fees.
- Compare your price with
price.amount.amount_requestedof such an invoice is the price converted into the token.
The quote
quote describes the rate the current amounts in the token were computed at:
| Field | Type | Description |
|---|---|---|
rate |
string | The rate of the amounts: US dollars per whole unit of the token, with 18 fractional digits. For a token not pegged to the US dollar, the markdown is already applied |
market_rate |
string | The market rate before the markdown, with 18 fractional digits |
markdown_percent |
string | The markdown applied to market_rate, as a percentage with two decimal places; "0.00" for a token pegged to the US dollar |
currency |
string | USD |
number |
integer | The number of the quote: 1 at the choice, then one more with each new quote. An invoice confirmed by a payment of the previous amount shows the number of that quote — see quote window |
expires_at |
string | Until when the amounts of this quote hold — see quote window |
quote is null for an invoice created with asset, before the choice, and for an invoice whose token was chosen before exchange rates were introduced: such an invoice was converted one to one and is not quoted again.
quote right after the choice of USDT_TRON at 0.9991 USD per USDT; expires_at is the invoice's deadline, since the token is pegged to the dollar:
{
"rate": "0.999100000000000000",
"market_rate": "0.999100000000000000",
"markdown_percent": "0.00",
"currency": "USD",
"number": 1,
"expires_at": "2026-09-11T21:03:05.531Z"
}
Quote window
The amounts of a quote hold until quote.expires_at.
- A token pegged to the US dollar (
USDT_*,USDC_*):quote.expires_atis the invoice'sexpires_at. The amount fixed at the choice holds for the whole life of the invoice, and so does the rest after a partial payment: such a token trades within hundredths of a percent of the dollar, there is no tolerance for an underpayment, and an amount that moved under the customer would leave one who paid exactly what was shown a few cents short. The rate is still read at the choice; it is not taken as one to one. - Any other token: a window the platform sets, counted from the moment of the quote and never later than the invoice's
expires_at. - After the window of a token not pegged to the dollar the service quotes what is still owed again at the current rate, if the invoice is
createdorpartially_paid, its deadline has not passed, and no transfer to it is waiting for confirmations. The window is judged by block time: the service quotes again only after it has processed blocks later than the end of the window, so every transfer sent inside the window is already counted. - What changes. The rest still owed is worth, in US dollars, what it was worth at the old rate; the new quote asks for that worth at the new rate, rounded up the same way.
amount_expectedbecomesamount_receivedplus the new rest, andamount_remainingfollows it.amount_requestedchanges by the same amount: the part ofamount_expectedthat is the service fee paid by the customer keeps its amount in units of the token.quotegets the nextnumber, new rates and a newexpires_at. - What does not change. The price in
price, the status, the address, the fee rate and the payers. No webhook is sent: read the current amounts withGET /v1/invoices/{id}. The checkout page shows them by itself. - A payment of the previous amount. The customer may have copied the amount while the previous quote held, and the transfer may reach a block only after the new one replaced it. A transfer in a block no later than 10 minutes after the end of the previous quote's window is judged against the previous quote's amount as well: if the payments by then cover it, the invoice becomes
confirmed, and itsamount_requested,amount_expectedandquoteare those of the previous quote again. A later transfer is judged against the quote in force only. What is credited is always what arrived. - No fresh rate. The old amounts hold until a fresh rate appears; the service tries again on its next pass.
- An invoice created with
assetis never quoted again: its amount is set by the merchant in the token.
Retrieve an invoice
GET /v1/invoices/{id} returns the current state of an invoice. Decide whether to fulfill an order by this response — after a webhook, after a redirect from the checkout page, and when polling — not by the webhook body: see webhooks.
Request
| Parameter | Where | Type | Required | Description |
|---|---|---|---|---|
id |
path | string (UUID) | yes | invoice_id from the response to creation |
include |
query | string | no | The only value is qr: the response then carries the QR code of the address. Any other query parameter, and any other value, is refused with 400 validation_failed |
The read is conditional: the answer carries an ETag, and a request that returns it in If-None-Match gets 304 Not Modified with no body while nothing has changed — see conditional reads.
Response
200 with the same object as the response to creation — see create an invoice. Fields of a deposits element:
| Field | Type | Description |
|---|---|---|
tx_hash |
string | Transaction hash |
amount |
string | Transfer amount in units of the asset |
confirmations |
integer | For seen: the number of the latest block the service has seen minus the number of the block with the transfer, updated as new blocks arrive. For confirmed: that number at the moment of confirmation; it is no longer updated |
status |
string | seen — the transfer is in a block, but it has fewer confirmations than the asset requires; confirmed — it has enough |
block_time |
string | Time of the block with the transfer |
late |
boolean | true if block_time is later than expires_at |
explorer_url |
string or null | The page of the transaction on the explorer of the network; null if the network has no explorer link |
deposits lists only payments to this invoice, in the order of their blocks. It does not list dust, transfers of another asset, transfers that disappeared in a chain reorganization, or per_user top-ups.
Errors
| HTTP | error.code |
When |
|---|---|---|
| 400 | validation_failed |
id is not a UUID |
| 401 | unauthorized |
The API key is missing or invalid |
| 403 | merchant_disabled |
The merchant is disabled |
| 404 | invoice_not_found |
There is no such invoice, or it belongs to another merchant |
| 429 | rate_limited |
The request limit is exceeded |
| 500 | internal_error |
An unhandled error of the service |
A merchant that has not been approved can read its invoices: this route does not return merchant_not_approved.
Example
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"
200, the invoice from the creation example after it was paid:
{
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"order_id": "ord_1001",
"status": "confirmed",
"asset": "USDT_TRON",
"network": "tron",
"network_name": "TRON",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"payment_uri": null,
"price": null,
"currencies": null,
"asset_selected_at": null,
"quote": null,
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "49.900000",
"amount_remaining": "0.000000",
"amount_credited": "49.401000",
"fees": {
"service": { "payer": "merchant", "percent": "1.00", "fixed": "0.000000", "amount": "0.499000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"external_user_id": null,
"return_url": null,
"cancel_url": null,
"checkout_url": "https://pay.example.com/pay/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"expires_at": "2026-09-11T21:03:05.531Z",
"created_at": "2026-09-11T20:03:05.531Z",
"deposits": [
{
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"amount": "49.900000",
"confirmations": 20,
"status": "confirmed",
"block_time": "2026-09-11T20:10:42.000Z",
"late": false,
"explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
}
]
}
Where the money goes
A confirmed payment waits on the deposit address until the service moves it. Where it is moved is a property of the project, set by the operator, and it changes nothing about the API: the same requests, the same statuses, the same webhooks.
- By default the money goes to the service's treasury, and the merchant's balance is what the service owes it — read with balance and withdrawn in the Dashboard.
- A project the operator has switched to direct settlement gets the money of each payment forwarded to its own wallet, net of the service fee, without asking for anything. Its balance therefore stays at zero: a zero there means the money is already in its own wallet, not that nothing was paid.
What this changes for an integration:
settledmeans "the money has left the deposit address". For a project settled directly, that is its own wallet; for every other project, the treasury. The order is fulfilled atconfirmedeither way — never wait forsettled.- Small payments accumulate. The service moves money only when the transfer is worth its cost, which is a threshold per asset: below it the payment waits on the deposit address until later payments to the same address add up, or until staff move it by hand. On TRON a transfer costs about a dollar, so the threshold there is tens of dollars; on the EVM networks and TON it is cents. The invoice is
confirmedall the same, andamount_receivedis the truth about what arrived. - A withdrawal request still works for whatever a project earned before the switch. Money already on its way to the project's own wallet is not offered for withdrawal: it would be the same money twice.
QR code
A product that draws its own payment screen asks for the QR code of the address with include=qr, on creation and on a read of one invoice:
curl -s "https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44?include=qr" -H "X-Api-Key: $API_KEY"
The invoice then has one field more, qr, with the code as an SVG image in a data: URL — shortened here, the whole value is about 11 KB:
"qr": { "address": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDUgNDUi…" }
Show it as the src of an <img>: no QR library on your side, and no second request.
<img src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDUgNDUi…" alt="" width="240" height="240">
- The code holds the payment
address, in full and in the form the invoice gives it. It is the same image the checkout page draws, so a wallet reads your screen and ours alike. qr.addressisnullwhile the customer has not chosen the token: the invoice has no address yet. The field itself is absent unlessinclude=qrasked for it.- Only
POST /v1/invoicesandGET /v1/invoices/{id}takeinclude. The change feed does not: a page of fifty codes is half a megabyte nobody asked for. - The code does not hold
payment_uri. Our own page offers that link as a button, not as a code; draw your own code of it if you have checked that your customers' wallets read such links from a camera. - The image is an SVG that runs nothing, black on white, and scales to any size. Give it a size in your markup, and keep
altempty: the address is beside it in text, in full, and a screen reader must read that, not a picture.
Statuses
The watcher changes statuses. It reads blocks one block behind the head of the chain, so a transfer becomes visible within a few seconds. A USDT confirmation on TRON takes 20 blocks, about a minute. On TON it takes one block, a few seconds.
Terms used below:
- On time — a transfer whose
block_timeis not later thanexpires_at. Late — a transfer whoseblock_timeis later. - The deadline has passed — the service has processed a block whose time is later than
expires_at. For an invoice whose token has not been chosen: the service's clock has reachedexpires_at.
| Status | Condition | Final | What the merchant does |
|---|---|---|---|
created |
No on-time transfer to the invoice | no | Wait |
seen |
The confirmed on-time total is below amount_expected, and at least one on-time transfer is still unconfirmed, before or after the deadline |
no | Wait: an unconfirmed transfer can still disappear |
partially_paid |
The deadline has not passed, confirmed on-time transfers total more than zero and less than amount_expected, and no on-time transfer is unconfirmed |
no | Do not fulfill the order. The customer can send amount_remaining to the same address before the deadline — see partial payment |
confirmed |
Confirmed on-time transfers total at least amount_expected. An overpayment is confirmed too |
only settled follows |
Check the status through the API and fulfill the order |
underpaid |
The deadline has passed, every on-time transfer is confirmed, their total is above zero and below amount_expected, and no late transfer is confirmed |
no | Decide yourself: the confirmed transfers are credited |
expired |
The deadline has passed, and no transfer is confirmed | no | Close the order |
paid_late |
The deadline has passed, the confirmed on-time total is below amount_expected, and at least one late transfer is confirmed |
yes | Decide yourself: amount_received shows the total, and the transfers are credited |
settled |
The invoice was confirmed, and the funds of all its confirmed payments have left the deposit address: to the service's treasury, or straight to the merchant's own wallet when the operator has turned that on for the project — see where the money goes |
yes | Nothing: for the merchant this is the same as confirmed |
Transitions:
created→seen: an on-time transfer appears in a block.seen→confirmed: the confirmed on-time transfers reachamount_expected.seen→partially_paid: before the deadline, the last unconfirmed on-time transfer is confirmed or disappears in a reorganization, and the confirmed on-time transfers are belowamount_expected.partially_paid→seen: before the deadline, another on-time transfer appears in a block.seen→created: before the deadline, the only on-time transfer disappears in a chain reorganization.created→expired: the deadline passes without on-time transfers.partially_paid→underpaid,partially_paid→paid_late: the deadline has passed without the rest arriving on time; the new status follows from the table.seen→underpaid,seen→paid_late,seen→expired: the deadline has passed, and the last unconfirmed on-time transfer is confirmed or disappears; the new status follows from the table.underpaid→paid_late,expired→paid_late: a late transfer is confirmed.underpaid→seen,expired→seen: a transfer that disappeared in a reorganization returns to a block before the deadline. This is rare.confirmed→settled: the funds have left the deposit address — see where the money goes.
stateDiagram-v2
[*] --> created
created --> seen: on-time transfer in a block
seen --> created: transfer vanished in a reorganization
seen --> confirmed: confirmed on-time total reached the amount
seen --> partially_paid: part of the amount confirmed before the deadline
partially_paid --> seen: another on-time transfer in a block
partially_paid --> underpaid: deadline, the rest did not arrive
partially_paid --> paid_late: deadline, late transfer confirmed
created --> expired: deadline without transfers
seen --> underpaid: deadline, confirmed less than expected
seen --> expired: deadline, on-time transfer vanished
seen --> paid_late: deadline, late transfer confirmed
underpaid --> paid_late: late transfer confirmed
expired --> paid_late: late transfer confirmed
underpaid --> seen: transfer returned to the chain
expired --> seen: transfer returned to the chain
confirmed --> settled: funds have left the address
Rules:
- Before the deadline, transfers to the invoice address add up: the first transfer is not final.
- A confirmed transfer stays counted. Only an unconfirmed transfer can disappear in a reorganization; the merchant then receives
deposit.orphaned. - A transfer to a
confirmed,settledorpaid_lateinvoice does not change its status:amount_receivedgrows, anddeposit.confirmedreports the transfer. - Because the deadline is decided by processed blocks, an invoice can stay
created,seenorpartially_paidfor a few seconds afterexpires_at, and longer while an on-time transfer waits for confirmations.
When the status changes, the merchant receives the webhook named after the new status: invoice.seen, invoice.partially_paid, invoice.confirmed, invoice.underpaid, invoice.expired or invoice.paid_late. An overpayment also sends invoice.overpaid together with invoice.confirmed. The transitions to created and to settled have no webhook. The event arrives on every transition: an invoice that goes from partially_paid to seen with a new transfer, and back when that transfer disappears, receives invoice.seen and invoice.partially_paid again.
Partial payment
There is no tolerance for an underpayment: an invoice becomes confirmed only when confirmed on-time transfers cover the whole amount_expected. If the customer sent less, they can send the rest to the same address before the deadline.
partially_paidis the moment to ask for the rest. Show the customeramount_remainingand the sameaddress, and remind them ofexpires_at. The checkout page does this by itself — see checkout page.- A rest below the minimum deposit of the asset pays the invoice too. While a payment to the invoice has already arrived and
amount_expectedis not reached yet, a transfer in the invoice's asset to its address is a payment even belowmin_amount, as long as it is above zero. The first transfer to an invoice, and a transfer after the amount is reached, are still dust when they are below the minimum deposit. seenmeans a transfer is waiting for confirmations, andamount_remainingdoes not count it yet. Do not ask for more: the rest may already be on its way. After the confirmation the invoice becomesconfirmed, orpartially_paidwith the exact rest.- After the deadline the rest no longer pays the invoice. A transfer in a block later than
expires_atmakes itpaid_late, and without such a transfer the invoice becomesunderpaid. The merchant decides what happens to the order. - An invoice created without
assetin a token not pegged to the US dollar can have its rest quoted again after the quote window: takeamount_remainingfrom a fresh response, not from an earlier one. - An exchange may keep its withdrawal fee out of the amount the customer sends, and less arrives at the address. Warn the customer: the whole amount must arrive at the address.
amount_remaining is amount_expected minus amount_received, but not below zero, so it is zero for confirmed and settled. For underpaid and paid_late it is the shortfall against amount_expected: late transfers count in amount_received.
per_user mode
In per_user mode a user of the merchant has one permanent address, and all invoices of the user are paid to it — see addresses. When a transfer to that address appears in a block, the service decides once what it pays:
- An invoice, if the user has one that matches: the same asset,
amount_expectedequal to the transfer amount, statuscreatedwith no other transfer, andexpires_atnot earlier than the time of the transfer's block. Of several such invoices, the earliest created one is paid. - A top-up otherwise: the transfer is credited to the merchant's balance with the top-up fee, and
deposit.confirmedarrives withinvoice_idset tonull. A top-up never pays an invoice later.
Consequences:
- a transfer of a different amount is a top-up, not a partial payment;
- a transfer of the exact amount after the deadline is a top-up;
- an invoice whose token has not been chosen takes no part;
- the amount compared is
amount_expected: if the customer pays the service fee, it is the price together with the fee; partially_paidandunderpaiddo not occur in this mode, andpaid_lateoccurs only if a reorganization moves the matched transfer into a block after the deadline.
Fees
STONS withholds a service fee from every confirmed payment: a percentage and a fixed amount set by the operator. The percentage is common to the platform or set for a specific merchant; the fixed amount is set per asset and is the same for all merchants. Either number can be zero. The project setting "Who pays the fees" in the cabinet decides who pays the service fee: the merchant or the customer.
An invoice has no separate network fee. The network costs of accepting a payment — moving the funds from the deposit address to the treasury — are included in the service fee. That is what the fees.network line says: status = included. Payouts have a fee of their own — see the payout fee.
The rate and the payers are recorded in the invoice when it is created, and the fixed amount when the asset becomes known: at creation for an invoice with asset, at the customer's choice for an invoice without it. Recorded terms do not change: a change of the tariff or of the project settings affects only new invoices. Invoices created before the fee was introduced carry a rate of 0, and those created before the fixed amount was introduced carry a fixed amount of 0.
Fields
| Field | Description |
|---|---|
fees.service.payer |
merchant — the fee is withheld from the amount received; customer — the fee is added to the price and included in amount_expected |
fees.service.percent |
The rate recorded in the invoice, as a percentage with two decimal places: "1.00" |
fees.service.fixed |
The fixed amount recorded in the invoice, in units of the asset: "1.700000". Withheld once per invoice, however many transfers pay it. null until the customer has chosen the token |
fees.service.amount |
The fee — the percentage and the fixed amount together — withheld from the confirmed payments of the invoice so far. Zero until the first confirmation; null until the customer has chosen the token |
fees.network.status |
Always included: network costs are part of the service fee and are not charged or withheld separately |
fees.network.payer |
Always merchant: the customer does not pay for the network separately |
fees.network.quote, fees.network.quote_rate, fees.network.actual |
Always null. Kept so as not to break integrations that already read these fields |
amount_credited |
amount_received minus fees.service.amount: how much of the amount received is credited to the merchant |
How the service fee is calculated
In the formulas, "rate" is fees.service.percent divided by 100, "fixed" is fees.service.fixed, and "received" is the total of all confirmed payments of the invoice.
- The customer pays the fee. The rate applied to the price, rounded up to the last decimal place of the asset, and the fixed amount are added to the price:
amount_expected = amount_requested + ⌈amount_requested × rate⌉ + fixed. An invoice for 100 USDT at 2% and 1.70 USDT expects 103.70 USDT. - The fee is withheld when each payment is confirmed, in the same operation that credits the payment, and it is calculated from "received":
- the merchant pays:
min(received, ⌊received × rate⌋ + fixed); - the customer pays:
min(received, fixed + ⌊max(0, received − fixed) × rate / (1 + rate)⌋): first the fixed amount, then the share of the percentage inside the amount paid.
- the merchant pays:
- No more than what was received. A payment smaller than the fixed amount is withheld in full, and no more: the credited amount does not go below zero.
- Rounding down, in the merchant's favor. Each subsequent transfer withholds the difference between the fee on the new total and the fee already withheld, so an invoice paid in several transfers withholds as much as one paid in a single transfer, and the fixed amount only once.
- An exact payment by the customer withholds exactly what was added to the price, or one minimum unit of the asset less if the percentage of the price does not divide evenly: rounding down leaves that unit to the merchant.
- Overpayment: the fee is taken from the larger amount. Underpayment: from the amount received. Payment after the deadline: like any confirmed payment.
- Dust and transfers of another asset carry no fee: they are not payments. A transfer that disappears in a reorganization before confirmation is not credited and carries no fee.
- A
per_usertop-up carries a fee on the transfer amount at the merchant's rate and the asset's fixed amount in effect at the time of the transfer's block, no more than the transfer amount, and the merchant pays it. Each top-up is a separate operation with its own fixed amount. The merchant's balance receives the transfer amount minus this fee;deposit.*webhooks carry no fee fields.
Example: price 100 USDT, rate 2%, fixed amount 1.70 USDT, the customer pays the fee. The customer pays in two transfers: 1 USDT, then 102.70 USDT.
| Moment | amount_requested |
amount_expected |
amount_received |
fees.service.amount |
amount_credited |
|---|---|---|---|---|---|
| Created | 100.000000 |
103.700000 |
0.000000 |
0.000000 |
0.000000 |
| First transfer confirmed | 100.000000 |
103.700000 |
1.000000 |
1.000000 |
0.000000 |
| Second transfer confirmed | 100.000000 |
103.700000 |
103.700000 |
3.700000 |
100.000000 |
Your integration compares its price with amount_requested for an invoice created with asset, and with price.amount for an invoice created without it: there amount_requested is the price converted into the token. The customer is shown amount_expected: only that amount closes the invoice.