Dolix Pay · v1
Dolix Pay API — accepting payments
You issue an invoice and give the payer a link; the money arrives in your wallet in the service, minus the fee. The payer can pay in crypto, from their wallet in the app, or via SBP (Russia's Faster Payments System). The payment page opens inside the Telegram app.
Format: JSON, UTF-8. Amounts are always strings with a decimal point ("100", "12.5"); the invoice currency is USDT. Timestamps are ISO 8601 (UTC).
Getting started
- Open the bot → “Profile” → Dolix Pay → “Connect Dolix Pay”. Anyone can connect.
- “API and webhooks” → create a key (it is shown only once) and, if you like, set a webhook URL.
- Or issue invoices by hand: “Create invoice” in the same section, no API needed.
Authentication
A key of the form rsp_live_… is sent in the Authorization: Bearer <key> or X-API-Key: <key> header. We store only a hash of the key; if you lose a key, revoke it in the app and create a new one. Up to 5 keys can be active at the same time.
The limit is 120 requests per minute per key. Above it you get 429 with a Retry-After header.
Create an invoice
{
"amount": "100",
"title": "Заказ №104",
"description": "Два товара",
"external_id": "ORD-104",
"payer_telegram_id": "5512340987",
"lifetime_sec": 86400
}| Field | Required | Description |
|---|---|---|
amount | yes | Amount in USDT, as a number or a string. |
title | yes | Title, up to 120 characters. |
description | no | Description, up to 500 characters. |
external_id | no | Your order number (A–Z a–z 0–9 . _ : -, up to 64). A repeat request with the same number returns the same invoice (200); with different data, 409. |
payer_telegram_id | no | The payer's Telegram ID. If set, only that user can pay. If not, the invoice goes to whoever opens the link first. |
lifetime_sec | no | Lifetime, from 300 to 604800 seconds (one day by default). |
The Idempotency-Key header works as external_id when the latter is not sent. You cannot pay your own invoice: the merchant cannot be the payer.
{
"invoice": {
"id": "210",
"status": "created",
"amount": "100",
"fee": "1",
"net_amount": "99",
"currency": "USDT",
"title": "Заказ №104",
"description": "Два товара",
"external_id": "ORD-104",
"payer_telegram_id": null,
"pay_url": "https://t.me/<bot>?startapp=serviceOrder_<token>",
"created_at": "2026-10-04T16:00:00.000Z",
"expires_at": "2026-10-05T16:00:00.000Z",
"paid_at": null,
"payment_method": null,
"credited": null,
"network_fee": null
}
}Put pay_url on a button in your bot or send it to the payer: the link opens the app on the payment screen. The fee is fixed when the invoice is created and does not change afterwards. After a crypto payment, credited shows how much was actually credited to you in the payment coin, and network_fee shows the network fee that was withheld.
Get, list, cancel
| Method | Description |
|---|---|
GET /invoices/{id} | A single invoice. |
GET /invoices?status=&limit=&before_id=&external_id= | A list, newest first. Paginate with next_before_id. Use external_id to find your order. |
POST /invoices/{id}/cancel | Cancel an unpaid invoice. A paid invoice cannot be cancelled (409); you make any refund yourself from your wallet. |
GET /me | Your fee and invoice limits. |
Invoice statuses
| Status | Meaning |
|---|---|
created | Issued; the payer has not opened the link yet. |
awaiting_payment | The payer is known; waiting for payment. |
under_review | Payment received and under review (AML; for SBP payments, also a match of the payer's full name against KYC). After the review the invoice becomes paid on its own. |
paid | Paid; the money has been credited to you. |
expired | The lifetime ran out without payment. |
cancelled | Cancelled. |
payment_method: wallet (from a wallet), crypto (on-chain), sbp (via SBP; an SBP amount is credited with a one-day hold, T+1).
SBP payments are available only to payers who have passed KYC, and they must pay from a personal card in their own name. If the payer's bank sends a full name that does not match KYC, the payment goes to manual review: the invoice is under_review and the money is not credited to you until support decides.
Webhooks
Set the URL in the “API and webhooks” section (https only, a public address) and we will send a POST with JSON.
{
"id": "evt_91",
"type": "invoice.paid",
"created_at": "2026-10-04T16:20:00.000Z",
"data": { "invoice": { "id": "210", "status": "paid", "external_id": "ORD-104" } }
}Events: invoice.paid, invoice.under_review, invoice.expired, invoice.cancelled. A delivery may be repeated, so handle events idempotently (by the event id). invoice.paid may arrive hours after invoice.under_review. Rely on invoice.paid.
Respond with any 2xx code within 10 seconds. Otherwise we retry the delivery after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (8 attempts in total). The “Send test” button in the app sends a ping event.
Verifying the signature
Header X-RSPAY-Signature: t=<unix-seconds>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<body as received>"). Verify against the raw body and reject requests with a stale t timestamp (for example, older than 5 minutes). The secret is shown in the “API and webhooks” section. The request also carries X-RSPAY-Event (the type) and X-RSPAY-Delivery (the event number).
const crypto = require('crypto');
// rawBody is the request body AS RECEIVED (string/Buffer), not re-serialized JSON
function verify(rawBody, header, secret, toleranceSec = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
if (!m) return false;
if (Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}import hmac, hashlib, re, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m[1])) > tolerance:
return False
signed = m[1].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, m[2])Errors
{ "error": { "code": "amount_too_small", "message": "…", "details": { "min_amount": "1" } } }| HTTP | Codes |
|---|---|
| 400 | invalid_amount, amount_too_small, invalid_title, invalid_description, invalid_external_id, invalid_payer, invalid_lifetime, invalid_status, invalid_request |
| 401 | unauthorized |
| 403 | merchant_blocked |
| 404 | invoice_not_found, not_found |
| 409 | external_id_conflict, too_many_open_invoices, invoice_already_paid, invoice_has_payment |
| 429 | rate_limited |
| 500 | internal_error |
Funds and security
- The money is credited to your wallet in the coin and network the buyer paid with. Deducted from the amount are the Dolix Pay fee (a percentage, but not less than a per-payment minimum) and, for crypto payments, the network fee for moving the coins to the pooled wallet.
- Every incoming payment goes through an AML check. Until it passes, the money is not credited to you and the invoice is
under_review. An address that received unverified coins is no longer issued for payments. - You withdraw your earnings from the wallet as usual, in any available network; the withdrawal address is checked too.
- Keep the key and the webhook secret on your server. If they end up in the wrong hands, revoke the key and reissue the secret in the app.
How to test
- Create a key in “Profile → Dolix Pay → API and webhooks” and put it in the
DOLIX_PAY_KEYvariable. - Check the key:
GET /meshould return your fee and limits. - Issue a test invoice for 1 USDT.
- Open
pay_urlfrom another Telegram account and pay (you cannot pay yourself). - Make sure
GET /invoices/{id}returnspaidand the amount, minus the fee, has appeared in your wallet.
curl -X POST https://exchange.dolix.io/api/rspay/v1/invoices \
-H "Authorization: Bearer $DOLIX_PAY_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":"1","title":"Тестовый счёт","external_id":"TEST-1"}'