# XAOC Pay API v1.4 Base URL: https://xaocpay.store Public documentation: https://xaocpay.store/docs OpenAPI JSON: https://xaocpay.store/openapi.json Accounts are created by the administrator after a connection application is approved. There is no public registration. Create API credentials in the merchant settings. Never put keys in client-side JavaScript. All API requests are server-to-server and merchant-scoped. The API uses live payments. No test or simulation operation is exposed publicly. ## Authentication and signing Every request includes X-API-Key, X-Timestamp (Unix seconds), X-Nonce (new UUID) and X-Signature (lowercase hex HMAC-SHA256). Signing input is the exact concatenation: timestamp + "." + nonce + "." + METHOD + "." + URL_PATHNAME + "." + RAW_REQUEST_BODY Use uppercase HTTP method. URL_PATHNAME includes /api/v1/ and excludes query string. GET has an empty body, leaving a trailing dot. Timestamp tolerance is 300 seconds. Nonces cannot be reused for 10 minutes. Sign exact UTF-8 bytes sent, without additional newlines. A secret belongs only to one merchant. A blocked user or merchant cannot use the API. ## POST /api/v1/payments JSON fields: - amount: required decimal RUB string, two fractional digits at most, minimum 1 RUB and maximum 1,000,000 RUB; actual channel may impose narrower limits. - order_id: required unique merchant order identifier, up to 100 characters. Repeating identical order_id and business fields returns the same payment; changing parameters returns 409. Use a new nonce even on retries. - description: optional customer-visible payment purpose, maximum 500 characters; omitted purpose shows the order number. - customer_id: optional customer reference, maximum 100 characters. - method: auto (default), sbp or crypto. Card payments are temporarily unavailable. The alias general also opens the unified payment form. Omitting method opens the unified form. A specific method opens its requisites directly. - buyer_share: integer 0..100, optional, defaults to the selected method setting, falling back to the merchant setting. This is the percentage of the combined payment-channel and platform fee paid by the customer. 0 = merchant pays; 100 = customer pays; values between split the fee. - metadata: optional JSON object, maximum 8 KB. The administrator sets the platform margin per user/merchant. Channel commission is learned from actual channel quotes or configured by the administrator, including whether it is added or withheld. The allocation buyer_share applies to both fees. Missing channel pricing blocks new live invoices until configured, rather than silently applying an unknown surcharge. Buyer surcharge is grossed up with cent precision; checkout shows the actual channel total. Conversion and settlement use actual provider amounts. Withdrawal requests in the console require at least 10 USDT for crypto and 35 USDT for an RF card; bank conversion is agreed with the administrator. Response 201: payment_id, id, order_id, description, customer_id, status, amount, charged_amount, platform_fee, method_options (for auto), currency (RUB), method, checkout_url, created_at, expires_at, metadata. For auto, charged_amount and platform_fee are null until a method is selected. method_options lists the exact charged_amount for each enabled, priced method. No channel invoice is issued until the customer selects a method on checkout. The selected method is then locked to that invoice; repeated order_id still returns the same invoice. Disabled methods and merchant-specific limits are enforced on the server. Always send the customer to checkout_url. Do not treat creation or browser redirects as payment confirmation. SBP checkout uses the current qr.nspk.ru bank link; the same link is encoded into the QR image. Crypto checkout shows the selected asset, network, amount and address. ## GET /api/v1/payments Signed request, empty body. Returns {items, total, page, limit, pages, hasMore}. Reads the merchant's entire stored history without the former 1,000-record window. No upstream status request is made for each list item; use the individual payment endpoint for live reconciliation. Query parameters: page (integer >=1), limit (1..100; default 50), status, method (auto/sbp/crypto), q (order, purpose, customer or payment ID), from/to (YYYY-MM-DD, Moscow calendar dates), min/max (customer amount in RUB), sort (newest/oldest/amount_desc/amount_asc). The signing pathname remains /api/v1/payments; query parameters are excluded from the existing v1 signature contract. API keys can never query another merchant. Repeated requests need a new timestamp/nonce signature as usual. ## GET /api/v1/payments/{payment_id} Signed request, empty body. Returns current payment data. Cross-merchant reads return 404. Payment status is reconciled with the payment channel before it is returned. ## GET /api/v1/balance Signed request, empty body. Returns {available: number, reserved: number}, both in USDT. Balance is based on actual settled and converted payments. A paid payment whose conversion has not finished is not yet available for withdrawal. ## Statuses created: waiting for payment. processing: payment is being processed. paid: confirmed payment. expired: payment window expired; a late payment can still produce paid. canceled: canceled or declined. failed: payment creation or processing failed. refunded: funds returned. chargeback: payment reversed. A previously paid payment can change to refunded or chargeback. Keep the mapping of order_id to payment_id and handle these later events idempotently. ## Merchant webhooks Set the HTTPS destination in the merchant integration settings. Webhooks use X-Timestamp, X-Signature and X-Event-ID. Webhook signing input is timestamp + "." + RAW_REQUEST_BODY (no nonce, method or path). HMAC key is the merchant signing secret. Validate the signature with a constant-time comparison and timestamp tolerance of 300 seconds. Respond 2xx after durably recording the event. Deduplicate X-Event-ID, verify amount/currency against the order, and fulfill exactly once. Body: {event:"payment.paid", payment_id, order_id, amount:"1490.00", charged_amount:"1536.08", currency:"RUB", status:"paid", test:false, metadata:{...}}. The readiness check sends a signed integration.test event with test:true, merchant_id and a message, without payment_id or monetary fields. Acknowledge it with 2xx but NEVER fulfill an order or change a balance for this event. Retries use exponential backoff, maximum eight attempts. In this release, retry jobs are driven by application traffic; the merchant console also offers manual resend. Use authenticated GET reconciliation as a fallback. ## Errors 400 invalid parameters; 401 missing/invalid key, signature or timestamp; 403 suspended or unauthorized account; 404 unknown payment; 409 duplicate order with changed parameters, reused nonce, or unavailable channel configuration; 429 rate limit; 502 payment channel error; 503 temporary service unavailability. On network timeout retry the SAME order_id and payload with a NEW nonce and timestamp. Do not create a different order while the previous result is uncertain. ## Node.js example import { createHmac, randomUUID } from 'node:crypto'; const base = process.env.XAOC_PAY_URL; const key = process.env.XAOC_API_KEY; const secret = process.env.XAOC_SIGNING_SECRET; const path = '/api/v1/payments'; const body = JSON.stringify({ amount: '1490.00', order_id: 'order_1048', description: 'Оплата заказа №1048', customer_id: 'customer_1048', method: 'auto', buyer_share: 100, metadata: { product: 'subscription' } }); const ts = String(Math.floor(Date.now() / 1000)); const nonce = randomUUID(); const message = ts + '.' + nonce + '.POST.' + path + '.' + body; const signature = createHmac('sha256', secret) .update(message).digest('hex'); const response = await fetch(base + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': key, 'X-Timestamp': ts, 'X-Nonce': nonce, 'X-Signature': signature }, body }); const payment = await response.json(); if (!response.ok) throw new Error(payment.error); console.log(payment.checkout_url); ## Python example import os, json, time, uuid, hmac, hashlib import requests base = os.environ['XAOC_PAY_URL'] key = os.environ['XAOC_API_KEY'] secret = os.environ['XAOC_SIGNING_SECRET'] path = '/api/v1/payments' body = json.dumps({ 'amount': '1490.00', 'order_id': 'order_1048', 'description': 'Order #1048', 'customer_id': 'customer_1048', 'method': 'auto', 'buyer_share': 100 }, separators=(',', ':')) ts = str(int(time.time())) nonce = str(uuid.uuid4()) message = f'{ts}.{nonce}.POST.{path}.{body}' signature = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() r = requests.post(base + path, data=body.encode(), headers={ 'Content-Type': 'application/json', 'X-API-Key': key, 'X-Timestamp': ts, 'X-Nonce': nonce, 'X-Signature': signature }, timeout=30) r.raise_for_status() print(r.json()['checkout_url']) ## Verify a webhook (Node.js) import { createHmac, timingSafeEqual } from 'node:crypto'; // rawBody — точные байты запроса, до JSON.parse(). const expected = createHmac('sha256', process.env.XAOC_SIGNING_SECRET) .update(timestamp + '.' + rawBody).digest('hex'); const a = Buffer.from(expected, 'hex'); const b = Buffer.from(signature, 'hex'); if (a.length !== b.length || !timingSafeEqual(a, b)) { throw new Error('Invalid webhook signature'); } if (Math.abs(Date.now()/1000 - Number(timestamp)) > 300) { throw new Error('Expired webhook timestamp'); } const event = JSON.parse(rawBody); // integration.test: подтвердите HTTP 2xx без выдачи товара. // Для оплаты обрабатывайте только payment.paid с test === false. // Сверьте payment_id, order_id, сумму и валюту. // Дедуплицируйте X-Event-ID, затем ответьте HTTP 2xx.