Dolix Pay API

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.

Base URL
https://exchange.dolix.io/api/rspay/v1

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

  1. Open the bot → “Profile” → Dolix Pay → “Connect Dolix Pay”. Anyone can connect.
  2. “API and webhooks” → create a key (it is shown only once) and, if you like, set a webhook URL.
  3. 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

POST/invoices
request body
{
  "amount": "100",
  "title": "Заказ №104",
  "description": "Два товара",
  "external_id": "ORD-104",
  "payer_telegram_id": "5512340987",
  "lifetime_sec": 86400
}
FieldRequiredDescription
amountyesAmount in USDT, as a number or a string.
titleyesTitle, up to 120 characters.
descriptionnoDescription, up to 500 characters.
external_idnoYour 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_idnoThe payer's Telegram ID. If set, only that user can pay. If not, the invoice goes to whoever opens the link first.
lifetime_secnoLifetime, 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.

response 201
{
  "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

MethodDescription
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}/cancelCancel an unpaid invoice. A paid invoice cannot be cancelled (409); you make any refund yourself from your wallet.
GET /meYour fee and invoice limits.

Invoice statuses

StatusMeaning
createdIssued; the payer has not opened the link yet.
awaiting_paymentThe payer is known; waiting for payment.
under_reviewPayment 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.
paidPaid; the money has been credited to you.
expiredThe lifetime ran out without payment.
cancelledCancelled.

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.

webhook body
{
  "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).

Node.js
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]));
}
Python
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

format
{ "error": { "code": "amount_too_small", "message": "…", "details": { "min_amount": "1" } } }
HTTPCodes
400invalid_amount, amount_too_small, invalid_title, invalid_description, invalid_external_id, invalid_payer, invalid_lifetime, invalid_status, invalid_request
401unauthorized
403merchant_blocked
404invoice_not_found, not_found
409external_id_conflict, too_many_open_invoices, invoice_already_paid, invoice_has_payment
429rate_limited
500internal_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

  1. Create a key in “Profile → Dolix Pay → API and webhooks” and put it in the DOLIX_PAY_KEY variable.
  2. Check the key: GET /me should return your fee and limits.
  3. Issue a test invoice for 1 USDT.
  4. Open pay_url from another Telegram account and pay (you cannot pay yourself).
  5. Make sure GET /invoices/{id} returns paid and the amount, minus the fee, has appeared in your wallet.
first invoice
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"}'