От запроса
до оплаты.
Создавайте платежи, передавайте назначение заказа
и получайте подписанные уведомления.
https://xaocpay.storeДоступ к API выдаётся после подключения проекта. Документация открыта для всех. Ключи создаются в настройках вашего мерчанта и хранятся только на сервере.
Аутентификация
Каждый запрос подписывается секретом мерчанта. Передайте четыре заголовка:
| ЗАГОЛОВОК | ЗНАЧЕНИЕ |
|---|---|
X-API-Key | API-ключ мерчанта |
X-Timestamp | Unix-время в секундах, допуск ±5 минут |
X-Nonce | Новый UUID для каждого запроса |
X-Signature | HMAC-SHA256 в lowercase hex |
Подпишите точные UTF-8 байты строки:
timestamp + '.' + nonce + '.' + METHOD + '.' + pathname + '.' + rawBody
Метод — заглавными буквами. Pathname включает /api/v1/ и не включает query string. Для GET тело пустое: строка заканчивается точкой. Nonce нельзя повторять в течение 10 минут.
Создать платёж
/api/v1/paymentsJSONОтвет содержит checkout_url — вашу ссылку на страницу оплаты. Назначение и идентификатор покупателя будут видны в кабинете и на форме.
| ПОЛЕ | ТИП | ОПИСАНИЕ |
|---|---|---|
amount * | string | Сумма RUB, от 1 до 1 000 000. Например «1490.00» |
order_id * | string | Уникальный номер заказа, до 100 символов |
description | string | Назначение платежа, до 500 символов |
customer_id | string | ID или имя покупателя, до 100 символов |
method | string | auto — общая форма (по умолчанию), sbp или crypto. Также принимается general. |
buyer_share | integer | Доля общей комиссии покупателя: 0–100% |
metadata | object | Ваши данные заказа, до 8 КБ |
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);При повторе запроса сохраняйте тот же order_id и параметры, но создавайте новый nonce. Одинаковый заказ вернёт существующий платёж; изменённые параметры — ошибку 409.
Общая платёжная форма
Не указывайте method или передайте auto. Покупатель увидит доступные способы и отдельную итоговую сумму для каждого. Реквизиты создаются после выбора. В ответе method_options содержит варианты и суммы. Для прямого перехода к СБП или крипте укажите соответствующий метод.
Распределение комиссии
buyer_share: 0 — комиссию оплачивает магазин. 100 — покупатель. Значение 50 делит её поровну. Распределяется общая комиссия: ставка канала плюс наша ставка. Администратор задаёт нашу ставку для пользователя, а комиссия канала определяется по его ответам или настраивается в админке. Расчёт выполняется до копейки. Без известной ставки канала счёт не создаётся. Конвертация и зачисление учитываются по факту.
Пример ответа
До выбора способа charged_amount и platform_fee равны null. Суммы вариантов зависят от тарифов вашего мерчанта.
{
"payment_id": "pay_…",
"order_id": "order_1048",
"status": "created",
"amount": "1490.00",
"charged_amount": null,
"platform_fee": null,
"currency": "RUB",
"method": "auto",
"method_options": [
{
"method": "sbp",
"charged_amount": "1752.95",
"merchant_net": "1490.00"
},
{
"method": "crypto",
"charged_amount": "1619.57",
"merchant_net": "1490.00"
}
],
"checkout_url": "https://xaocpay.store/pay/pay_…"
}Список платежей
/api/v1/paymentsВся история мерчанта с пагинацией: page, limit (до 100), status, method, q, from, to, min, max, sort. Ответ: items, total, page, pages, hasMore. Даты фильтров — по Москве.
Список показывает сохранённые статусы. Для сверки с каналом запросите отдельный платёж. В подпись GET входит путь без query string и пустое тело, как и раньше.
Статус платежа
/api/v1/payments/:payment_idПодписанный запрос без тела. Данные доступны только владельцу соответствующего мерчанта.
createdОжидает оплатыprocessingПлатёж обрабатываетсяpaidОплата подтвержденаexpiredИстекло время оплатыcanceledОтменён или отклонёнfailedОшибка создания или обработкиrefundedСредства возвращеныchargebackПлатёж отозванНе подтверждайте заказ по возврату покупателя на сайт. Дождитесь paid в API или проверенного вебхука. Поздняя оплата может изменить expired на paid; после оплаты возможны возврат и чарджбэк.
Баланс
/api/v1/balance{
"available": 125.50,
"reserved": 20.00
}Все суммы — USDT. Баланс формируется из фактических зачислений после конвертации. Одобрение платежа и появление доступного остатка могут происходить в разное время.
Вебхуки
Укажите публичный HTTPS-адрес в настройках интеграции. События подписываются тем же секретом мерчанта.
HMAC-SHA256(signing_secret, timestamp + '.' + rawBody)
Заголовки: X-Timestamp, X-Signature, X-Event-ID. Сверьте подпись, время, сумму и валюту. Дедуплицируйте ID события, сохраните его и ответьте HTTP 2xx.
Кнопка проверки в кабинете отправляет integration.test с test: true, без платёжного ID и сумм. Ответьте 2xx, но не выдавайте товар и не меняйте баланс по этому событию.
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.Доступно до 8 попыток доставки с увеличивающимся интервалом. В текущей версии обработка очереди запускается при обращениях к платформе. Повтор можно выполнить вручную из кабинета. Для независимой сверки используйте GET статуса.
Ошибки API
400Некорректные параметры401Неверные ключ, подпись или timestamp403Нет доступа или аккаунт приостановлен404Платёж не найден409Конфликт заказа или повтор nonce429Слишком много запросов502 / 503Канал оплаты временно недоступенПри сетевом тайм-ауте повторите тот же заказ с новым nonce. Не создавайте другой order_id, пока результат предыдущей попытки не установлен.