Skip to content
STONS

Turnkey integration with an AI assistant

This document is a brief for an AI assistant in the merchant's project: Claude Code, Cursor, or any other assistant that writes code from instructions. It lists, in the order of the work, what to build to accept payments with STONS, which decisions to make and how, and what to check at the end. Every rule links to the API document that states it; if they disagree, the API document is right.

Glossary

The cabinet (Dashboard) is the STONS web application where the project owner manages the project. The cabinet and the API name some things differently; this documentation uses the terms in the first column.

Term In the cabinet Meaning
cabinet "Dashboard" The STONS web application at https://app.stons.io where the project owner manages the project
merchant project ("Projects") A website, bot, or service that accepts payments with its own API keys and settings
project owner the account that created the project Issues API keys and sets the webhook URL, the return URLs, the tokens the project accepts, and who pays the fees. Sees webhook deliveries and sends undelivered ones again
operator support STONS staff: reviews projects, keeps the payout whitelist, approves payouts that wait for a person, sets fees and limits, allows a project the restricted modes — a permanent address per user and direct settlement to the project's own wallet
invoice invoice, in "Payments" A request to pay a specific amount before a deadline: POST /v1/invoices
API key "API key" sk_<16 hex characters>_<43 base64url characters>, sent in the X-Api-Key header. Shown once, when it is issued
webhook secret "Webhook signing secret" The key of the HMAC-SHA256 signatures of webhooks and of redirects from the checkout page. Shown once, when the project is created. Used as the string itself
asset token and network The identifier of a token on a network, <TICKER>_<NETWORK>: USDT_TRON, USDC_SOLANA
checkout page payment link The service's own page for one invoice, at the checkout_url link from the invoice
confirmation — A block added on top of the block with a transfer. A transfer counts once it has as many confirmations as its asset requires
top-up — In per_user mode: a transfer to a user's permanent address that pays no invoice. It is credited to the project balance, and deposit.confirmed arrives with invoice_id set to null
payout — A transfer from the service's hot wallet to an address on the merchant's whitelist: POST /v1/payouts
whitelist — The payout addresses of a merchant, per asset. The operator adds them; a new address becomes usable after a delay, 24 hours by default

Step 0. What to ask the human

Before you write code, get from the project owner:

  1. The API key. The owner issues it in the project settings in the cabinet — see authentication.
  2. The webhook secret. The cabinet shows it once, together with the first key, when the project is created — see webhooks.
  3. Whether the project has passed review. Until it is approved, invoices are not created: 403 merchant_not_approved. Keys already work, so the code can be written — see error codes.
  4. The address mode of the project: per_invoice — a new address for every invoice, per_user — a permanent address for each user. A project is created in per_invoice; per_user is a restricted mode an operator allows per project, and only then can the project be moved into it. The mode can change later, and a change decides where the next payment is collected: addresses already issued keep their own mode and keep being credited — see addresses. Two refusals mean different things: 422 address_mode_disabled — the platform has paused the mode for everybody (a paused mode); 422 mode_not_allowed — this project was not allowed the mode. Ask the owner which mode the project is in now rather than assuming it never moves.
  5. How the payment is shown: on the product's own page, or on the checkout page. Use the rules of step 3 to ask the right question.
  6. The tokens: which assets the product offers. Payments work on TRON, on seven EVM networks, on TON and on Solana — in USDT, USDT0, USDC and the native coin of each network; USDC_TRON is switched off by the operator entirely — see networks and tokens. Do not hard-code this list. GET /v1/assets returns the assets the project can accept right now, with the network, the decimals and the minimum amount of each; the project's settings in the cabinet decide which tokens it accepts — see list the assets.
  7. Who pays the service fee — the "Who pays the fees" setting of the project. If the customer pays it, the amount to pay is greater than the price — see fees.
  8. For the checkout page: the return and cancel URLs, which the owner sets in the project settings as "After successful payment" and "Return to the shop / on failure" — see return URLs.
  9. Whether payouts are needed. If they are, the operator must add every payout address to the whitelist in advance — see payouts.

When the webhook handler is ready, point the project at it yourself: PATCH /v1/project with webhook_url, using the same key — nobody has to open the cabinet (project settings). GET /v1/project first tells you what is set now, and whether the project is approved. Until a URL is set, the project gets no events, and events from that time are never sent later — see delivery.

The same key sets the return addresses of the checkout page (return_url, cancel_url) and chooses the tokens the project accepts (GET and PUT /v1/project/assets). Two limits worth knowing before you try: the webhook URL can be replaced but never removed, and the signing secret is not returned by any route — ask the owner for it once, when the project is created.

Do not invent values on the human's behalf: only they have the key and the secret.

Step 1. Environment variables

The key and the secret live only on the product's server, in a secrets manager or in environment variables. They are not in a browser, a mobile app, a repository, or logs — see checklist. The project chooses the variable names, for example:

PAYMENTS_API_URL=https://api.stons.io
PAYMENTS_API_KEY=sk_...
PAYMENTS_WEBHOOK_SECRET=...

Use the API address with https://: over http:// the key would travel in plain text before the redirect even happens — see authentication.

Step 2. API client

  • The base URL is https://api.stons.io/v1, the key goes in the X-Api-Key header, and request and response bodies are JSON with Content-Type: application/json.
  • The API description in OpenAPI 3.1 format is at https://api.stons.io/v1/openapi.json. Request bodies accept only the documented fields: an unknown field gets 400 validation_failed.
  • Amounts are strings in units of the asset: "49.9" in a request, "49.900000" in a response. An amount of an asset in a response has exactly as many digits after the point as the asset has decimals, so removing the point gives the integer in minimum units. A request amount must not have more decimals than the asset — see amounts.
  • An error has the body {"error": {"code", "message", "details"}}. Base decisions on error.code, not on the message text — see error codes.
  • 429 rate_limited: retry after the number of seconds in the Retry-After header — see rate limits.
  • 500 internal_error and 503 service_unavailable: retry later with the same order_id or reference. What to do on the other codes is in the code list.
  • Keep X-Request-Id from the response in your logs: support uses it to find the request.

Step 3. Choose how the payment is shown

Apply the first rule that matches:

  • If the customer must choose the coin and the network, create the invoice without asset and send the customer to checkout_url (step 5B). Such an invoice has no address until the choice, so it cannot be shown on your own page. If the service has no checkout page, the request gets 422 asset_required: then use the next rule — see the customer chooses the token.
  • If the product prices in dollars and the coin is known in advance, create the invoice with asset and price_currency: "USD", putting the price in amount. The service quotes it at its own rate and answers with address, amount_expected and quote.rate in the same response — you neither need nor should compute a rate yourself. To show «≈ 0.45 SOL» before creating anything, ask GET /v1/rates — see a price in dollars in the asset you name.
  • If the product has its own payment screen, create the invoice with asset and show address and amount_expected on it (step 5A).
  • Otherwise create the invoice with asset and send the customer to checkout_url (step 5B). If checkout_url is null, the service has no checkout page: use step 5A.

Independently of the choice:

  • If the mode is per_user, pass external_user_id in every invoice. A user's permanent address without an invoice comes from POST /v1/addresses — see addresses.
  • If the product credits per_user top-ups to user balances, check every deposit.confirmed with invoice_id set to null with GET /v1/deposits/{tx_hash}, and credit the user only when the transfer with the event's log_index has status = confirmed and credited = true — see verify a transfer.

Step 4. Create an invoice

POST /v1/invoices — see creating an invoice.

curl -s -X POST https://api.stons.io/v1/invoices \
  -H "X-Api-Key: $PAYMENTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "ord_1001", "asset": "USDT_TRON", "amount": "49.9", "expires_in": 3600}'
  • order_id is the order number in the product: 1–200 visible ASCII characters, no spaces. It is the idempotency key: after a timeout, retry the request with the same order_id and the same fields — you get the same invoice with code 200 instead of 201, and no second invoice is created. The same order_id with a different amount, asset, user, or return URLs gets 409 order_id_conflict — see idempotency.
  • expires_in is optional: from 300 (5 minutes) to 259200 seconds (72 hours), 3600 by default.
  • The amount must be at least the minimum deposit of the asset, otherwise 422 amount_below_minimum.
  • Store invoice_id with the order: you use it to check the status later.

Without asset, amount is the price in US dollars, and the optional currencies lists the assets the customer chooses from. In the response, asset, network, address, and the amounts in the asset are null, and price holds the price:

curl -s -X POST https://api.stons.io/v1/invoices \
  -H "X-Api-Key: $PAYMENTS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "ord_1002", "amount": "49.9"}'

Step 5A. Payment on your own page

Show the customer, from the response:

  • address — in full, never shortened to "start…end": shortened addresses are the target of attacks that substitute a look-alike address — see displaying the address;
  • amount_expected and the asset. This is the amount to pay: if the customer pays the service fee, it is greater than the order price, and the price itself is in amount_requested — see fees;
  • the network of the asset by its name from network_name — TRON, one of the EVM networks, TON or Solana — with a warning that a transfer of another token or on another network is not credited automatically — see checklist;
  • the deadline, expires_at;
  • payment_uri, a link that opens a wallet with the transfer filled in, when it is not null: the customer then neither copies the address nor types the amount. TON gives one today; on Solana and the other networks it is null, so the address and the amount are copied from the page;
  • a QR code — ask for it with include=qr when the invoice is created or read, and show qr.address as the src of an <img>. It holds the same address, and it is the same image our own payment page draws, so no QR library is needed on your side — see QR code.

What does not change from invoice to invoice — the confirmations of the asset (confirmations), its mark (icon), the token standard and the minimum amount — take once from GET /v1/assets and keep: confirmations is what «2 of 20» is drawn from while a transfer gathers them.

A project may be settled directly — the money of each payment is forwarded to the project's own wallet, net of the service fee, instead of being kept. An operator allows that mode for the project; the owner of the project enters the wallet itself in the cabinet, and a member of staff approves it — until that, the address receives nothing. Nothing in the API changes for it, and the order is fulfilled at confirmed either way; what does change is the meaning of two things: the balance of such a project stays at zero, because the money is already on its wallet, and settled means that wallet rather than the service's treasury. In an asset the project has no wallet in force for, that payment goes to the treasury instead and the project withdraws it as any other — so a zero balance is not a promise either. Do not build an integration that waits for the balance to grow — see where the money goes.

The money that has arrived is read with GET /v1/balance: the ledger balance per asset, what is still unconfirmed, and both in dollars — see balance.

Get the status from webhooks (step 6) and with a GET /v1/invoices/{id} request. When a webhook never arrives, walk the change feed — step 7. A screen that polls keeps the ETag of the last answer and sends it back in If-None-Match: nothing changed is then a 304 without a body — see conditional reads.

Step 5B. The checkout page

Send the customer to checkout_url from the response. If the invoice was created without asset, the page first lets the customer choose the coin and the network. The page shows the amount, the address, the QR code, and the status, and when the invoice reaches an outcome, it sends the customer to the return or cancel URL — see checkout page.

  • A single invoice can override the URLs with return_url and cancel_url in POST /v1/invoices, only within the project's sites, otherwise 422 redirect_url_not_allowed — see return URLs.
  • While the invoice is unpaid, the page shows a "Return to the shop" button that leads to the cancel URL with status=created, status=seen, or status=partially_paid. The invoice can still be paid: do not cancel the order on such a visit — see return before an outcome.

Step 6. Webhook handler

The service sends a POST to the project's webhook URL. The handler — see webhooks:

  1. Reads the raw body of the request, before JSON parsing.
  2. Verifies the signature: X-Signature: sha256=<hex> is an HMAC-SHA256 with the webhook secret over the string <X-Timestamp>.<raw body>. Rejects the webhook if X-Timestamp differs from the current time by more than 5 minutes, and compares signatures in constant time. Take the verification code in Node.js or Python and the worked example to test it against from signature.
  3. Drops duplicates by the event id: the same event can arrive several times.
  4. Responds 2xx within 10 seconds, including to event types it does not process, and does the work asynchronously: without a 2xx response the webhook is delivered again.
  5. Does not trust the body. For invoice.* and for deposit.* with an invoice_id, requests GET /v1/invoices/{id}; for deposit.* without an invoice_id, GET /v1/deposits/{tx_hash}; for payout.*, GET /v1/payouts/{id}; and acts on the API response — see the main rule.

Delivery order is not guaranteed: invoice.confirmed can arrive before invoice.seen. Rely on the state from the API.

Step 7. When to fulfill the order

By the status from GET /v1/invoices/{id} — see statuses:

Status What to do
created, seen Wait. seen means the transfer has been detected but can still be rolled back
partially_paid Do not fulfill the order. Part of the amount arrived before the deadline: if the product shows the payment itself, show the customer amount_remaining and the same address — see partial payment
confirmed, settled Fulfill the order. An overpayment is also confirmed, with amount_received greater than amount_expected
underpaid The deadline has passed, and part of the amount arrived: the product decides
expired The deadline has passed without a payment: close the order
paid_late The payment arrived after the deadline: the product decides
  • Compare your price with amount_requested for an invoice created with asset. For an invoice created without asset, compare with price.amount: amount_requested is null before the choice and the price converted at an exchange rate after it — see price and exchange rate.
  • expired and underpaid can still become paid_late if money arrives later — see checkout page.
  • Make fulfillment idempotent by order_id: a repeated webhook, a second event of the same change, or a redirect must not fulfill the order a second time.

Step 8. Return from the checkout page

If the checkout page is used, the customer arrives at the return URL after confirmed, settled, or paid_late, and at the cancel URL after expired or underpaid or with the "Return to the shop" button. The parameters are invoice_id, order_id, status, ts, and signature. The handler — see redirect signature:

  1. Verifies the signature: HMAC-SHA256 with the webhook secret over the lines redirect, ts, invoice_id, status, order_id joined with the \n character, and a 5-minute window for ts. The verification code and a worked example are in the linked section.
  2. Checks that invoice_id is the invoice of this order_id.
  3. Takes the status from GET /v1/invoices/{id}, not from the parameter. The order is never fulfilled on a redirect.

Step 9. Payouts, if you need them

POST /v1/payouts — see payouts:

  • You can pay only to addresses on the merchant's whitelist, after their delay. A payout address is never taken from transaction history.
  • reference is the idempotency key: a retry with the same reference, asset, address, and amount returns the same payout with 200 instead of 202. Different values get 409 reference_conflict — see idempotency.
  • pending_review is not an error: the payout is accepted and waits for a person's decision, and review_reasons says why.
  • 503 payouts_halted: retry later with the same reference.
  • Take the outcome from GET /v1/payouts/{id}. The payout.* webhooks are signals to make that request, and not every status change has one: which statuses are final and which transitions send a webhook is in payout statuses.

Step 10. Before launch

Go through the whole production checklist and test the full path with a small amount: invoice, payment, webhook, status check, fulfillment.

What never to do

  • Do not hardcode decimals and do not convert amounts to floating-point numbers: keep strings or integers in minimum units — see amounts.
  • Do not shorten an address anywhere it is shown or logged — see displaying the address.
  • Do not lower-case an address and do not normalise it in any other way. On Solana the address is base58 and its case is part of it, so a lower-cased address is one nobody holds the key to; store and compare every address exactly as the API returned it — see identifiers and decimals.
  • Do not fulfill an order on a webhook body or on a redirect without GET /v1/invoices/{id} — see the main rule and checkout page.
  • Do not fulfill an order on seen.
  • Do not parse and serialize the webhook body again before verifying the signature — see signature.
  • Do not create a second invoice or payout after a timeout: retry with the same order_id or reference.
  • Do not take a payout address from transaction history — see payouts.
  • Do not put the API key or the webhook secret in a browser, a mobile app, a repository, or logs — see authentication.
  • Do not base decisions on error.message — see error codes.

Self-check

Before you report the integration as done, confirm each item in the code:

  • The key and the secret are read from the server environment and are not logged.
  • Every API call goes to https://, sends X-Api-Key, and handles error.code, 429 with Retry-After, and 500 and 503 service_unavailable with a retry under the same idempotency key.
  • Amounts are strings or integers in minimum units everywhere, including the database of the product.
  • An invoice is created with a stable order_id, and invoice_id is stored with the order.
  • The page shows the full address, amount_expected, the asset, and the network, or the customer is sent to checkout_url.
  • The webhook handler verifies the signature against the raw body with a 5-minute window, drops duplicates by id, responds 2xx within 10 seconds, and passes the worked example from signature.
  • The order is fulfilled only when GET /v1/invoices/{id} returns confirmed or settled, once per order_id.
  • The product has a decision for underpaid, paid_late, and an overpayment.
  • An order is not fulfilled on partially_paid.
  • The return and cancel URL handlers verify the signature and never fulfill or cancel an order on their own.
  • Payouts, if any, use a stable reference and take their outcome from GET /v1/payouts/{id}.
  • The whole path has been tested with a small amount, and the production checklist is complete.