Подключите приём платежей
и выплат к своей системе
REST API LEON для приёма пополнений (Вход), управления диспутами и получения событий по вебхукам.
Введение
Формат данных — JSON, кроме создания диспута (multipart/form-data — нужен для чека).
Базовый адрес для всех запросов из этого раздела:
{BASE_URL}/api/merchant
{BASE_URL} — домен вашего личного кабинета LEON. Пути ниже указаны относительно этого адреса.
Заявка «Вход» за три шага
POST /payin/create— заявка создана, получен реквизит для перевода.- Перевод подтверждён (или заявка отменена по истечении времени жизни).
- Вебхук
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,
"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,
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,
"payment_methods": ["sbp"],
"external_id": "order-10231",
},
)
payin = response.json()
Курс. Фиксируется сервером при создании заявки — передавать его не нужно. GET /rate отдаёт чистый рыночный курс (без вашей расчётной ставки); фактический курс сделки возвращается в ответе на создание заявки.
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Ошибки
Ответы об ошибках — JSON одной формы:
{
"status": "error",
"message": "Недостаточно USDT на балансе"
}
200 OKЗапрос выполнен успешно.
400 Bad RequestОшибка валидации — детали в message.
401 UnauthorizedКлюч не передан, недействителен или отозван.
403 ForbiddenНет нужного права, либо запрос вне IP-белого списка.
404 Not FoundОбъект не найден или принадлежит другому мерчанту.
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.
04Курс USDT/RUB
Средний курс между лучшим bid и ask в спотовом стакане Rapira (пара USDT/RUB). Кеш 30 секунд.
Текущий курс
Чистый рыночный курс, без вашей расчётной ставки. Курс, применённый к конкретной заявке, смотрите в поле rate её объекта.
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 | Сумма пополнения в рублях. |
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,
"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Вебхуки
POST-запрос на указанный 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-й неудачной попытки доставка прекращается.
Каждая попытка — отдельная строка в истории доставок с номером
в поле attempt. При полном отказе доставки — сверяйте статус через
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 секунд |
Карта эндпоинтов
| Метод | Путь | Право | Описание |
|---|---|---|---|
| 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 | Отправить тестовое событие |