Подключите приём платежей
и выплат к своей системе
REST API LEON для приёма пополнений (Вход), работы с диспутами и получения событий по вебхукам — в реальном времени, тем же курсом и тем же пулом трейдеров, что видит трейдер в своей торговой панели.
Введение
API работает поверх HTTPS и обменивается данными в формате JSON (кроме создания диспута,
где чек загружается как multipart/form-data). Базовый адрес для всех запросов
из этого раздела документации:
{BASE_URL}/api/merchant
{BASE_URL} — домен, на котором развёрнут ваш личный кабинет LEON. Все пути ниже указаны относительно этого адреса.
Заявка «Вход» за три шага
- Мерчант создаёт заявку —
POST /payin/create— и получает реквизит, на который клиент должен перевести средства. - Клиент переводит деньги; трейдер подтверждает получение в своей панели (или заявка отменяется по истечении времени жизни).
- LEON отправляет вебхук
payment.successилиpayment.cancelledна ваш URL — на этом заявка завершена.
curl -X POST {BASE_URL}/api/merchant/payin/create \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount_rub": 5000,
"rate": 95.40,
"payment_methods": ["sbp"],
"external_id": "order-10231"
}'
const res = await fetch("{BASE_URL}/api/merchant/payin/create", {
method: "POST",
headers: {
"Authorization": "Bearer sk_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
amount_rub: 5000,
rate: 95.40,
payment_methods: ["sbp"],
external_id: "order-10231",
}),
});
const payin = await res.json();
import requests
response = requests.post(
"{BASE_URL}/api/merchant/payin/create",
headers={"Authorization": "Bearer sk_live_..."},
json={
"amount_rub": 5000,
"rate": 95.40,
"payment_methods": ["sbp"],
"external_id": "order-10231",
},
)
payin = response.json()
Курс. Перед созданием заявки запросите актуальный курс через GET /rate — он парсится из спотового стакана Rapira и обновляется каждые 30 секунд, тем же способом, что и в торговой панели трейдера.
01Аутентификация
Каждый запрос подписывается API-ключом в заголовке Authorization.
Ключи выпускаются в личном кабинете мерчанта, в разделе «API-ключи» — сгенерируйте
отдельный ключ для каждой интеграции, а не переиспользуйте один и тот же ключ везде.
Authorization: Bearer sk_live_7f2c9a1e4b8d6f0a3c5e7b9d1f3a5c7e
Окружения
Ключ несёт префикс окружения — используйте тестовый ключ на стейджинге, чтобы не создавать реальные заявки.
sk_live_…боевые операции
sk_test_…песочница, без движения реальных средств
Права доступа
При создании ключа вы выбираете, какие права ему выдать. Запрос к эндпоинту без нужного права вернёт 403.
| Право | Даёт доступ к |
|---|---|
read | Чтение заявок, диспутов, баланса, курса и логов вебхуков |
write_payments | Создание и отмена заявок на Вход, открытие и отмена диспутов |
write_payouts | Вывод средств с баланса |
webhooks | Управление URL, событиями, секретом и тестовой отправкой вебхуков |
IP-белый список
При создании ключа можно ограничить его список IP-адресов, с которых он принимается — запрос с адреса вне списка получит 403. Оставьте список пустым, чтобы разрешить любой адрес.
Храните ключ как пароль. Полное значение ключа показывается один раз, в момент создания или перегенерации — LEON хранит только его хеш и не может показать значение повторно. Если ключ скомпрометирован, немедленно отзовите его в кабинете и выпустите новый.
02Ошибки
Все ответы API — включая ошибки — приходят в формате JSON одной и той же формы:
{
"status": "error",
"message": "Недостаточно USDT на балансе"
}
200 OKЗапрос выполнен успешно.
400 Bad RequestНе прошла валидация тела запроса — текст в message объясняет, что именно не так.
401 UnauthorizedКлюч не передан, недействителен или отозван.
403 ForbiddenУ ключа нет нужного права, либо запрос пришёл не из IP-белого списка.
404 Not FoundОбъект с таким ID не найден либо принадлежит другому мерчанту.
415 Unsupported Media TypeТело запроса должно быть application/json (кроме создания диспута — там multipart/form-data).
500 Internal Server ErrorВнутренняя ошибка сервиса. Повторите запрос позже; если повторяется — напишите в поддержку.
03Формат данных
Единые соглашения, которые действуют во всех эндпоинтах и вебхуках ниже.
| Что | Формат |
|---|---|
| Дата и время | YYYY-MM-DD HH:MM:SS, всегда UTC. Без суффикса Z — прибавляйте смещение на своей стороне, если нужен другой часовой пояс. |
| Сумма в рублях | Число с плавающей точкой, напр. 5000 или 4999.5. |
| Сумма в USDT | Число, округлённое до 2 знаков после запятой, напр. 52.41. |
Списки (GET .../list) | Всегда форма { "data": [...], "total", "page", "per_page" }. page — с 1; per_page — от 1 до 100, по умолчанию 20. |
| Идентификаторы | Целые числа, возрастающие, без переиспользования. |
external_id | Произвольная строка на вашей стороне. Возвращается как есть в ответах и вебхуках, и служит ключом идемпотентности при создании заявки — см. раздел ниже. |
Идемпотентность
POST /payin/create идемпотентен по external_id.
Если ваш запрос не долетел до вас (таймаут, обрыв сети, краш до чтения ответа) — просто повторите его
с тем же external_id: вместо второй заявки вы получите тот же самый объект, который был
создан первым запросом, с его актуальным текущим статусом.
- Идемпотентность работает только если вы передали
external_id— без него каждый запрос создаёт новую заявку. - Повтор с той же суммой — возвращается существующая заявка,
200, баланс/лимит трейдера не трогается повторно. - Повтор с другой суммой под тем же
external_id— расценивается как ошибка использования ключа, а не как новая заявка:400.
Используйте собственный ID заказа как external_id и ретраите запрос при любом сетевом сбое или таймауте на вашей стороне — так вы никогда не создадите вторую заявку и не заморозите баланс трейдера дважды за один и тот же заказ.
04Курс USDT/RUB
Возвращает средний курс между лучшим bid и ask в спотовом стакане Rapira (пара USDT/RUB). Значение кешируется на 30 секунд — ровно так же курс обновляется в шапке торговой панели трейдера.
Текущий курс
Используйте перед созданием заявки, чтобы предложить клиенту актуальную сумму в USDT.
curl {BASE_URL}/api/merchant/rate \
-H "Authorization: Bearer sk_live_..."
{ "rate": 95.42 }
05Вход (PayIn)
Заявка на приём средств от клиента. При создании система подбирает активного трейдера, у которого есть реквизит под нужный способ перевода, банк и лимиты — заявка возвращает реквизит, на который клиент должен отправить деньги.
Статусы заявки
pendingОжидает подтверждения трейдером.
successТрейдер подтвердил получение — средства зачислены на баланс мерчанта.
cancelledОтменена мерчантом либо истекло время жизни без подтверждения.
disputeПо отменённой заявке открыт диспут.
Время жизни заявки — 15 минут. Если трейдер не подтвердит получение до истечения expires_at, заявка автоматически отменяется и приходит вебхук payment.cancelled.
Создать заявку
Тело запроса — JSON.
| Параметр | Тип | Описание |
|---|---|---|
amount_rub обязательно | number | Сумма пополнения в рублях. |
rate обязательно | number | Курс USDT/RUB, по которому зафиксировать сумму в USDT (см. GET /rate). |
payment_methods опционально | string[] | Допустимые способы перевода: c2c, sbp, mobile_commerce. Пустой список или отсутствие поля — подходит трейдер с любым способом. |
bank опционально | string | Предпочитаемый банк: any, sberbank, tinkoff, vtb, alfabank, raiffeisen. По умолчанию any. |
external_id опционально | string | Ваш собственный ID заказа — вернётся как есть во всех вебхуках по этой заявке. |
| 400 | «Некорректная сумма» / «Некорректный курс» / «Некорректный способ перевода» — не прошла валидация тела запроса. |
| 400 | «Подходящий трейдер не найден. Попробуйте изменить способ перевода или банк.» — нет активного трейдера с нужным способом, банком и свободным лимитом. Заявка не создаётся. |
| 400 | «external_id уже использован для заявки с другой суммой» — повтор ключа идемпотентности с другими параметрами. |
curl -X POST {BASE_URL}/api/merchant/payin/create \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount_rub": 5000,
"rate": 95.40,
"payment_methods": ["sbp"],
"bank": "any",
"external_id": "order-10231"
}'
{
"id": 764,
"status": "pending",
"trader_name": "Trader #1",
"client_bank": "СБП · +7 916 987-65-43",
"rate": 95.4,
"amount_usdt": 52.41,
"expires_at": "2026-08-03 14:54:31"
}
Список заявок
| Параметр | Тип | Описание |
|---|---|---|
statuses | string | Список статусов через запятую, напр. pending,success. |
date_from / date_to | string | Диапазон дат создания, YYYY-MM-DD. |
amount_from / amount_to | number | Диапазон суммы в рублях. |
page | int | Номер страницы, по умолчанию 1. |
per_page | int | Размер страницы, 1–100, по умолчанию 20. |
curl "{BASE_URL}/api/merchant/payin/list?statuses=pending,success&per_page=20" \
-H "Authorization: Bearer sk_live_..."
{
"data": [
{
"id": 764,
"date": "2026-08-03 14:39:31",
"amount_rub": 5000,
"rate": 95.4,
"amount_usdt": 52.41,
"status": "success",
"client_bank": "СБП · +7 916 987-65-43",
"external_id": "order-10231",
"expires_at": "2026-08-03 14:54:31",
"payment_methods": ["sbp"],
"bank": "any"
}
],
"total": 1,
"page": 1,
"per_page": 20
}
Заявка по ID
Полная карточка заявки, включая историю смены статусов.
curl {BASE_URL}/api/merchant/payin/764 \
-H "Authorization: Bearer sk_live_..."
{
"id": 764,
"amount_rub": 5000,
"rate": 95.4,
"amount_usdt": 52.41,
"status": "success",
"client_bank": "СБП · +7 916 987-65-43",
"external_id": "order-10231",
"trader_id": 1,
"trader_name": "Trader #1",
"created_at": "2026-08-03 14:39:31",
"updated_at": "2026-08-03 14:41:02",
"expires_at": "2026-08-03 14:54:31",
"payment_methods": ["sbp"],
"bank": "any",
"history": [
{ "time": "2026-08-03 14:39:31", "status": "pending" },
{ "time": "2026-08-03 14:41:02", "status": "success" }
]
}
Отменить заявку
Доступно только для заявок в статусе pending. Тела запроса не требует.
| 404 | «Заявка не найдена» — нет заявки с таким ID у вашего аккаунта. |
| 400 | «Отмена недоступна для текущего статуса заявки» — заявка уже success, cancelled или по ней открыт диспут. |
curl -X POST {BASE_URL}/api/merchant/payin/764/cancel \
-H "Authorization: Bearer sk_live_..."
{ "status": "cancelled" }
06Диспуты
Если заявка на Вход была отменена, а клиент утверждает, что перевод всё же ушёл —
откройте диспут и приложите чек. Открыть диспут можно только по заявке в статусе
cancelled, и только один активный диспут на заявку одновременно.
Статусы диспута
open / pending_traderОжидает решения трейдера.
resolved_merchantРешён в пользу мерчанта — заявка переходит в success, средства зачислены.
resolved_pspРешён в пользу трейдера — заявка остаётся cancelled.
cancelled_merchantДиспут отозван мерчантом.
Открыть диспут
Тело запроса — multipart/form-data (нужен для прикладываемого чека).
| Поле | Тип | Описание |
|---|---|---|
transaction_id обязательно | int | ID отменённой заявки на Вход. |
reason обязательно | string | Причина диспута — покажется трейдеру. |
duration_minutes опционально | int | Срок на решение, 15–360 минут. По умолчанию 30. |
file опционально | file | Чек об оплате — PDF, JPG или PNG, до 5 МБ. |
| 400 | «Не указана сделка» / «Укажите причину диспута» — не заполнены обязательные поля. |
| 400 | «Диспут можно открыть только по сделке в статусе «Отменён»» — заявка ещё не отменена. |
| 400 | «По этой сделке уже открыт диспут» — активный диспут по заявке уже существует. |
| 400 | «Недостаточно средств на рабочем и страховом балансе трейдера» — сумму диспута не удалось заморозить у трейдера. |
| 400 | «Недопустимый формат файла» / «Файл превышает 5 МБ» — проблема с приложенным чеком. |
curl -X POST {BASE_URL}/api/merchant/disputes/create \
-H "Authorization: Bearer sk_live_..." \
-F "transaction_id=764" \
-F "reason=Клиент прислал чек об успешном переводе" \
-F "file=@receipt.pdf"
{
"id": 42,
"status": "pending_trader",
"expires_at": "2026-08-03 15:24:31"
}
Список диспутов
Фильтры: statuses, date_from, date_to, search, page, per_page.
{
"data": [
{
"id": 42,
"transaction_id": 764,
"amount_rub": 5000,
"amount_usdt": 52.41,
"status": "resolved_merchant",
"reason": "Клиент прислал чек…",
"created_at": "2026-08-03 14:54:31",
"expires_at": "2026-08-03 15:24:31",
"is_overdue": false
}
],
"total": 1, "page": 1, "per_page": 20
}
Диспут по ID
То же самое плюс has_receipt — есть ли приложенный чек.
{
"id": 42,
"transaction_id": 764,
"amount_rub": 5000,
"amount_usdt": 52.41,
"status": "resolved_merchant",
"reason": "Клиент прислал чек…",
"has_receipt": true,
"created_at": "2026-08-03 14:54:31",
"expires_at": "2026-08-03 15:24:31",
"is_overdue": false
}
Есть ли диспут по заявке
Проверьте перед открытием диспута, чтобы не наткнуться на ошибку «уже открыт» — вернёт dispute_id: null, если активного диспута нет.
curl {BASE_URL}/api/merchant/disputes/for-transaction/764 \
-H "Authorization: Bearer sk_live_..."
{ "dispute_id": 42, "status": "pending_trader" }
Отменить диспут
Доступно, пока диспут активен (open или pending_trader). Заявка остаётся cancelled.
| 404 | «Диспут не найден». |
| 400 | «Отмена доступна только для открытого диспута» — диспут уже решён. |
{ "status": "cancelled_merchant" }
Скачать приложенный чек
Возвращает бинарный файл, который вы сами загрузили при открытии диспута. 404, если чек не прикладывался.
curl {BASE_URL}/api/merchant/disputes/42/receipt \
-H "Authorization: Bearer sk_live_..." \
-o receipt.pdf
07Баланс
Баланс мерчанта хранится в USDT. Пополняется автоматически при успешных заявках на Вход.
Текущий баланс
reserved — сумма, удерживаемая по графику резервирования и пока недоступная к выводу.
{
"balance": 1284.56,
"reserved": 120.00,
"available": 1164.56
}
Вывести средства
| Параметр | Тип | Описание |
|---|---|---|
amount обязательно | number | Сумма в USDT, минимум 10. |
network обязательно | string | trc20 или erc20. |
address обязательно | string | Адрес кошелька в выбранной сети. |
| 400 | «Некорректная сумма» / «Минимальная сумма вывода — 10 USDT». |
| 400 | «Недостаточно средств на балансе» — сумма превышает доступный остаток (available). |
| 400 | «Некорректная сеть» / «Укажите адрес кошелька» / «Некорректный адрес TRC20» / «Некорректный адрес ERC20». |
curl -X POST {BASE_URL}/api/merchant/balance/withdraw \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 250,
"network": "trc20",
"address": "TXy2Ss9K...pQ7m"
}'
{ "id": 88, "status": "pending" }
История движений
Фильтры: type (deposit / withdraw / adjustment), date_from, date_to, page, per_page.
{
"data": [
{
"id": 88,
"type": "withdraw",
"amount": 250,
"status": "success",
"tx_hash": "0x91a…",
"network": "trc20",
"address": "TXy2Ss9K...pQ7m",
"created_at": "2026-08-03 10:02:00",
"updated_at": "2026-08-03 10:14:20"
}
],
"total": 1, "page": 1, "per_page": 20
}
08Вебхуки
LEON отправляет POST-запрос на ваш URL при каждом событии, на которое вы подписаны.
Настройте URL и события в личном кабинете (раздел «Вебхуки») либо через API ниже.
События
| Событие | Когда отправляется |
|---|---|
payment.success | Заявка на Вход подтверждена трейдером или решена в пользу мерчанта по диспуту. |
payment.cancelled | Заявка на Вход отменена мерчантом или истекла по времени. |
dispute.opened | Открыт диспут по отменённой заявке на Вход. |
dispute.resolved | Диспут решён — в пользу мерчанта, трейдера, или отозван. |
Когда диспут решается в пользу мерчанта, помимо dispute.resolved дополнительно придёт payment.success по той же заявке — так что если вы слушаете только платёжные события (без диспутов), вы всё равно узнаете об успехе.
Тело запроса
Каждое событие оборачивается общими полями event и sent_at:
{
"event": "payment.success",
"sent_at": "2026-08-03 14:41:02",
"transaction_id": 764,
"external_id": "order-10231",
"amount_rub": 5000,
"amount_usdt": 52.41,
"status": "success"
}
{
"event": "dispute.resolved",
"sent_at": "2026-08-03 15:10:44",
"dispute_id": 42,
"transaction_id": 764,
"resolution": "resolved_merchant",
"amount_usdt": 52.41
}
Проверка подписи
Каждый запрос подписывается HMAC-SHA256 по вашему секрету вебхука и точному телу запроса,
подпись передаётся в заголовке X-Leon-Signature. Секрет доступен в личном кабинете
(раздел «Вебхуки») либо через GET /webhook-secret — сверяйте подпись перед тем,
как доверять содержимому запроса.
X-Leon-Signature: 3f9a1c...e07b2d
const crypto = require("crypto");
function isValidSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // необработанное тело запроса, без JSON.parse
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader),
);
}
import hmac
import hashlib
def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
Считайте HMAC от сырых байт тела запроса, полученных до какого-либо парсинга — если сначала распарсить JSON и затем сериализовать обратно, порядок ключей или пробелы могут отличаться, и подпись не совпадёт.
Ретраи и таймауты
Ваш эндпоинт должен ответить в течение 5 секунд. Доставка — «по возможности»: если вебхук выключен или не подписан на событие — заявка или диспут всё равно обрабатываются штатно, событие просто не отправляется. Если вебхук включён, но ваш эндпоинт ответил с кодом вне диапазона 200–299 (или не ответил вовремя) — попытка считается неудачной и повторяется автоматически:
| Попытка | Когда |
|---|---|
| 1 | сразу |
| 2 | через 30 секунд после первой |
| 3 | через 2 минуты после второй |
| 4 | через 10 минут после третьей |
Как только любая попытка получает ответ 200–299 — цепочка останавливается. После 4-й неудачной
попытки LEON прекращает попытки по этому событию. Каждая попытка — отдельная строка в
истории доставок, с номером в поле attempt — так вы
отличите повтор одного и того же события от двух разных событий. Если все 4 попытки
провалились (эндпоинт был недоступен дольше ~12 минут) — сверяйте состояние вручную через
GET-эндпоинты по id из последнего известного события.
Настройки вебхука
POST на тот же адрес обновляет настройки: url, events[], status (active / disabled).
{
"url": "https://shop.example.com/leon/webhook",
"events": ["payment.success", "payment.cancelled"],
"status": "active"
}
Логи доставки
Последние попытки отправки — код ответа и время выполнения. Хранятся последние 50 записей.
{
"data": [
{
"id": 1205,
"url": "https://shop.example.com/leon/webhook",
"event": "payment.success",
"attempt": 2,
"response_code": 200,
"response_time_ms": 211,
"created_at": "2026-08-03 14:41:32"
},
{
"id": 1204,
"url": "https://shop.example.com/leon/webhook",
"event": "payment.success",
"attempt": 1,
"response_code": 503,
"response_time_ms": 184,
"created_at": "2026-08-03 14:41:02"
}
],
"total": 2, "page": 1, "per_page": 20
}
Тестовая отправка
Отправляет пробный подписанный запрос на указанный URL, не создавая никаких реальных заявок — удобно проверить обработчик перед подключением.
curl -X POST {BASE_URL}/api/merchant/webhook-test \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://shop.example.com/leon/webhook",
"event": "payment.success"
}'
09Справочник
Сводные таблицы для быстрого поиска — без пересказа деталей из разделов выше.
Жизненный цикл заявок
pending
├─ трейдер подтвердил ───────────▶ success
└─ отменена / истекло время ─────▶ cancelled
└─ открыт диспут ─▶ см. Dispute ниже
open / pending_trader
├─ решён в пользу мерчанта ───▶ resolved_merchant (заявка PayIn → success)
├─ решён в пользу трейдера ───▶ resolved_psp (заявка остаётся cancelled)
└─ отозван мерчантом ─────────▶ cancelled_merchant (заявка остаётся cancelled)
Лимиты
| Параметр | Значение |
|---|---|
| Время жизни заявки (PayIn) | 15 минут от создания до автоматической отмены |
| Срок на решение диспута | 15–360 минут, по умолчанию 30 |
| Чек к диспуту | 1 файл, PDF / JPG / PNG, до 5 МБ |
| Минимальный вывод с баланса | 10 USDT |
Размер страницы (per_page) | 1–100, по умолчанию 20 |
| Хранение логов вебхуков | последние 50 попыток |
| Таймаут ответа на вебхук | 5 секунд |
| Повторные попытки вебхука | до 4 попыток всего: сразу, +30с, +2мин, +10мин |
| Обновление курса | кеш на 30 секунд |
Если все 4 попытки доставки вебхука провалились — LEON прекращает пытаться по этому событию. Ориентируйтесь на лог доставок и на опрос состояния через GET-эндпоинты, если подозреваете пропуск события.
Карта эндпоинтов
| Метод | Путь | Право | Описание |
|---|---|---|---|
| GET | /rate | read | Текущий курс USDT/RUB |
| POST | /payin/create | write_payments | Создать заявку на Вход |
| GET | /payin/list | read | Список заявок на Вход |
| GET | /payin/{id} | read | Заявка на Вход по ID |
| POST | /payin/{id}/cancel | write_payments | Отменить заявку на Вход |
| POST | /disputes/create | write_payments | Открыть диспут |
| GET | /disputes/list | read | Список диспутов |
| GET | /disputes/statistics | read | Счётчики открытых / просроченных / решённых диспутов |
| GET | /disputes/{id} | read | Диспут по ID |
| GET | /disputes/for-transaction/{id} | read | Активный диспут по заявке, если есть |
| POST | /disputes/{id}/cancel | write_payments | Отменить диспут |
| GET | /disputes/{id}/receipt | read | Скачать приложенный чек |
| GET | /balance | read | Баланс, резерв, доступно к выводу |
| POST | /balance/withdraw | write_payouts | Вывести USDT на внешний адрес |
| GET | /balance/history | read | История движений по балансу |
| GET | /balance/history/{id} | read | Операция по балансу по ID |
| GET | /webhook-settings | webhooks | Текущие настройки вебхука |
| POST | /webhook-settings | webhooks | Обновить URL / события / статус |
| GET | /webhook-secret | webhooks | Маскированный секрет подписи |
| POST | /webhook-secret/regenerate | webhooks | Перевыпустить секрет — прежний перестаёт действовать немедленно |
| GET | /webhook-logs | webhooks | Лог последних доставок |
| POST | /webhook-test | webhooks | Отправить тестовое событие |