Skip to content
STONS

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/invoices and POST /v1/addresses answer 403 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.json and 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 304 carries the same ETag and no body: what you already have is current. A 200 carries a new ETag — 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 200 is tagged. An error is never tagged, and a 304 never replaces one: a request that would answer 404 answers 404.

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_mode decides what this API accepts: POST /v1/addresses works only in per_user, and an invoice in per_user without external_user_id is refused — see addresses.
  • review_status of pending or rejected means invoices and permanent addresses answer 403 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_url can be set and replaced, never removed: null answers 400. Events already queued need somewhere to go.
  • return_url and cancel_url: a URL sets it, null clears 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:

  1. The project owner issues a new key in the "API keys" section of the project in the cabinet.
  2. The product switches to the new key.
  3. 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 returns 404 with an empty body;
  • a request over http:// is redirected to https:// with code 308. By the time of that redirect, the header with the key has already crossed the network in plain text, so configure your client with the https:// 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.