Skip to content
STONS

Checkout page

The checkout page is the service's own page where a customer pays an invoice: it shows the amount, the address and the payment status, and when the invoice reaches an outcome, it returns the customer to the merchant's site with a signed redirect. Read this document if you send customers to checkout_url: it covers what the page does, the return URLs, the redirect signature, and the routes the page server uses.

The main rule

Do not fulfill an order on a redirect. When a customer arrives at return_url, verify the signature, then request the invoice with GET /v1/invoices/{id} and fulfill the order only based on the API response — the same as after a webhook.

The signature proves one thing: the service built this URL for this invoice in this status. It does not stop the customer from opening the same link again while its ts is still accepted, and it does not replace checking the status.

How it works

  1. The product creates an invoice with POST /v1/invoices. The checkout_url field of the response is the link to the checkout page of this invoice. If the installation has no checkout page, the field is null — see invoices.
  2. The product sends the customer to checkout_url.
  3. For an invoice created without asset, the page first lets the customer choose the network, then the coin — see choosing the token. Then it shows the amount, the address and the status, and updates them while the customer pays.
  4. When the invoice reaches an outcome, the page sends the customer to the merchant's return URL or cancel URL with parameters and a signature. While the invoice is unpaid, the "Return to the shop" button leads to the cancel URL with the same parameters and signature.
  5. The product verifies the signature and takes the status from the API.

Webhooks arrive the same way as without the page.

The page the customer sees

The page opens at checkout_url without signing in and without keys. Another site cannot embed it in a frame, and the page sends no Referer to the merchant's site.

In the created status the page shows:

  • the name of the merchant;
  • the status "Waiting for payment";
  • the time left until the payment deadline;
  • the amount to pay — amount_expected — with the ticker and the network, and a copy button;
  • the network: its mark by network_icon_slug, its name network_name and the standard of its tokens token_standard, for example "BNB Smart Chain · BEP-20 token". The native coin of a network — BNB, ETH, TRX, GRAM, SOL — has no token standard: it is not a token, and the page shows no "… token" label for it;
  • under "Details", collapsed: the rate of the quote; if the customer pays the service fee, the breakdown "Price · Service fee · Total": amount_requested, the fee with its rate, and amount_expected; the fee shown is amount_expected minus amount_requested;
  • also there, the amount already received, if amount_received is above zero;
  • a warning to send exactly this asset and on this network, in one line together with the network fee being on top of the amount;
  • a QR code;
  • the full address, with a copy button;
  • on TON, the "Open in wallet" button: the wallet_url link, which opens a wallet application with the address, the amount and the token already filled in;
  • a "Return to the shop" button, if the invoice or the project has a cancel URL.

The amount, the network and the address stand one under another in one card. The address takes a row of its own across the card, with an icon copy button beside it; in the 512 px column an address of up to 48 characters fits one line, and on a narrow screen it wraps. The QR code is under the address. On a screen narrower than 640 px it is behind a "Show QR code" button, since a phone cannot scan its own screen; on a wider screen the code is shown at once. The time left is at the right of the row with the merchant's name.

The page writes amounts without trailing zeros: 49.900000 is shown and copied as 49.9.

When transfers to the invoice have arrived, the page lists them under "Received transfers": the amount, the confirmations out of those required, the full transaction hash with a copy button, and a link to the transaction on the explorer of the network. The page shows a link only if it is an https address; without one the hash stays.

The QR code contains only the address. A wallet that scans it gets neither the amount nor the token. That is why the amount is shown and copied separately, and the page asks the customer to check the whole address after pasting it. The address is never shortened anywhere. On TON the "Open in wallet" button carries the amount and the token as well, in the ton://transfer link of the TON documentation; the customer still checks them in the wallet before confirming.

The page is available in English and Russian. To open it in a given language, add lang to checkout_url: ?lang=en or ?lang=ru; the page remembers this choice in a cookie. Without the parameter the page takes, in this order, the remembered choice, the browser language, and English. The customer can switch the language at the bottom of the page.

The page updates the status by itself; there is no need to reload it:

  • the first request comes after 3 seconds, the second after 5, then every 10;
  • after a status change, the sequence starts again from 3 seconds;
  • while the tab is hidden, a request comes every 30 seconds;
  • when the customer returns to the tab, the next request comes after 3 seconds;
  • a failed request does not change the screen, and the page keeps asking.

The time left is counted by the clock of the page's server, not the customer's device. Without JavaScript the page does not update, count down, copy or redirect.

What the page shows in each status:

Status What the customer sees
created The list above
seen "Payment detected", the number of network confirmations out of those required, the estimate of the remaining wait from confirmation_estimate — "About N minutes left", "Less than a minute left" or "Almost done" — and the time left. The page writes no time of its own for a network: how long it takes depends on the confirmations of the asset and the block time of the network, which the administrator changes. On a network that also waits for a final block, the page adds that it may take a little longer. The page does not say that nothing more needs to be sent: if the customer did not send the whole amount, the rest can be sent to the same address before the time runs out, and after the confirmation the page shows how much is left. The address, the QR code and the "Return to the shop" button stay; the invoice amount is shown for reference, without a copy button and without the breakdown, and there is no "Open in wallet" button: the transfer that arrived may already cover the amount
partially_paid "Part of the amount arrived" and the time left. Instead of the amount to pay, "Left to pay": amount_remaining with a copy button; the invoice amount and the amount already received are under "Details". The address, the QR code and the "Return to the shop" button stay; on TON, "Open in wallet" fills in amount_remaining. A short hint under the warning: when withdrawing from an exchange, withdraw extra to cover its withdrawal fee, so that the whole rest arrives at the address
confirmed, settled "Payment received"
paid_late "The payment arrived late": the shop decides what happens to the order
underpaid "Only part of the amount arrived". If there is no cancel URL, also a request not to send the rest to this address and to contact the shop
expired "The payment time has expired" and a request not to send the money again if it was already sent, but to contact the shop

In a status with an outcome — confirmed, settled, paid_late, underpaid or expired — the page hides the amount, the address and the QR code and stops updating. The list of transfers stays, and a link to the payment address on the explorer of the network appears instead of the address. Then:

  • With a URL of the right kind (see return URLs), the page counts down 5 seconds and takes the customer to the merchant's site. A button goes right away: "Go to the shop" to the return URL, "Return to the shop" to the cancel URL. Right before going, the page requests a freshly signed URL instead of using an earlier one; if that request fails, the page stops and asks the customer to press the button again. The same happens when the customer opens the page of an invoice that already has an outcome.
  • Without a URL of the right kind, the page shows the outcome and leaves the customer where they are.

While the invoice has no outcome, the page does not send the customer anywhere by itself. The "Return to the shop" button requests a freshly signed cancel URL when pressed. If the invoice has reached an outcome in the meantime, the page shows the outcome instead of going, and then redirects as for any outcome.

expired and underpaid can still change to paid_late if the money arrives later. A customer who opens the page again sees the new status; the merchant receives the invoice.paid_late webhook.

A nonexistent invoice and an invoice of a disabled merchant show the same "Invoice not found" page. The page limits the number of requests from one IP address per minute and above the limit shows "Too many requests".

Choosing the token

An invoice created without asset (invoices) opens with a selection screen while its status is created. At the top are the name of the merchant and the time left, and under them, in one row, the order amount in USD. Then:

  1. Network — a drop-down list of networks with at least one asset offered by the API, with the catalogue mark and name. No network is selected in advance.
  2. Coin — a second drop-down list, available once a network is selected: only assets offered on that network, each with its catalogue icon, ticker and the amount from the API at the current exchange rate. Even a single coin requires an explicit choice. Changing networks clears the chosen coin; a matching ticker does not carry the choice to another network.
  3. Summary — one "To pay" row: the amount in the chosen coin and the network name; under it, in small print, the rate and, if the customer pays the service fee, its rate. Below, one line: send only this coin on this network. The amounts of the breakdown "Price · Service fee · Total" are shown after the choice, under "Details".

The screen does not add native coins or other assets absent from choices: a payment method is supported by the core, not by the presence of an icon.

The "Proceed to payment" button saves the choice, and the page shows the payment screen described above. The button is visible on a screen of any height: when the end of the form is below the edge of the screen, the button sticks to the bottom edge. Under the button a line says that the coin and the network cannot be changed after this step.

  • Amounts shown and amounts recorded. The page shows each coin's amount at the rate the API returned with it, and sends that rate's rate_id with the choice. The service records exactly the amount shown while that rate is still fresh; otherwise it converts the price at the latest fresh rate. After the choice the page shows the recorded amount. In a token pegged to the US dollar the recorded amount holds until the invoice's deadline; in any other token what is still owed can be converted again after the quote window — see quote window.
  • The choice is final. Pressing again with the same token changes nothing. If a different token was already chosen, for example in another tab, the page shows the payment screen in that token with a note that it cannot be changed.
  • Another tab. The selection screen checks the invoice every 10 seconds and reloads if the token has been chosen or the status is no longer created.
  • The deadline. When the time left reaches zero, the screen says that the time is over and disables the button. When the service marks the invoice expired, the page reloads, shows "The payment time has expired" and redirects to the cancel URL if there is one.
  • Nothing to choose. If no asset can be offered — the shop accepts none of them now, the merchant is not approved, the platform has paused the shop's address mode, or the deadline has passed — the page says that payment is not possible right now and asks the customer not to send money. An allowed coin whose exchange rate is not fresh right now is not offered until it is fresh again; the API reports this case with rates_unavailable.
  • The token became unavailable by the moment of the press: the page asks the customer to choose another one. If saving fails for another reason, the page asks to try again in a minute.
  • Without JavaScript the interactive selector is hidden; a native form groups coin options and their amounts by network.

Return URLs

Which URL the customer is sent to depends on the invoice status:

Status Where Why
created, seen, partially_paid cancel URL — only with the "Return to the shop" button The invoice can still be paid: the page does not take the customer away by itself
confirmed, settled, paid_late return URL The invoice is paid; with paid_late the payment arrived after the deadline, and the merchant decides
expired, underpaid cancel URL The deadline passed without the full amount

The project's URLs are set in the project settings in the cabinet (Dashboard) — "After successful payment" is the return URL, "Return to the shop / on failure" is the cancel URL — or by the operator. An empty field means no URL. Without a URL of the right kind there is no redirect, and without a cancel URL there is no "Return to the shop" button.

For a single invoice, a URL can be overridden with the return_url and cancel_url fields in POST /v1/invoices or in the invoice form in the cabinet. An override is allowed if its origin — the scheme, host and port — matches the origin of one of these URLs of the project:

  • the URL of the same kind from the project settings;
  • the project site from the project application.

For example, with the project site https://shop.example, https://shop.example/orders/1001/paid is allowed, but https://pay.shop.example/… and https://shop.example:8443/… are not. On the shared hosts t.me and telegram.me, different bots and channels live under one origin, so there the first path segment — the bot or channel name — must match too, case-insensitively. With the project site https://t.me/example_shop_bot, https://t.me/example_shop_bot?start=paid_1001 is allowed, but https://t.me/other_bot is not. A project URL on a shared host without a name in the path, such as https://t.me, allows nothing.

If nothing matches, or the project has neither a URL of the same kind nor a site, the response is 422 redirect_url_not_allowed, and the message names the allowed origins. The project's URLs are changed in the cabinet or by the operator, never with the API key: a leaked key cannot send customers to a site or a bot that is not part of the project.

Requirements for any return or cancel URL:

  • it starts with https://, in lowercase;
  • no @ character and no spaces, so that a user name and password cannot get into it;
  • no longer than 2000 characters;
  • no invoice_id, order_id, status, ts or signature query parameters: the service adds them.

Other query parameters and the fragment are kept as they are. The URL used is the invoice override if there is one, otherwise the project's URL at the moment of the redirect: a change of the project's URL applies at once to invoices without an override.

Return before an outcome

The customer can arrive at the cancel URL while the invoice is still open: with status=created, status=seen or status=partially_paid, after pressing "Return to the shop". Such an invoice can still be paid; with seen a transfer has already been detected on the network, and with partially_paid part of the amount has already arrived. Do not cancel the order on such a redirect: take the status from GET /v1/invoices/{id} and from webhooks, as after any redirect.

If you are moving from Heleket

Heleket STONS How it works here
url_success return_url Set in the project settings or in the invoice. After payment the page takes the customer there by itself after 5 seconds, or right away with a button
url_return cancel_url Set in the same places. While the invoice is unpaid, it is the "Return to the shop" button; after the deadline passes without the full amount, the page takes the customer there by itself
lifetime expires_in From 300 to 259200 seconds (72 hours), 3600 by default — see invoices
order_id order_id Unique within the project: 1–200 visible ASCII characters. A retry with the same data returns the invoice already created; with different data, 409 order_id_conflict
url_callback the project's webhook URL One per project, set in the project settings, not in the invoice — see webhooks

URLs in an invoice are allowed only within the project's sites — see return URLs. The service adds the parameters and the signature described below to every redirect.

Redirect parameters

Parameter Value
invoice_id Invoice identifier
order_id The merchant's order identifier
status Invoice status at the moment the service built the URL; after the "Return to the shop" button, created, seen or partially_paid
ts The moment the service built the URL, whole Unix seconds
signature Signature, 64 lowercase hex characters

The values are encoded by query string rules: read them with URLSearchParams or an equivalent, not by parsing the string by hand.

Signature

The signature is an HMAC-SHA256 in hex, with the merchant's webhook secret as the key. The signed string consists of five lines joined with the newline character \n (byte 0x0A), with no newline at the end:

redirect
<ts>
<invoice_id>
<status>
<order_id>

The separator is a newline rather than a dot, because order_id can contain dots, while no field can contain a newline. The first line, redirect, keeps a redirect signature from passing as a webhook signature made with the same secret.

The recipient must:

  • reject the redirect if ts differs from the current time by more than 300 seconds (5 minutes);
  • compare signatures in constant time;
  • check that invoice_id is the invoice of the order order_id, and take the status from the API, not from the parameter.

Verification in Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

// query is the URLSearchParams of the request that arrived at return_url or cancel_url.
export function verifyRedirect({ query, secret, nowMs = Date.now() }) {
  const fields = ["ts", "invoice_id", "status", "order_id"].map((name) => query.get(name));
  const signature = query.get("signature");
  if (fields.some((value) => value === null || value.includes("\n")) || signature === null) return false;
  const [ts, invoiceId, status, orderId] = fields;
  if (!/^\d{1,12}$/.test(ts) || !/^[0-9a-f]{64}$/.test(signature)) return false;
  if (Math.abs(nowMs / 1000 - Number(ts)) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", secret)
    .update(["redirect", ts, invoiceId, status, orderId].join("\n"))
    .digest();
  return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}

The function returns true only for an unchanged URL signed with this secret within the last 300 seconds. The webhook secret is shown once, when the project is created in the cabinet — see webhooks.

An example to test your implementation against. With the secret example-webhook-secret and the parameters ts=1789300000, invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44, status=confirmed, order_id=ord_1001, the signature is 2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651.

Checkout page routes

The two routes below are not for merchants. The checkout page server calls them over the private network: they are not reachable from the internet and are not in the OpenAPI description.

  • No API key. The page is opened by a customer who has no key; the invoice is opened by its identifier, a random UUID that reaches the customer inside checkout_url.
  • A disabled merchant. Its invoices do not exist for these routes (404 invoice_not_found): the page must not keep collecting money for a merchant that the operator or the owner has turned off.
  • Caching. A successful response carries Cache-Control: no-store: it contains signed URLs.
  • Rate limit. Requests from one IP address are limited in a 60-second window, with the same value as the IP limit of /v1 but a separate counter. All requests of the page come from the address of its server, so this limit protects the API and does not tell customers apart; the page limits customers itself.

Read an invoice for the page

GET /checkout/v1/invoices/{id} returns the invoice as the checkout page shows it.

Request

Parameter Where Type Required Description
id path string (UUID) yes invoice_id

Response

200. Every field is always present; a field that does not apply is null.

Field Type Description
invoice_id string (UUID) Invoice identifier
merchant_name string The merchant name the customer sees
status string Invoice status — see invoices
asset string or null Asset, for example USDT_TRON; null until the customer has chosen the token
asset_icon_slug string or null Asset mark slug from assets.icon_slug, for example usdt0; never inferred from the symbol. null before selection or when no mark is configured
symbol string or null Ticker of the asset for display: USDT; 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 the customer, from the network registry: TRON; null until the customer has chosen the token
network_icon_slug string or null Slug of the network's mark, from the network registry: tron. null until the customer has chosen the token, or if the network has no mark; the page then draws a neutral mark with the first letter of the name
token_standard string or null How wallets name the tokens of the network: TRC-20; null until the customer has chosen the token, and for the native coin of the network (an asset without a contract), which is not a token. The page shows the network as "TRON (TRC-20)", and with null by its name alone
address string or null Payment address, in full, in the form shown to the customer: on EVM networks with the EIP-55 checksum; on TON, user-friendly and non-bounceable, starting with UQ (0Q on a test network) — see addresses on TON; on Solana, base58 of 32 to 44 characters whose case must be kept — see addresses on Solana. null until the customer has chosen the token
wallet_url string or null A link that opens a wallet application with the transfer filled in. Only on TON: ton://transfer/<address>?amount=<amount>, and for a token also &jetton=<jetton master>, as the TON documentation defines the link (https://docs.ton.org/onboarding/wallet-apps/deep-links). amount is what is left to pay, in minimal units of the asset: nanograms of GRAM, millionths of USDT; the jetton master is in the user-friendly bounceable form. The service returns no wallet link on Solana: the field stays null there, the customer copies the address and the amount from the page, and the "Open in wallet" button is not shown. null on other networks, until the customer has chosen the token, while a transfer waits for confirmations (seen), in a status with an outcome, after the deadline, and while the quote is being renewed — whenever the exact amount to send is not known
address_explorer_url string or null The page of the address on the explorer of the network; null until the customer has chosen the token, or if the network has no explorer link
price object or null Price of an invoice created without asset: amount in US dollars with 6 fractional digits, currency is USD. null for an invoice created with asset
choices array or null For an invoice without a token: the assets that can be chosen now, in catalog order — see the table below. An empty array means nothing can be chosen now: the status is not created, the deadline has passed, the merchant is not approved, the platform has paused the merchant's address mode, or no allowed asset passes the checks. null for an invoice with a token
rates_unavailable boolean true when an allowed asset is left out of choices only because its exchange rate is not fresh right now; such an asset returns once the rate is fresh. Always false for an invoice with a token
quote object or null For an invoice whose token was chosen: rate — the exchange rate of the amounts, US dollars per whole unit of the token with 18 fractional digits; currency — USD; expires_at — until when the amounts hold, see quote window. null for an invoice created with asset, before the choice, and for an invoice whose token was chosen before exchange rates were introduced
amount_requested string or null The merchant's price in units of the asset; null until the customer has chosen the token
amount_expected string or null The amount to pay, with all the decimals of the asset: the price plus the service fee if the customer pays it; null until the customer has chosen the token
amount_received string or null Total of confirmed payments; null until the customer has chosen the token
amount_remaining string or null amount_expected minus amount_received, but not below zero, as in the invoice. The page asks the customer to send it in the partially_paid status; null until the customer has chosen the token
fees object The fees of the invoice, as in the invoice. The page shows the breakdown when fees.service.payer is customer, and does not read fees.network
expires_at string Payment deadline
confirmations_required integer or null How many confirmations a payment in this asset needs: 20 on the TRON mainnet, 1 on TON, 1 on Solana; null until the customer has chosen the token
confirmation_estimate object or null While a transfer waits for confirmations (its status is seen): how much longer the slowest of them waits. remaining_blocks — how many blocks are missing to confirmations_required; remaining_seconds — those blocks times the block time of the network from the network registry, rounded up to a whole second; waits_for_finality — true if the network counts a transfer only once its block is also final (the finalized tag), and then the wait may be longer than the estimate. Once the depth is reached both numbers are 0. It is an estimate: blocks do not come strictly on schedule, and the scanner reads the network in passes. null when no transfer waits for confirmations
deposits array Payments to the invoice: tx_hash, amount, confirmations, status (seen or confirmed) and explorer_url, as in the invoice
redirect object or null In a status with an outcome, if the invoice or the project has a URL of that kind: kind — return or cancel, url — the signed URL. Otherwise null
back_url string or null In created, seen and partially_paid, if the invoice or the project has a cancel URL: the signed cancel URL for the "Return to the shop" button. Otherwise null

Fields of a choices element:

Field Type Description
asset string Asset identifier: the value for saving the choice
asset_icon_slug string or null Asset mark slug from the catalog, independently of symbol; null when no mark is configured
network string Network of the asset
network_name string Name of the network for the customer, from the network registry
network_icon_slug string or null Slug of the network's mark; null if the network has none
token_standard string or null How wallets name the tokens of the network; null for the native coin of the network
symbol string Ticker of the asset
decimals integer Decimals of the asset
amount_requested string The price converted into this asset at rate, rounded up
amount_expected string The amount to pay in this asset at rate, including the service fee if the customer pays it. It equals the amount_expected of the invoice after a choice made at this rate, unless the fixed part of the fee changes in between
min_amount string The minimum deposit of the asset
rate string The exchange rate of the amounts: US dollars per whole unit of the asset, with 18 fractional digits. For an asset not pegged to the US dollar, the platform's markdown is already applied
rate_id string The identifier of that rate: send it with the choice, so that the amounts recorded are the amounts shown
rate_expires_at string Until when a choice with this rate_id is recorded at this rate. A choice sent later is converted at the latest fresh rate, so the page reads the invoice again by then

Asset and network marks are independent. The page never guesses a token mark from symbol: an unknown slug displays a neutral mark. A missing field from an older API, or a malformed asset_icon_slug, is read as null without blocking payment.

The signed URLs in redirect and back_url are built at the moment of the request, with ts set to that moment. The response does not include the merchant's identifier, its secrets, external_user_id, order_id as a separate field, or amount_credited: the order number reaches the customer only inside a signed URL.

Errors

HTTP error.code When
400 validation_failed id is not a UUID
404 invoice_not_found The invoice does not exist, or its merchant is disabled
429 rate_limited The request limit for the IP address is exceeded
500 internal_error An unhandled error of the service

Example

GET /checkout/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44

200, a paid invoice whose project has a return URL:

{
  "invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
  "merchant_name": "Shop",
  "status": "confirmed",
  "asset": "USDT_TRON",
  "asset_icon_slug": "usdt",
  "symbol": "USDT",
  "network": "tron",
  "network_name": "TRON",
  "network_icon_slug": "tron",
  "token_standard": "TRC-20",
  "address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "wallet_url": null,
  "price": null,
  "choices": null,
  "rates_unavailable": false,
  "quote": null,
  "amount_requested": "49.900000",
  "amount_expected": "49.900000",
  "amount_received": "49.900000",
  "amount_remaining": "0.000000",
  "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" }
  },
  "expires_at": "2026-09-11T21:03:05.531Z",
  "confirmations_required": 20,
  "confirmation_estimate": null,
  "deposits": [
    {
      "tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
      "amount": "49.900000",
      "confirmations": 20,
      "status": "confirmed",
      "explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
    }
  ],
  "redirect": {
    "kind": "return",
    "url": "https://shop.example/thanks?invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44&order_id=ord_1001&status=confirmed&ts=1789300000&signature=2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651"
  },
  "back_url": null
}

200, an invoice without a token, whose customer pays a 1% service fee and whose project has no cancel URL, at a USDT rate of 0.9991 USD:

{
  "invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
  "merchant_name": "Shop",
  "status": "created",
  "asset": null,
  "asset_icon_slug": null,
  "symbol": null,
  "network": null,
  "network_name": null,
  "network_icon_slug": null,
  "token_standard": null,
  "address": null,
  "address_explorer_url": null,
  "wallet_url": null,
  "price": { "amount": "100.000000", "currency": "USD" },
  "choices": [
    {
      "asset": "USDT_TRON",
      "asset_icon_slug": "usdt",
      "network": "tron",
      "network_name": "TRON",
      "network_icon_slug": "tron",
      "token_standard": "TRC-20",
      "symbol": "USDT",
      "decimals": 6,
      "amount_requested": "100.090082",
      "amount_expected": "101.090983",
      "min_amount": "1.000000",
      "rate": "0.999100000000000000",
      "rate_id": "48213",
      "rate_expires_at": "2026-09-16T12:05:00.000Z"
    }
  ],
  "rates_unavailable": false,
  "quote": null,
  "amount_requested": null,
  "amount_expected": null,
  "amount_received": null,
  "amount_remaining": null,
  "fees": {
    "service": { "payer": "customer", "percent": "1.00", "fixed": null, "amount": null },
    "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
  },
  "expires_at": "2026-09-11T21:03:05.531Z",
  "confirmations_required": null,
  "confirmation_estimate": null,
  "deposits": [],
  "redirect": null,
  "back_url": null
}

The API returns a flat list of assets, each with its own network; grouping them by coin is up to the page.

Save the token choice

POST /checkout/v1/invoices/{id}/select records the token the customer chose for an invoice created without asset. The page server calls it when the customer presses "Proceed to payment".

Request

id in the path is the invoice_id. The body is a JSON object with Content-Type: application/json; a field that is not in the table is refused with 400 validation_failed.

Field Type Required Description
asset string yes One of choices[].asset, for example USDT_TRON
rate_id string no The rate_id of the same element of choices: the rate whose amounts the customer saw

Response

200 with the invoice in the format of reading an invoice for the page, already with the token, the address, the amounts and quote, and with choices set to null.

  • The choice is final. A repeated request with the same asset returns the invoice unchanged, even after the deadline, so a request whose response was lost is safe to retry. A different asset gets 409 asset_already_selected. The same holds for an invoice created with asset.
  • Concurrent requests are processed one at a time: the first one records its asset, and the others get 200 or 409 asset_already_selected by the rule above.
  • When a choice is possible: the status is created, and the service's clock has not reached expires_at.
  • Checks, in this order, with the same codes as creating an invoice: the merchant is approved; the platform has not paused its address mode; the asset is in the invoice's currencies, if the invoice has a list; it is accepted for payments; it is enabled by the operator and in the project settings; it has an exchange rate, and a fresh one; the price converted at that rate is not below its minimum deposit.
  • Rate. With rate_id, the price is converted at that rate if it is an accepted rate of the coin of this asset and is still fresh. Otherwise, and without rate_id, it is converted at the latest fresh rate of the coin. A rate_id of another coin or an old one is not an error: it is simply not used. The choice records the first quote, which the response shows in quote — see price and exchange rate.
  • Terms. The fee rate and the payers are those recorded when the invoice was created; the fixed part of the fee is that of the asset at the moment of the choice. amount_expected is computed from the converted price the same way as for an invoice created in this asset.
  • Address. A per_invoice merchant gets a newly derived address; a per_user merchant gets the permanent address of the user from external_user_id.

Errors

HTTP error.code When
400 validation_failed id is not a UUID; the body has no asset, an unknown field, asset is not an asset identifier, or rate_id is not a rate identifier; 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)
403 merchant_not_approved The merchant has not been approved after review yet, or was rejected
404 invoice_not_found The invoice does not exist, or its merchant is disabled
409 asset_already_selected The invoice already has a different asset
409 invoice_expired The deadline has passed, or the invoice is no longer in the created status
422 address_mode_disabled The platform has temporarily paused the address mode of the merchant — see a paused mode
422 asset_not_offered The asset is not in the invoice's currencies, or it has no exchange rate
422 asset_not_supported The asset is unknown or not accepted for payments
422 asset_payments_disabled The operator has temporarily disabled payments in the asset
422 asset_disabled_by_merchant Payments in the asset are disabled in the project settings
422 amount_below_minimum The price in the asset at the rate used is below its minimum deposit
429 rate_limited The request limit for the IP address is exceeded
500 internal_error An unhandled error of the service
503 rate_unavailable The coin of the asset has no fresh exchange rate right now. Nothing is recorded; the customer can choose another asset or try again later
503 addresses_exhausted The address derivation indexes have run out. Practically unreachable
503 service_unavailable The service cannot derive a new address in its current configuration; the message says why

Example

POST /checkout/v1/invoices/3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63/select
Content-Type: application/json

{"asset": "USDT_TRON", "rate_id": "48213"}

200, the invoice without a token from the previous example after the choice at the rate it showed:

{
  "invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
  "merchant_name": "Shop",
  "status": "created",
  "asset": "USDT_TRON",
  "asset_icon_slug": "usdt",
  "symbol": "USDT",
  "network": "tron",
  "network_name": "TRON",
  "network_icon_slug": "tron",
  "token_standard": "TRC-20",
  "address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
  "address_explorer_url": "https://tronscan.org/#/address/TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
  "wallet_url": null,
  "price": { "amount": "100.000000", "currency": "USD" },
  "choices": null,
  "rates_unavailable": false,
  "quote": { "rate": "0.999100000000000000", "currency": "USD", "expires_at": "2026-09-11T21:03:05.531Z" },
  "amount_requested": "100.090082",
  "amount_expected": "101.090983",
  "amount_received": "0.000000",
  "amount_remaining": "101.090983",
  "fees": {
    "service": { "payer": "customer", "percent": "1.00", "fixed": "0.000000", "amount": "0.000000" },
    "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
  },
  "expires_at": "2026-09-11T21:03:05.531Z",
  "confirmations_required": 20,
  "confirmation_estimate": null,
  "deposits": [],
  "redirect": null,
  "back_url": null
}