Authentication
This document describes how a merchant's server authenticates to the API with an API key, and the request limits, key rotation and network rules that come with the key. Read it before sending the first request.
API key
Every request to the /v1 routes passes the merchant's API key in the X-Api-Key header. The one exception is GET /v1/openapi.json, the machine-readable description of the API, which needs no key.
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"
The key belongs to a merchant, which the cabinet (Dashboard) calls a project. The project owner gets the first key when submitting the project application in the cabinet, and issues more keys in the "API keys" section of the project with "Issue new key". A key has the form sk_<16 hex characters>_<43 base64url characters>: the first part is the public identifier of the key, which the cabinet shows in the list of keys, and the second part is the secret.
The service does not store the key itself, only an argon2id hash of its secret, and compares hashes in constant time. This has two consequences:
- a lost key cannot be recovered, only replaced with a newly issued one;
- the key is shown once, at the moment it is issued — save it in a secrets manager.
Rules for using the key:
- The key is meant for server-to-server requests only. It must never end up in a browser, a mobile app, a repository or logs.
- A key sees only its own merchant's data: an invoice of another merchant does not exist for it (
404). - A key works from the moment it is issued. Until the project is approved after review,
POST /v1/invoicesandPOST /v1/addressesanswer403 merchant_not_approved, while reading invoices works. - The service remembers a successful key check for up to 60 seconds. A revoked key and a key of a disabled merchant are therefore refused no later than 60 seconds after the change.
Errors
The key is checked before the request body, so a request with an invalid key gets 401 even if its body is also invalid.
| HTTP | error.code |
When |
|---|---|---|
| 401 | unauthorized |
The header is missing, the key has the wrong format, is unknown or revoked, or its secret does not match. The response does not say which |
| 403 | merchant_disabled |
The key is valid, but the merchant is disabled — by the operator or by the owner in the cabinet |
| 429 | rate_limited |
A request limit is exceeded — see rate limits |
Example: a request without a key.
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
401:
{"error":{"code":"unauthorized","message":"Missing or invalid X-Api-Key header"}}
Rate limits
Requests to the /v1 routes are limited twice, in a window of 60 seconds: per client IP address and per API key. The limit values are set by the operator of the installation.
- The IP limit is checked first, before the key: requests with a missing or invalid key count toward it too.
- The key limit is checked after the key: it counts the requests of one key from any address.
GET /v1/openapi.jsonand paths that do not exist are not limited.
Responses of the limited routes carry these headers:
| Header | Meaning |
|---|---|
x-ratelimit-limit |
The number of requests allowed in the window. A request that passed both limits reports the key limit; a request refused by a limit reports that limit |
x-ratelimit-remaining |
How many requests are left in the window |
x-ratelimit-reset |
Seconds until the window resets |
retry-after |
Only in a 429 response: seconds to wait before retrying |
Conditional reads
Every plain read answers with an ETag, a tag of that exact answer: GET /v1/assets, GET /v1/invoices, GET /v1/invoices/{id} and GET /v1/deposits/{tx_hash}. Send the tag back in If-None-Match and an answer that has not changed comes as 304 Not Modified with no body — useful to a product that polls an invoice while the customer pays, or walks the change feed between webhooks.
GET /v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
X-Api-Key: <your key>
If-None-Match: "LXEWQrcmsEQBYnyp-6wy9chTD7GQPMTbAiWHF5IaSIE"
- A
304carries the sameETagand no body: what you already have is current. A200carries a newETag— store it next to the answer and send it with the next request. - The tag covers the whole body, so any change to the answer changes it: a transfer that arrived, a status, an amount repriced, the same invoice asked for with its QR code. It is not a version number of the invoice, and two different requests have two different tags even about the same invoice.
- The tag is opaque: a quoted string to be stored and returned, with nothing to be read out of it.
If-None-Match: *always matches the answer the service holds. - A conditional request is a request: it counts toward the limits above and costs the service the same work. What it saves is on your side — the body over the network and its parsing.
- Only a
200is tagged. An error is never tagged, and a304never replaces one: a request that would answer404answers404.
Project settings
An integration configures the project itself, with its own key: where to send webhooks, where the payment page returns a customer, and which tokens the project accepts. Nobody has to open the dashboard for that (ADR-0067).
These routes never return the webhook signing secret. It is shown once, when the project is created; an API key is not access to the secret.
GET /v1/project
{
"project_id": "7f2c1b94-3d5a-4e86-9a2f-6c0b1d8e4a37",
"name": "Shop",
"address_mode": "per_invoice",
"webhook_url": "https://shop.example/hooks",
"return_url": "https://shop.example/thanks",
"cancel_url": null,
"review_status": "approved"
}
address_modedecides what this API accepts:POST /v1/addressesworks only inper_user, and an invoice inper_userwithoutexternal_user_idis refused — see addresses.review_statusofpendingorrejectedmeans invoices and permanent addresses answer403 merchant_not_approved: the project is not approved yet.
PATCH /v1/project
Changes one or more fields; an empty body and any other field answer 400:
PATCH /v1/project
X-Api-Key: <your key>
Content-Type: application/json
{ "webhook_url": "https://shop.example/hooks", "return_url": "https://shop.example/thanks" }
webhook_urlcan be set and replaced, never removed:nullanswers400. Events already queued need somewhere to go.return_urlandcancel_url: a URL sets it,nullclears it, a field left out stays as it is. The rules of the addresses are the payment page's.- The address mode is not changed here: an operator allows it.
- The answer is the project after the change. The change goes to the audit log under the name of the key that made it.
GET /v1/project/assets
Which assets the project accepts, with both switches — the platform's and its own:
{
"assets": [
{
"asset": "USDT_TRON",
"network": "tron",
"symbol": "USDT",
"decimals": 6,
"min_deposit": "1000000",
"platform_enabled": true,
"enabled": true,
"accepting": true
}
]
}
accepting says whether an invoice in this asset can be created now: the
platform takes it and the project has not switched it off.
PUT /v1/project/assets
Chooses the assets the project accepts. Only assets the service accepts for payments may be named; one to fifty, each at most once:
PUT /v1/project/assets
X-Api-Key: <your key>
Content-Type: application/json
{ "assets": [{ "asset": "USDT_TRON", "enabled": true }, { "asset": "TRX_TRON", "enabled": false }] }
An asset the service does not accept answers 422 asset_not_supported, and
nothing of the request is written. The answer is the whole list after the change.
A switch that changes nothing writes no audit record.
Rotation
A merchant can have several active keys at the same time, so a key is replaced without downtime:
- The project owner issues a new key in the "API keys" section of the project in the cabinet.
- The product switches to the new key.
- The owner revokes the old key there. The API refuses the revoked key no later than 60 seconds after the revocation. A revoked key cannot be restored.
Network
The merchant API answers at https://api.stons.io/v1. In a production installation an HTTPS proxy sits in front of it:
- only paths that start with
/v1/are reachable from the internet; any other path returns404with an empty body; - a request over
http://is redirected tohttps://with code308. By the time of that redirect, the header with the key has already crossed the network in plain text, so configure your client with thehttps://address.
The /admin/v1, /cabinet/v1 and /checkout/v1 routes serve the staff admin panel, the cabinet application and the checkout page server of STONS. They are not part of a merchant integration and are not reachable from the internet.