Deposit addresses
This document describes deposit addresses: the two address modes, how POST /v1/addresses issues a permanent address to a user of a per_user merchant, and what happens to each kind of transfer that arrives at an address. Read it before choosing the address mode and before showing an address to a customer.
Modes
The owner chooses the mode when creating the project (the merchant) in the cabinet (Dashboard), where per_invoice is offered. per_user is a restricted mode an operator allows per project; once allowed, the owner switches to it in the cabinet (ADR-0066). The change decides where the next payment is collected: addresses already issued keep their own mode, keep being credited and keep being swept.
| Mode | Address | When it fits |
|---|---|---|
per_invoice |
A new address for every invoice, issued when the invoice is created; for an invoice without a token, when the customer chooses the token. The address identifies the invoice | Payment for a specific order |
per_user |
One permanent address for each user of the merchant (external_user_id), issued by the first POST /v1/addresses for that user or when the first invoice of that user gets an address |
Balance top-ups and repeated payments to one address |
- In
per_invoicemode the address comes in the response toPOST /v1/invoices, andPOST /v1/addressesis refused with409 address_mode_mismatch. - In
per_usermode a user's address and the address of every invoice of that user are the same address. Which invoice a transfer pays is decided by the amount — see per_user mode. - A payment is never matched to an order by a "unique amount" with extra cents: the address decides.
A paused mode
The platform can pause an address mode for a while. A paused mode stops only what is new in it, and each such request gets 422 address_mode_disabled:
- a new project in the mode;
- for a project in the mode:
POST /v1/addresses, for a new permanent address and for an address issued before alike;POST /v1/invoices, withassetand without it; the token choice on the checkout page,POST /checkout/v1/invoices/{id}/select. While the mode is paused, the page offers such an invoice no token.
A retry of POST /v1/invoices with the order_id of an invoice created before the pause returns that invoice. Everything that concerns money already on its way goes on as usual: a transfer to an address issued before, a permanent one included, is recorded, credited and reported by webhooks; invoices that already have an address are paid and closed; GET /v1/invoices/{id} and GET /v1/deposits/{tx_hash} answer. Tell the customer that payment is not available right now and retry later: the answer stays the same until the platform turns the mode back on.
Permanent user address
POST /v1/addresses returns the permanent address of a user of a per_user merchant and creates it on the first request.
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.
| Field | Type | Required | Description |
|---|---|---|---|
external_user_id |
string | yes | The user's identifier on the merchant's side: 1–200 visible ASCII characters (0x21–0x7E), no spaces |
network |
string | yes | Network of the address: the identifier of an enabled network of the service's registry in which the service issues addresses. GET /v1/assets lists the networks of the assets on offer — see list the assets. Today that is only tron |
Response
200 for a new address and for an existing one.
| Field | Type | Description |
|---|---|---|
address |
string | The address in full: base58 on TRON; 0x and 40 hexadecimal digits in lower case on EVM networks; on TON, the user-friendly form — 48 base64url characters starting with UQ (0Q on a test network). Compare EVM addresses without regard to case, and show them to people with the EIP-55 checksum. Show a TON address to people exactly as returned — see addresses on TON |
network |
string | The network of the request |
assets |
array of string | Assets the merchant can offer the user for payment to this address at the moment of the request, sorted by identifier. An asset is listed if the service accepts it for payments, the operator has not disabled payments in it, and it is not disabled in the project settings. On mainnet the list for tron can contain only USDT_TRON today: payments in USDC_TRON are disabled; for ton, USDT_TON, and GRAM_TON once the platform has enabled payments in it — see assets |
- A repeated request with the same
external_user_idreturns the same address. Concurrent requests for one user also get one address. assetsshows what to offer, not what is credited: a transfer in an asset that has dropped out of the list is still credited — see what arrives at an address.
Errors
| HTTP | error.code |
When |
|---|---|---|
| 400 | validation_failed |
A field is missing, unknown, of the wrong type or format; network is not a network the service issues addresses in |
| 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: a permanent address is a way to pay, and it is not issued before approval. Checked after the mode |
| 409 | address_mode_mismatch |
The merchant works in per_invoice mode |
| 422 | address_mode_disabled |
The platform has paused the per_user mode — see a paused mode. Checked after approval |
| 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 | 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/addresses \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "user_42", "network": "tron"}'
200:
{"address":"TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH","network":"tron","assets":["USDT_TRON"]}
The address in the example is illustrative: every installation has its own addresses, derived from its own key.
Where addresses come from
- Every address is derived from the service's master key according to BIP44: along the path
m/44'/195'/0'/0/{index}on TRON andm/44'/60'/0'/0/{index}on EVM networks. On TON an address is a wallet contract, Wallet V5R1, of one public key of the pathm/44'/607'/0', and the index is written into the wallet's identifier; the address also depends on the network, mainnet or testnet. On Solana an address is the public key of the pathm/44'/501'/0'/{index}'itself, every step hardened: SLIP-0010 derives ed25519 keys along hardened paths only. - An address belongs to a family of networks, not to one network: one EVM address receives in every EVM network. A
per_useruser therefore has one address for all EVM networks and another one on TRON, and a repeatedPOST /v1/addressesfor another EVM network returns the same address. - Indexes are issued strictly in order within a family and are never reused, even if an address never received a payment. Two users or two invoices never share an address.
- The API server holds only the public part of the master key: it can derive addresses but cannot spend funds from them. On Solana it holds even less: an extended public key does not exist for this kind of key, so the API server only hands out public keys that the signing server derived and published to it in advance, and can compute no address of its own.
- Addresses are issued in the networks the service has enabled — see networks and tokens. Show the customer the network together with the address.
Addresses on TON
A TON account has two written forms: the raw one, 0: and 64 hexadecimal digits, and the user-friendly one, 48 base64url characters with a checksum. The API returns the user-friendly form in address everywhere — POST /v1/addresses, invoices, GET /v1/deposits/{tx_hash} and the deposit.* webhooks — because it is the form wallets accept and check.
- The address is non-bounceable: it starts with
UQ, or0Qon a test network. A deposit address is not deployed until the service moves the money on, and a wallet sends to a non-bounceable address so that the coins stay; a bounceable message to an address that is not deployed returns them to the sender, and the invoice is not paid. - Show the address to the customer and put it into a QR code exactly as returned: do not convert it into another form.
- A TON payment needs no comment (memo): the address alone tells whose payment it is. A comment the customer adds does not change anything.
- A token on TON is kept by a separate jetton wallet of its owner. The customer sends the token to the deposit address as usual; the service finds it on the jetton wallet of that address.
Addresses on Solana
A Solana address is a public key written in base58: 32 to 44 characters, and case-sensitive. Base58 treats the two cases of the Latin alphabet as distinct symbols, so an address differing only in case is a different address — one nobody holds the key to.
- Never lower-case or upper-case an address of this network, and do not normalise it in any other way: not in your database, not in a URL, not before a comparison, not in a QR code. Store it and compare it byte for byte as the API returned it, and show it in full.
- The signature of a Solana transaction is base58 too: 86 to 88 characters, not 64 hexadecimal digits. The service puts it in
tx_hash— inGET /v1/deposits/{tx_hash}and in thedeposit.*webhooks — and a column sized for a hexadecimal hash will not hold it. Compare signatures case-sensitively as well. - A token on Solana is not held at the owner's address. It lives in an associated token account, a separate account for the pair (owner, mint), whose address is derived deterministically. The service creates that account when it issues the address, so the customer's wallet sends the token to the deposit address as usual and needs to know nothing about it.
- One consequence worth knowing if you check a payment yourself:
getSignaturesForAddresson a deposit address will not show an SPL token transfer at all. Signatures are indexed by the accounts a transaction touched, and a token transfer touches the associated token account, not the owner's address. The address itself shows only transfers of the native coin SOL, which really does change its own balance. This is a property of the method, not of a provider. UseGET /v1/deposits/{tx_hash}instead of reading the chain yourself — see verify a transfer. - A Solana address needs no activation and holds no rent of its own: the fee of a transfer out is paid by a separate account of the service, so there is no coin to send to a deposit address beforehand.
What arrives at an address
The service watches every address it has issued, at all times, including after an invoice's deadline. A transfer to an address is handled by these rules, in this order:
- A token the service does not know (a contract that is not in its asset catalog) is ignored and not recorded. Some such tokens are trap contracts: do not interact with them.
- An amount below the minimum deposit of its asset is dust: recorded, not credited to the merchant, no webhook. The exception is the rest of an invoice at a
per_invoiceaddress: a transfer above zero in the invoice's asset is a payment when a payment to the invoice has already arrived andamount_expectedis not reached yet (see partial payment). - A
per_invoiceaddress:- a transfer in the invoice's asset is a payment of that invoice, before or after the deadline — see statuses;
- a transfer in another asset from the catalog — for example
USDC_TRONto the address of aUSDT_TRONinvoice, orTRX_TRON— is recorded asunexpected_asset: not credited, the invoice does not change, no webhook.
- A
per_useraddress:- a transfer in an asset the service accepts for payments (
USDT_TRON,USDC_TRON, andTRX_TRONonce the platform has enabled payments in it) pays a matching invoice or tops up the merchant's balance — see per_user mode. It is credited even if the operator or the project settings have disabled the asset, or the platform has paused the mode: the switches stop new invoices, addresses and projects and remove the asset fromassets, they do not stop crediting; - a transfer in an asset the service does not accept for payments (
TRX_TRONuntil the platform enables payments in it) is recorded asunexpected_asset: not credited, no webhook.
- a transfer in an asset the service accepts for payments (
- The native coin of an EVM network follows these rules only while the platform accepts payments in it, whether a wallet sent it or a smart contract did — a smart-contract wallet, a multisig, an exchange's withdrawal. A transfer from a contract is found by tracing its block and recorded only when the trace accounts for the whole rise of the address's balance. One that cannot be accounted for is neither credited nor listed by
GET /v1/deposits/{tx_hash}, and no webhook is sent: the operator resolves it by hand. While the platform does not accept a native coin of an EVM network, its transfers are not recorded at all. - GRAM on TON follows these rules only when it arrives as a plain transfer from a wallet — a message without a body or with a text comment — and only while the platform accepts payments in it. GRAM that comes with a message of another kind, such as the GRAM a token transfer carries to pay for its notification, is not credited, and no webhook is sent.
The project owner sees unexpected_asset transfers in the cabinet. The service does not credit them; to resolve one, contact the operator.
Verify a transfer
GET /v1/deposits/{tx_hash} returns every transfer of one transaction to the merchant's addresses, as the service recorded it. Use it to act on a deposit.* webhook, a per_user top-up without an invoice included, instead of trusting the webhook body — see the main rule. The route works in either address mode, also while the platform has paused one.
Request
tx_hash in the path is the transaction hash as its network writes it, in either case: on TRON and TON, 64 hexadecimal characters without 0x; on EVM networks, 0x and 64 hexadecimal characters. Take it from data.tx_hash of the webhook: the service looks for exactly this form and neither adds nor removes 0x. The request has no body.
On TON a transfer is a chain of transactions, one per contract. tx_hash is the hash of the transaction in which the deposit address — for a token, the jetton wallet of that address — received the money, not the hash of the transaction the sender's wallet shows. TON explorers write it in hexadecimal; an API that gives it in base64 needs it converted.
Response
200 with the array deposits: the transfers of the transaction to addresses of this merchant, in the order of log_index. One transaction can carry several transfers: find the one from the webhook by log_index. Transfers of the same transaction to addresses of other merchants are not listed.
| Field | Type | Description |
|---|---|---|
tx_hash |
string | Transaction hash, in lower case |
log_index |
integer | Number of the transfer within the transaction: together with tx_hash it identifies the transfer. A transfer of a native coin has no event and gets a negative number: -1 is the value of the transaction itself, and on TRON and EVM networks -2, -3 and so on are transfers made by contracts within it. On TON a token transfer is 0 and a GRAM transfer is -1 |
asset |
string | Asset |
network |
string | Network of the asset, for example tron |
amount |
string | Transfer amount with all the decimals of the asset |
address |
string | Deposit address of the merchant, in full, in the same form as in POST /v1/addresses |
invoice_id |
string (UUID) or null | The invoice the transfer pays; null for a per_user top-up and for a transfer that is not a payment |
external_user_id |
string or null | The user of a per_user address; null for a per_invoice address |
kind |
string | What the service decided the transfer is: payment — a payment of an invoice or a per_user top-up; unexpected_asset — an asset the address does not expect; dust — below the minimum deposit and not the rest of an invoice payment already started. See what arrives at an address |
status |
string | seen — in a block, without enough confirmations yet; confirmed — the confirmations its asset requires are reached; orphaned — the transfer disappeared from the chain in a reorganization, or its transaction was unsuccessful |
confirmations |
integer | How many blocks have been added on top of the block with the transfer, as the service last saw it |
block_number |
string | Number of the block with the transfer; on TON, the number (seqno) of the masterchain block that includes the transaction |
block_time |
string | Time of that block, ISO 8601 in UTC |
credited |
boolean | true if the ledger holds the transfer amount on the merchant's balance. The service fee is withheld from the balance by a separate posting |
explorer_url |
string or null | The page of the transaction on the explorer of the network; null if the network has no explorer link |
tx_hash,log_index,asset,amount,address,invoice_id,external_user_idandblock_timehave the same names and values as in the data of thedeposit.*webhooks.confirmationsin the response is the current depth, not the depth at the moment of the event.- Credit a top-up to the user only when its transfer has
kind=payment,status=confirmedandcredited=true. Fulfill an order by the status of its invoice fromGET /v1/invoices/{id}, not by a single transfer. - A transfer in a block the service has not scanned yet is not listed: the response is
404, and a later request finds it. - A merchant that has not been approved can read its transfers: this route does not return
merchant_not_approved.
Errors
| HTTP | error.code |
When |
|---|---|---|
| 400 | validation_failed |
tx_hash is not a transaction hash: neither 64 hexadecimal characters nor 0x and 64 characters |
| 401 | unauthorized |
The API key is missing or invalid — see authentication |
| 403 | merchant_disabled |
The merchant is disabled |
| 404 | deposit_not_found |
The transaction has no transfer to an address of this merchant: the service does not know it, it went only to addresses of other merchants, or its block has not been scanned yet. The response is the same in all three cases |
| 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/deposits/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c \
-H "X-Api-Key: $API_KEY"
200:
{
"deposits": [
{
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"log_index": 0,
"asset": "USDT_TRON",
"network": "tron",
"amount": "25.500000",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"invoice_id": null,
"external_user_id": "user_42",
"kind": "payment",
"status": "confirmed",
"confirmations": 20,
"block_number": "75231904",
"block_time": "2026-09-11T20:04:38.000Z",
"credited": true,
"explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
}
]
}
The hash, the address and the block in the example are illustrative.
Displaying the address
Show the address in full, never shortened to "the first and last characters with an ellipsis". Attackers send transfers from addresses whose first and last characters match, counting on a person checking only those (address poisoning).