Lave Pay · API для мерчантов
Принимайте оплату с балансов Laveshka и по сети, платите с баланса мерчанта — людям, на карты, по QR, в блокчейн — и узнавайте об исходе подписанным вебхуком. Один ключ, один базовый URL, JSON.
Начало
Обзор
Мерчант создаётся в мини-приложении Laveshka (Сервисы → Для бизнеса → Lave Pay). Там же выпускается API-ключ, задаётся вебхук и виден журнал всех доставок. Дальше всё делает ваш сервер через REST API. Базовый сценарий — четыре шага:
- Сервер выставляет счёт —
POST /invoices— и получаетpay_url. - Вы отдаёте ссылку покупателю: кнопкой в боте, ссылкой на сайте, QR-кодом.
- Покупатель открывает её — мини-приложение показывает экран оплаты — и платит одним нажатием.
- Сумма за вычетом комиссии ложится на баланс мерчанта, ваш сервер получает вебхук
invoice.paid.
Баланс мерчанта — отдельный счёт. На него приходят оплаты, депозиты клиентов и купленные USDT; с него уходят возвраты и всё, что ваш сервер тратит по API. Владелец переводит его на свой основной баланс кнопкой в консоли — до основного баланса API-ключ дотянуться не может.
Карточка API
| Базовый URL | https://<домен>/merchant/v1 |
| Авторизация | X-Api-Key: LAVE-<merchant_id>-<secret> или Authorization: Bearer … |
| Формат | JSON; суммы — строки. amount, fee, net — оценка в USD с двумя знаками; поля активов — в единицах своего актива |
| Конверт ответа | {"ok": true, "data": …} / {"ok": false, "error": {"code", "message", "details"}} |
| Лимит запросов | 300 в минуту на ключ — остальные лимиты |
| Идентификаторы | с префиксом объекта: inv_, addr_, dep_, tr_, chq_, qr_, cp_, sbp_, st_, tg_, wd_, buy_, evt_; qrq_ — цена QR из POST /qr/check, op_ — строка истории баланса |
| Время | ISO 8601 с зоной: 2026-08-22T09:10:00+00:00 |
| Подпись вебхуков | HMAC-SHA256(secret, "<timestamp>.<raw body>") — проверка |
| Библиотеки | Python — lavepay |
Быстрый старт
- Создайте мерчанта в приложении. API-ключ показывается один раз сразу после создания: на сервере хранится только хеш, потерянный ключ не восстанавливается, а выпускается новый (Настройки → Выпустить новый ключ; старый перестаёт работать мгновенно).
- Выставьте счёт и получите
pay_url:
# ключ вида LAVE-12-k8Vn3pQ… — 12 здесь merchant_id export LAVE_KEY="LAVE-12-k8Vn3pQxxxxxxxxxxxxxxxxxxxx" curl -X POST "https://<домен>/merchant/v1/invoices" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"amount": "12.34", "description": "Заказ №1042", "order_id": "ord-1042"}'
import asyncio from lavepay import LavePay async def main(): async with LavePay("LAVE-12-k8Vn3pQxxxxxxxxxxxxxxxxxxxx") as lp: invoice = await lp.invoices.create( "12.34", order_id="ord-1042", description="Заказ №1042", ) print(invoice.pay_url) asyncio.run(main())
-
Отдайте
pay_urlпокупателю. Это ссылка видаhttps://t.me/laveshka?startapp=lpay_…: она открывает мини-приложение сразу на экране оплаты. -
Узнайте об оплате. Быстрый путь — вебхук
invoice.paid(URL задаётся в приложении, маршрут проверяетсяPOST /webhook/test). Запасной — опросGET /invoices/{id}. Товар выдавайте поstatus: paid, сверивorder_idи сумму.
Ключ можно проверить прямо здесь: нажмите «Попробовать» у
GET /me и введите его в окне — запрос уйдёт из вашего
браузера в API этого же домена. Так же пробуется любой эндпоинт на этой странице.
Библиотеки
Запросы можно не писать руками. Пока библиотека одна, для Python; на других языках работайте с REST API напрямую — в примерах ниже рядом с curl есть вкладка Python.
pip install lavepay pip install "lavepay[fastapi]" # приём вебхука в FastAPI pip install "lavepay[aiogram]" # кнопка оплаты в боте pip install "lavepay[aiohttp]" # вебхук на aiohttp pip install "lavepay[qr]" # PNG с QR-кодом ссылки
В библиотеке — счета и история, переводы и чеки, оплата QR и вывод на карту, Steam,
Stars и Premium, покупка USDT, вебхуки: проверка подписи, дедупликация по
id события и опрос статусов, если вебхук поднимать негде
(lp.start_polling()). Суммы передавайте строкой или Decimal:
float библиотека не примет. Исключения повторяют error.code
ответа: InsufficientFundsError, RateChangedError,
QuoteExpiredError и остальные. Адреса клиентов и вывод в блокчейн
библиотека пока не покрывает — для них REST напрямую.
Основы
Авторизация
Каждый запрос несёт ключ в заголовке — любым из двух способов:
GET /merchant/v1/me X-Api-Key: LAVE-12-k8Vn3pQxxxxxxxxxxxxxxxxxxxx # или Authorization: Bearer LAVE-12-k8Vn3pQxxxxxxxxxxxxxxxxxxxx
Ключ вида LAVE-<merchant_id>-<secret> выпускается в приложении
и показывается один раз. У каждого мерчанта свой ключ, у одного аккаунта — до десяти
мерчантов; ключи и их деньги не пересекаются. Утёкший ключ меняют кнопкой
Выпустить новый ключ — старый гаснет мгновенно.
/me
Кто я и что лежит на балансе мерчанта. Заодно — проверка, что ключ работает.
{
"ok": true,
"data": {
"id": 12,
"name": "Pixel Shop",
"description": "Стикеры и наборы",
"project_url": "https://pixel.shop",
"avatar_url": "https://…/avatar.jpg?v=3",
"status": "active",
"fee_bps": 0,
"payouts_enabled": false,
"webhook": { "state": "healthy", "url": "https://pixel.shop/lavepay", … },
"balance": "1532.20",
"currency": "USD",
"balances": {
"USD": "1532.20",
"GRAM": "0.2",
"TRX": "0",
"SOL": "0",
"BNB": "0.025",
"ETH": "0"
},
"created_at": "2026-08-22T09:00:00+00:00"
}
}
| Поле | Тип | Что это |
|---|---|---|
status | строка | active или paused — на паузе мерчант не выставляет счета и не тратит |
fee_bps | число | ваша ставка с оплаты балансом Laveshka в базисных пунктах (0 = 0 %) — см. комиссии |
payouts_enabled | bool | включён ли переключатель Выплаты по API — без него все траты с баланса отвечают 403 |
webhook | объект | состояние endpoint'а, как в GET /webhook |
balances | объект | балансы мерчанта по активам. USDT лежит под ключом USD; оплата балансом Laveshka или по сети остаётся в уплаченной монете, CryptoBot и xRocket рассчитываются в USDT и попадают на USD |
balance, currency | строка | USD-баланс и его код — для старых интеграций; то же, что balances.USD |
Ответы и ошибки
Все ответы завёрнуты в единый конверт. Успех — {"ok": true, "data": …};
ошибка — {"ok": false, "error": {"code", "message", "details"}}, где
error.code — стабильный код, на который можно завязать логику, а
message — текст для людей. В примерах ниже конверт для краткости опущен.
{
"ok": false,
"data": null,
"error": {
"code": "rate_limited",
"message": "Rate limit is 300 requests per minute",
"details": { "retry_after": 12 }
}
}
| HTTP | error.code | Когда |
|---|---|---|
| 401 | unauthorized | ключа нет, он неверный или мерчант удалён; мерчант или аккаунт отключён или ограничен — обратитесь в поддержку |
| 403 | forbidden | мерчант на паузе; выплаты по API выключены; клиент адреса не прошёл AML (details.reason: aml) |
| 403 | buy_blocked | покупка USDT временно закрыта: слишком много неоплаченных и снятых покупок; до details.until |
| 404 | not_found | объект, событие или получатель не найден |
| 409 | duplicate_operation | order_id уже занят счётом с другой суммой или другим методом |
| 409 | insufficient_funds | на балансе мерчанта не хватает: на возврат, выплату, цену QR на проверке (POST /qr/check), вывод в блокчейн — «Недостаточно USDT на балансе мерчанта» (или GRAM, TRX, SOL, BNB, ETH); ничего не списано |
| 409 | limit_exceeded | потолок: 1000 открытых счетов, 100 000 адресов клиентов, 10 покупок USDT в работе |
| 409 | quote_expired | цена QR истекла, а QR стал дороже — новая цена в details.quote |
| 409 | rate_changed | списание вышло дороже max_debit (карта, блокчейн, Steam, Stars/Premium) или курс покупки USDT выше max_rate; ничего не списано |
| 409 | payout_unavailable | оплата QR, вывод на карту или вывод по этой сети временно недоступны — попробуйте позже; ничего не списано |
| 409 | idempotency_conflict | тот же idempotency_key уже стоит за другой операцией |
| 422 | validation_error | плохое тело запроса; details.errors — что именно; у сумм — details.minimum/maximum |
| 422 | steam_account_rejected | такого логина Steam нет или его нельзя пополнить; ничего не списано |
| 429 | rate_limited | превышен лимит; details.retry_after — через сколько секунд можно |
| 502 | provider_error | внешняя сторона не ответила до списания (Fragment у Stars/Premium, реквизиты бесшовного счёта); ничего не списано |
| 503 | steam_unavailable / steam_paused | пополнение Steam сейчас недоступно или на паузе; ничего не списано |
| 503 | buy_unavailable | покупка USDT за рубли выключена или временно недоступна; ничего не создано |
| 503 | provider_not_configured | направление временно выключено на сервере: Lave Pay целиком, оплата по сети или монете, QR, карты, блокчейн, Stars/Premium |
failed.
У всех трат с баланса мерчанта одно правило: ошибка 4xx/5xx
значит, что ничего не списано и операции нет. Если же деньги уже удержаны, а
банк, сеть или поставщик отказали, ответ — 200 с объектом
failed, причина в error, деньги уже вернулись на баланс
мерчанта, и приходит вебхук *.failed. Логику завязывайте на
status, текст error — для людей.
Идемпотентность
Сетевая ошибка на записи — таймаут, обрыв, 5xx — не означает, что
операции не было. Повторяйте тот же запрос с тем же ключом, а не
новый: повтор вернёт уже сделанное и второй раз ничего не спишет. Новый ключ после
неясного исхода может отправить деньги дважды.
| Что | Ключ | Повтор с тем же ключом | Тот же ключ, другие данные |
|---|---|---|---|
| Счёт | order_id (необязателен, но лучше задавать) | тот же счёт — если сумма и метод те же | 409 duplicate_operation |
| Перевод, чек | idempotency_key обяз. | та же операция; чек читается по ключу: GET /cheques/{ключ} | — |
| Оплата QR, вывод на карту, Steam, Stars/Premium, вывод в блокчейн, покупка USDT | idempotency_key обяз. | тот же объект с replayed: true; объект читается по ключу: GET …/{ключ}. Повтор узнаётся раньше всех проверок — даже если выплаты с тех пор выключили | 409 idempotency_conflict |
| Вебхук | id события (evt_…) | повтор несёт тот же id и то же тело — дедуплицируйте по нему | — |
Ключ — до 64 символов, уникален в рамках мерчанта. Собирайте его из своего заказа
(payout-4665), а не случайно: тогда повтор после сбоя совпадёт сам.
Отказ до списания (422, 409, 403, 429,
503) ключ не расходует. Два мерчанта одного владельца с одинаковыми
ключами не пересекаются.
Списки и пагинация
Все списки устроены одинаково: курсорная пагинация, новые первыми.
limit — до 200, по умолчанию 50; before — next_cursor
из предыдущей страницы; на последней странице его нет (null).
Фильтр status принимает одно значение.
# оплаченные счета по 50, вторая страница — before из next_cursor первой curl "https://<домен>/merchant/v1/invoices?status=paid&limit=50&before=4192" \ -H "X-Api-Key: $LAVE_KEY"
{ "ok": true, "data": { "items": [ … ], "next_cursor": 4192 } }
| Список | Фильтры | status |
|---|---|---|
GET /invoices | status, order_id | active paid expired cancelled refunded |
GET /addresses | client_id, network | — |
GET /deposits | address_id, client_id, status | pending processing completed failed |
GET /cheques | status | active completed cancelled |
GET /qr/payments, /card/payouts, /sbp/payouts, /steam/orders, /withdrawals | status | pending processing completed failed |
GET /telegram/orders | status, service | те же; service — stars или premium |
GET /buy/orders | status | pending awaiting_payment overdue completed cancelled |
GET /webhook/deliveries | status | pending delivered failed |
GET /history | — | — (история баланса) |
Лимиты
Каждое действие, которое заставляет сервер Laveshka отправить запрос наружу,
метится счётчиком — Lave Pay нельзя превратить в источник трафика на чужой адрес.
Превышение — 429 rate_limited с details.retry_after.
| Что | Лимит |
|---|---|
| Запросы к API | 300 / мин на ключ |
| Отклонённые ключи | ограничены с одного адреса; валидный ключ с того же адреса продолжает работать |
Новые адреса клиентов (POST /addresses) | 60 / мин на мерчанта; повтор существующего не в счёт |
Проверки QR (POST /qr/check и перепроверка истёкшей цены) | 20 / мин на мерчанта |
Выводы на карту (POST /card/payouts) | 60 / мин на мерчанта |
Выводы по СБП (POST /sbp/payouts) | 60 / мин на мерчанта |
Заказы Steam (POST /steam/orders) | котировки и заказы ограничены на мерчанта |
Покупки Stars и Premium (POST /telegram/orders) | 30 / мин на мерчанта |
Выводы в блокчейн (POST /withdrawals) | 30 / мин на мерчанта; повтор с тем же ключом не в счёт |
Сети и котировка вывода (GET /withdrawals/networks, /withdrawals/quote) | 60 / мин на мерчанта на двоих |
Покупки USDT за рубли (POST /buy/orders) | 20 / мин на мерчанта |
Курс (GET /qr/rate, /card/rate, /sbp/rate, /sbp/banks, /steam/rate, /telegram/rate, /buy/rate) | 60 / мин на мерчанта по каждому направлению |
POST /webhook/test | 6 / мин и 60 / час; сверх — событие не создаётся |
| Повтор события вручную | 30 / мин на мерчанта |
| Смена URL вебхука | 30 / час на мерчанта |
| Создание мерчантов | 20 / день, 10 живых на аккаунт |
Потолки объёма — не про частоту, а про количество: 1000 открытых счетов,
100 000 адресов клиентов, 10 покупок USDT в работе на мерчанта; сверх —
409 limit_exceeded.
Комиссии и суммы
Комиссия с оплаты счёта зависит от того, чем заплатил покупатель; способ
приходит в paid_via счёта, округление вверх до цента. Персональная
ставка мерчанта (fee_bps в GET /me), если назначена,
заменяет ставки баланса и перевода по сети; у CryptoBot и xRocket ставка всегда своя.
| Способ оплаты | paid_via | Комиссия |
|---|---|---|
| Баланс Laveshka | balance | 0 % — ваша ставка в fee_bps |
| USDT по сети (TRC-20, BEP-20, ERC-20, GRAM, Solana) | chain | 0.1 % |
| GRAM, TRX, SOL, BNB и ETH по своей сети | chain | 0.05 % |
| CryptoBot (мультивалютно) | cryptobot | 3.1 %: 3 % комиссия CryptoBot + 0.1 % service |
| xRocket (мультивалютно) | xrocket | 1.5 % — комиссия xRocket, без service |
Депозит на адрес клиента и недоплата бесшовного счёта берут ту же ставку перевода по сети.
| Операция | Суммы и сроки | Комиссия |
|---|---|---|
| Счёт | 0.10 … 10 000 USD; срок жизни 1 час по умолчанию, 60 с … 7 дней (ttl) | по способу оплаты, см. выше |
| Перевод пользователю | от 0.10 $ | нет |
| Чек | 0.10 … 100 000 $ за активацию, до 100 000 активаций, не больше 1 000 000 $ на чек | нет |
| Оплата QR | 1 … 400 000 ₽ за оплату, в USDT с баланса мерчанта | внутри курса «₽ за 1 USDT» |
| Вывод на карту | 100 … 300 000 ₽ за выплату, в USDT с баланса мерчанта | внутри курса; персональная ставка мерчанта, если назначена, уже в GET /card/rate |
| Вывод по СБП | 500 … 100 000 ₽ за перевод, в USDT с баланса мерчанта | внутри курса (GET /sbp/rate) |
| Steam | 1 … 1000 $ за заказ, точная цена — в момент заказа | внутри цены |
| Stars и Premium | по курсу Laveshka (GET /telegram/rate) | внутри цены |
| Вывод в блокчейн | от min_amount сети до 10 000 $ за вывод, в монете вывода | сверху суммы, своя у каждой сети (GET /withdrawals/networks) |
| Покупка USDT за рубли | от 500 ₽, потолок одной покупки у каждого мерчанта свой (max_rub); до 10 покупок в работе | внутри курса (GET /buy/rate) |
Приём оплаты
Счета
Счёт — просьба заплатить. Обычный счёт отдаёт pay_url: покупатель
открывает мини-приложение и платит балансом Laveshka, переводом по сети или через
CryptoBot и xRocket — чем захочет. Бесшовный счёт вместо ссылки
сразу отдаёт реквизиты перевода по выбранной сети — для своего экрана оплаты.
/invoices
Выставить счёт.
curl -X POST "https://<домен>/merchant/v1/invoices" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": "12.34", "description": "Набор стикеров «Лето»", "order_id": "ord-1042", "payload": "{\"cart\":7,\"user\":99}", "return_url": "https://pixel.shop/thanks?o=1042", "ttl": 1800 }'
invoice = await lp.invoices.create( "12.34", description="Набор стикеров «Лето»", order_id="ord-1042", payload='{"cart":7,"user":99}', return_url="https://pixel.shop/thanks?o=1042", ttl=1800, ) print(invoice.pay_url)
| Поле | Тип | Что это |
|---|---|---|
amount обяз. | строка | в currency: USD и USDT — до двух знаков после точки, монета — до своей точности. 0.10 … 10 000 USD |
currency | USD | в чём записана сумма: USD, USDT, GRAM, TRX, SOL, BNB или ETH. Счёт в монете стоит своих монет, а не долларов |
accept | ["USDT"] | чем можно заплатить: USDT, GRAM, TRX, SOL, BNB, ETH. Зачисление — в монете оплаты. Переводом по сети USDT принимается по TRC-20, BEP-20, ERC-20 (Ethereum), GRAM и Solana; GRAM, TRX, SOL, BNB и ETH — каждая по своей сети |
description | до 256 | видит покупатель на экране оплаты |
order_id | до 128 | ваш номер заказа; уникален в рамках мерчанта и работает как ключ идемпотентности |
payload | до 1024 | непрозрачная строка, возвращается как есть в ответах и вебхуках |
return_url | http(s), до 512 | кнопка «Вернуться» после оплаты; пусто — берётся ссылка проекта |
ttl | 60 … 604800 | секунд до истечения; по умолчанию час |
method | {asset, network} | бесшовный счёт: вместо ссылки сразу реквизиты перевода — ниже |
Объект счёта
Один и тот же объект приходит в ответ на создание, в GET и в data вебхуков.
{
"id": "inv_87c8af4c-9200-45af-9bf0-d322e4ac45b5",
"merchant_id": 12,
"status": "active",
"amount": "12.34",
"fee": "0.00",
"net": "12.34",
"currency": "USD",
"face_amount": "12.34",
"accept": ["USDT"],
"paid_via": null,
"paid_asset": null,
"paid_amount": null,
"payer": null,
"description": "Набор стикеров «Лето»",
"order_id": "ord-1042",
"payload": "{\"cart\":7,\"user\":99}",
"return_url": "https://pixel.shop/thanks?o=1042",
"pay_url": "https://t.me/laveshka?startapp=lpay_Xyug2fo4Mwuj54Wd",
"method": null,
"payment": null,
"created_at": "2026-08-22T09:10:00+00:00",
"expires_at": "2026-08-22T09:40:00+00:00",
"paid_at": null,
"refunded_at": null
}
| Поле | Тип | Что это |
|---|---|---|
status | строка | active → paid → refunded; из active также expired (вышел ttl, а перевод по счёту не найден) и cancelled (ваш вызов) |
amount, fee, net | USD | оценка в долларах. При создании считаются по ставке оплаты балансом — это и есть цифры, если покупатель платит балансом. При оплате по сети или через кошелёк окончательные fee и net считаются по ставке этого способа в момент оплаты и приходят в invoice.paid |
currency, face_amount | строка | в чём назван счёт и сама названная сумма: у счёта в монете face_amount — монеты, amount — их оценка в USD |
accept | список | чем разрешено платить |
paid_via | строка | чем заплатили: balance, chain, cryptobot или xrocket; null, пока не оплачен. По нему берётся ставка комиссии |
paid_asset, paid_amount | строка | в какой монете и сколько пришло на баланс мерчанта; это же вернётся покупателю при возврате |
payer | объект | после оплаты — покупатель: {"telegram_id": 123456789, "username": "durov"} (username может быть null); у перевода по сети — null, плательщик анонимен |
pay_url | строка | ссылка на экран оплаты; null у бесшовного счёта |
method, payment | объект | только у бесшовного счёта: выбранный метод и реквизиты перевода |
expires_at, paid_at, refunded_at | время | когда истекает, когда оплачен, когда возвращён |
Получить счёт и список
/invoices/{id}
Один счёт по inv_…. Подходит для опроса статуса, если вебхук не настроен,
и как запасной канал рядом с ним.
curl "https://<домен>/merchant/v1/invoices/inv_87c8af4c-9200-45af-9bf0-d322e4ac45b5" \ -H "X-Api-Key: $LAVE_KEY"
invoice = await lp.invoices.get("inv_87c8af4c-9200-45af-9bf0-d322e4ac45b5") # или подождать оплату опросом, если вебхука нет invoice = await lp.invoices.wait(invoice.id, timeout=600) if invoice.matches(amount="12.34", order_id="ord-1042"): ...
/invoices
Список: status (active|paid|expired|cancelled|refunded),
order_id, limit, before — как во всех
списках.
Отмена и возврат
/invoices/{id}/cancel
Отменить счёт. Только для active. Покупатель, открывший старую ссылку,
увидит «счёт больше не действует». Отправляется invoice.cancelled.
/invoices/{id}/refund
Полный возврат. Только для paid. Покупателю возвращаются
paid_amount в paid_asset: при оплате GRAM с баланса Laveshka
это GRAM, у CryptoBot и xRocket — USDT. Комиссия Laveshka не возвращается. Если
соответствующего баланса мерчанта не хватает, недостающее списывается с такого же
актива на основном балансе владельца; не хватило и там — 409 insufficient_funds.
Отправляется invoice.refunded.
Счёт, оплаченный прямым переводом по сети, автоматически не возвращается:
плательщик такого перевода анонимен, метод ответит 422 validation_error.
curl -X POST "https://<домен>/merchant/v1/invoices/inv_87c8af4c-…/cancel" \ -H "X-Api-Key: $LAVE_KEY" curl -X POST "https://<домен>/merchant/v1/invoices/inv_87c8af4c-…/refund" \ -H "X-Api-Key: $LAVE_KEY"
invoice = await lp.invoices.cancel("inv_87c8af4c-…") invoice = await lp.invoices.refund("inv_87c8af4c-…")
Бесшовный счёт: реквизиты сразу
Обычный счёт отдаёт pay_url, и монету с сетью покупатель выбирает в
мини-приложении. Бесшовный сразу отдаёт реквизиты перевода по выбранному методу —
их показывают у себя. method.asset — USDT, GRAM,
TRX, SOL, BNB или ETH; method.network —
TRX, BSC, ETH, GRAM или SOL
(читаются и TRC20/TRON, BEP20, ERC20/ERC-20/ETHEREUM,
TON). USDT ходит по всем пяти сетям, GRAM — только по GRAM, TRX — по TRON,
SOL — по Solana, BNB — по BSC, ETH — по Ethereum. В сети Ethereum
{"asset": "USDT", "network": "ETH"} — это USDT ERC-20,
{"asset": "ETH", "network": "ETH"} — сама монета ETH.
{
"amount": "18",
"currency": "USDT",
"order_id": "ord-7",
"method": { "asset": "USDT", "network": "BSC" }
}
"pay_url": null, "method": { "asset": "USDT", "network": "BSC" }, "payment": { "address": "0x8c1d…", "amount": "18.003", "amount_units": "18003000", "asset": "USDT", "network": "BSC", "status": "waiting", "confirmations": 0, "confirmations_required": 0, "received": null, "tx_hash": null, "expires_at": "2026-10-06T13:10:00+00:00", "paid_at": null }
payment.amount до последнего знака. Оплата опознаётся
по точной сумме: к сумме счёта могут добавляться последние знаки — тысячные у USDT,
GRAM и TRX, десятитысячные у SOL, стотысячные у BNB, миллионные у ETH. Перевод ровно этой суммы до
expires_at (и ещё пять минут на последний блок) и есть оплата. Адрес
одноразовый: после оплаты, отмены или истечения переводы на него автоматически не
засчитываются — по такому переводу обратитесь в поддержку. Для повторяющихся
пополнений есть адреса клиентов.
payment.status:waiting→confirming(перевод найден, идут подтверждения) →paid; вместе со счётом —expiredиcancelled. Об оплате приходит обычныйinvoice.paidсpayment.tx_hash.- Найденный перевод счёт не теряет. Если перевод пришёл до
expires_at, счёт не истекает по часам: он остаётсяactiveсpayment.status: confirming, пока перевод обрабатывается, и закрываетсяinvoice.paid. Обычно это минуты, но бывает и дольше — не отменяйте заказ поexpires_at, пока статусconfirming, ждите вебхука. Дольше всего — в сети Ethereum: USDT обычно 3–25 минут, монета ETH — 15–25 минут. Перевод, ушедший доexpires_at, оплачивает счёт, даже если зачислен позже. - Недоплата — перевод меньше суммы — счёт не оплачивает. Пришедшее за вычетом
платы за обработку ложится на баланс мерчанта, приходит
invoice.underpaidсunderpayment(received,credited,cost— плата за обработку,asset,network,tx_hash), счёт остаётся открытым с теми же реквизитами. Переплата счёт оплачивает, излишек остаётся у Laveshka. - Единицы.
amount_units— та же сумма в наименьших единицах учёта: шесть знаков у USDT и TRX, девять у GRAM, SOL, BNB и ETH. У USDT в Ethereumamount_units— ровно единицы контракта (6 знаков):"18003000"и есть сумма перевода. У USDT в BSC контракт считает 18 знаков — если ваш код сам собирает перевод, этоamount_units× 10¹². У BNB это гвей (10⁻⁹ BNB), а не wei сети:"10000000"— это 0.01 BNB; перевод в wei —amount_units× 10⁹. У ETH так же — гвей (10⁻⁹ ETH):"2000000"— это 0.002 ETH. - Монета метода. Переводить нужно именно её: BNB на реквизиты счёта в USDT
(и USDT — на реквизиты в BNB) счёт не оплачивает.
BNBвasset— монета,BNBвnetwork— другое имя сети BSC, так что{"asset": "USDT", "network": "BNB"}— это USDT BEP-20; нативная монета пишется{"asset": "BNB", "network": "BSC"}.ETHвnetwork— сеть Ethereum,ETHвasset— монета ETH: ETH на реквизиты счёта в USDT ERC-20 (и USDT — на реквизиты в ETH) счёт не оплачивает. - Сумма в GRAM, TRX, SOL, BNB или ETH считается по курсу в момент создания и держится весь срок счёта; зачисление — в той же монете. Комиссия — как у перевода по сети: USDT 0.1 %, GRAM, TRX, SOL, BNB и ETH 0.05 %.
- Минимальный перевод. У монеты в каждой сети есть наименьший перевод, который
сеть зачтёт. Счёт, сумма которого в монете метода меньше него, не выдаётся:
422 validation_errorсdetails.field: amountиdetails.minimum— минимумом в монете метода. Мелкий счёт выставляйте в USDT по сети, где он проходит, или обычным счётом со ссылкой — там его можно оплатить и с баланса Laveshka. - Тот же
order_idс тем же методом возвращает тот же счёт и те же реквизиты; с другим методом —409 duplicate_operation. - С баланса Laveshka и через кошельки такой счёт не оплачивается; возврат по API недоступен — плательщик перевода анонимен.
- Оплата по этой сети или этой монетой временно недоступна —
503 provider_not_configured; реквизиты не выданы —502 provider_error. В обоих случаях счёт не создаётся.
Экран оплаты
pay_url открывает мини-приложение на экране оплаты: аватар и название
мерчанта, описание, сумма, срок действия. Покупатель выбирает, чем платить —
балансом Laveshka, переводом по сети, через CryptoBot или xRocket, — и платит одним
нажатием (под PIN, если он включён). После оплаты — кнопка
«Вернуться в <мерчант>»: ведёт в return_url счёта или в ссылку
проекта.
Если счёт истёк, отменён, уже оплачен или принадлежит мерчанту самого пользователя — кнопка оплаты недоступна, причина написана на экране.
Адреса клиентов
Постоянный адрес для вашего клиента — по одному на клиента в каждой сети, навсегда.
Клиент пополняет его когда угодно, каждый перевод — депозит на баланс мерчанта с
вебхуком. База клиентов у каждого мерчанта своя: клиент 1 у вас и
клиент 1 у другого мерчанта получают разные адреса.
/addresses
Выдать адрес. Первый вызов выдаёт адрес (created: true), каждый следующий
для того же клиента и сети отдаёт его же (created: false).
{ "client_id": "1", "network": "BSC", "asset": "USDT" }
{
"id": "addr_17",
"client_id": "1",
"network": "BSC",
"address": "0x8c1d…",
"assets": ["USDT", "BNB"],
"min_deposit": { "USDT": "1", "BNB": "0.001666667" },
"deposits_count": 0,
"last_deposit_at": null,
"created_at": "2026-10-06T10:00:00+00:00",
"created": true
}
| Поле | Тип | Что это |
|---|---|---|
client_id обяз. | до 64 | ваш id клиента |
network обяз. | строка | TRX, BSC, ETH, GRAM или SOL (читаются и TRC20/TRON, BEP20/BNB, ERC20/ERC-20/ETHEREUM, TON) |
asset | USDT | какую монету ждёте (USDT, GRAM, TRX, SOL, BNB, ETH) — только проверяется, что сеть её принимает |
Один адрес принимает все монеты своей сети, они перечислены в assets: на
TRON — USDT и TRX, на GRAM — USDT и GRAM, на Solana — USDT и SOL, на BSC — USDT и BNB,
на Ethereum — USDT и ETH.
Запрос с "asset": "BNB" отдаёт тот же адрес, что и с "asset": "USDT".
Проверяйте asset депозита: TRX на TRON-адрес зачисляется как TRX,
BNB на BSC-адрес — как BNB, ETH на адрес сети ETH — как ETH. Первый перевод USDT на свежий TRON-адрес стоит отправителю
дороже — слот USDT у адреса ещё пуст; на новый адрес Solana первый USDT стоит плательщику
~0.002 SOL — кошелёк создаёт счёт токена.
Адрес клиента в Ethereum и BSC один. Запрос с "network": "ETH" отдаёт
клиенту тот же 0x…, что и с "network": "BSC", но это два адреса
API — у каждой сети свой addr_… и свои депозиты. В какой сети пришёл
перевод, говорит network депозита: USDT BEP-20 придёт с BSC,
USDT ERC-20 и монета ETH — с ETH. Сеть следит за адресом только с тех пор, как он выдан в
ней: прежде чем называть клиенту адрес для Ethereum, получите его запросом с
"network": "ETH".
Ошибки: 422 — сеть или монета; 403 forbidden — мерчант на паузе
или клиент не прошёл AML (details.reason: aml); 409 limit_exceeded —
потолок адресов; 429 — слишком много новых адресов в минуту;
503 — выдача выключена или сеть недоступна. Мерчанта с адресами клиентов
удалить нельзя — деньги на них могут прийти в любой день; его ставят на паузу: новые
адреса не выдаются, выданные зачисляют как прежде.
Адреса и депозиты
/addresses
Список адресов: client_id, network, limit, before.
/addresses/{id}
Один адрес по addr_….
/deposits
Депозиты: address_id, client_id, status, limit, before.
/deposits/{id}
Один депозит по dep_….
Объект депозита
{
"id": "dep_901",
"status": "completed",
"address_id": "addr_17",
"client_id": "1",
"address": "0x8c1d…",
"network": "BSC",
"asset": "USDT",
"amount": "25.5",
"fee": "0.03",
"net": "25.47",
"tx_hash": "0x5b…",
"from_address": "0x9f…",
"confirmations": 0,
"confirmations_required": 0,
"error": null,
"created_at": "2026-10-06T10:05:00+00:00",
"completed_at": "2026-10-06T10:05:31+00:00"
}
Статусы: pending (увидели, ждём сеть) →
processing (подтверждён, обрабатывается; иногда дольше
обычного) → completed (на балансе мерчанта) или
failed (не зачислим; причина — в error).
Вебхуки: deposit.pending при первом обнаружении, deposit.completed,
deposit.failed. Зачислять своему клиенту — только по
deposit.completed.
Зачисление — в монете депозита, комиссия как у перевода по сети: USDT 0.1 % (до
цента вверх), GRAM, TRX, SOL, BNB и ETH 0.05 %. Депозит ниже min_deposit
зачисляется за вычетом платы за обработку, а если она больше суммы — failed.
Пыль — рассылки на суммы меньше пары центов — не записывается.
error есть только у failed и бывает одной из фраз:
«Не прошёл AML», «Сумма ниже минимальной: плата за обработку больше самого перевода»,
«Сумма ниже минимальной для зачисления», «Сумма слишком мала для зачисления»,
«Блок не закрепился в сети» (редкость: в Ethereum USDT виден уже с первого блока —
deposit.pending, — а блок ещё может выпасть из цепи) или общая
«Депозит не зачислен».
Выплаты с баланса мерчанта
Как это устроено
Всё, что ваш сервер тратит, уходит только с баланса мерчанта: перевод
пользователю Laveshka, чек по ссылке, оплата QR СБП, вывод на карту, Steam, Stars и
Premium, вывод в блокчейн. Эндпоинты с меткой
Выплаты по API работают, только когда владелец включил
переключатель Настройки → Выплаты по API (под PIN); иначе —
403 forbidden. Состояние видно в payouts_enabled ответа
GET /me.
- Одна команда. Вывод на карту, Steam, Stars и Premium, вывод в блокчейн — цена по курсу этой минуты, удержание с баланса и отправка за один вызов, без подтверждения. Оплата QR — два шага: проверка называет получателя и цену, оплата её тратит.
- Страховка от курса.
max_debit— «списать не больше»: если цена выше,409 rate_changedи ничего не списано. - Исход — вебхуком. Обычно ответ —
processing: деньги удержаны, исполнение идёт; итог приходит вебхуком*.completed/*.failed, аdataв нём — тот же объект, что отдаётGET. Наpendingиprocessingвебхуков нет. - Любой возврат — на баланс мерчанта. Отказ банка, сети или поставщика после
списания возвращает деньги ровно туда, откуда взято удержание; объект становится
failedс причиной вerror. На основной баланс владельца деньги отсюда не уходят, пушей о тратах по API он не получает. idempotency_keyобязателен у каждой траты, и объект читается по нему:GET …/{ваш ключ}— см. идемпотентность.
Перевод пользователю
Выплатить можно двумя способами, и выбор между ними — про то, кто должен существовать. Перевод уходит на конкретный аккаунт Laveshka: если человек его ещё не открывал, платить некому. Чек не адресован никому — деньги ждут на ссылке, получатель открывает её, и аккаунт создаётся по дороге. Платите своим клиентам, которых не знаете заранее, — берите чек.
Перевести пользователю Laveshka с баланса мерчанта.
curl -X POST "https://<домен>/merchant/v1/transfers" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient": "@durov", "amount": "5.00", "comment": "Бонус за отзыв", "idempotency_key": "bonus-99-2026-08-22" }'
transfer = await lp.transfers.create( "@durov", "5.00", comment="Бонус за отзыв", idempotency_key="bonus-99-2026-08-22", )
{
"id": "tr_8f618f50-…",
"amount": "5.00",
"currency": "USD",
"recipient": { "telegram_id": 123456789, "username": "durov" },
"comment": "Бонус за отзыв",
"created_at": "2026-08-22T09:12:00+00:00"
}
| Поле | Тип | Что это |
|---|---|---|
recipient обяз. | строка | @username, username или Telegram ID пользователя Laveshka. Аккаунт должен уже существовать, иначе 404 not_found |
amount обяз. | USD | без комиссии; минимум 0.10 |
comment | до 140 | покажется получателю |
idempotency_key обяз. | до 64 | повтор с тем же ключом вернёт ту же операцию и не спишет второй раз |
Выплата чеком
Отложить деньги на ссылку. Списывается сразу вся сумма
(amount × activations) и держится в эскроу; каждая активация
выплачивает одну долю, невостребованное возвращается при отзыве. Получателя называть
не нужно — кто первым откроет ссылку, тот и заберёт.
curl -X POST "https://<домен>/merchant/v1/cheques" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"amount": "5.00", "comment": "Выплата №4665", "idempotency_key": "payout-4665"}'
cheque = await lp.cheques.create( "5.00", comment="Выплата №4665", idempotency_key="payout-4665", ) print(cheque.url)
{
"id": "chq_311",
"public_id": "CQF5L6J29VI",
"status": "active",
"amount": "5.00",
"total": "5.00",
"currency": "USD",
"activations_total": 1,
"activations_used": 0,
"comment": "Выплата №4665",
"recipient": "",
"url": "https://t.me/laveshka?startapp=chq_…",
"idempotency_key": "payout-4665",
"created_at": "2026-08-30T18:47:04+00:00",
"closed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
amount обяз. | строка | сколько получает одна активация — не сумма чека; без комиссии; 0.10 … 100 000 $ в asset по курсу |
asset | USDT | USDT, GRAM, TRX, SOL, BNB или ETH: списывается с баланса мерчанта в этой монете, получатель получает её же |
activations | 1 … 100 000 | сколько человек могут забрать по доле; итог amount × activations ≤ 1 000 000 $ |
comment | до 140 | покажется получателю |
recipient | @username | забрать сможет только он; пусто — чек на предъявителя |
idempotency_key обяз. | до 64 | повтор с тем же ключом вернёт тот же чек, а не выпишет второй. По нему же чек потом читается |
url — это и есть деньги. Ссылку достаточно передать получателю,
больше ничего не нужно. И наоборот: у кого она есть, тот и заберёт, так что в открытый
чат её лучше не класть — либо укажите recipient.
Статус и отзыв чека
/cheques/{id}
Один чек — по chq_… или по вашему idempotency_key
(удобно, если ответ на создание потерялся). Забрал ли получатель деньги, показывает
activations_used: 0 у чека active — ещё не забрал.
Статусы: active, completed (все доли разобраны),
cancelled; closed_at — когда закрыт.
curl "https://<домен>/merchant/v1/cheques/payout-4665" \ -H "X-Api-Key: $LAVE_KEY"
cheque = await lp.cheques.get("payout-4665") # chq_311 или ваш ключ if cheque.activations_used: ...
/cheques
Список: status (active|completed|cancelled), limit, before.
/cheques/{id}/cancel
Отозвать чек. Невостребованные активации возвращаются на баланс мерчанта, ссылка
перестаёт работать. Уже активированные доли вернуть нельзя — это потраченные деньги,
как и любая выплата. Только для active.
Рубли: QR и карты
Оплата QR
Ваш сервер оплачивает QR-коды СБП по ссылке из QR: USDT списываются с баланса мерчанта, рубли приходят получателю. Цена — как в приложении Laveshka, все комиссии внутри; единственный курс в ответах — «₽ за 1 USDT». Два шага: проверка называет получателя и сколько USDT спишется и держит эту цену несколько секунд, оплата её тратит.
/qr/rate
Ориентир: rate — ₽ за 1 USDT для оплат до small_max_rub,
rate_large — для оплат дороже, лимиты min_rub/max_rub,
quote_ttl — сколько секунд держится проверенная цена, и
max_affordable_rub — сколько рублей покрывает баланс мерчанта. Точную цену
конкретного QR называет только проверка.
Проверить QR
Проверить QR и заморозить его цену на несколько секунд. qr — ссылка из
QR-кода (https://qr.nspk.ru/…) или текст, в котором она есть. Ничего не
списывается.
curl -X POST "https://<домен>/merchant/v1/qr/check" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"qr": "https://qr.nspk.ru/AD10006M…?type=02&bank=100000000111&sum=150000&cur=RUB&crc=1A2B"}'
quote = await lp.qr.check("https://qr.nspk.ru/AD10006M…") print(quote.payee, quote.amount_rub, quote.debit)
{
"quote_id": "qrq_5f0c2a8e-…",
"payee": "ООО «Ромашка»",
"amount_rub": "1500",
"debit": "18.35",
"currency": "USDT",
"rate": "81.74",
"expires_at": "2026-10-02T20:31:26+00:00"
}
debit — сколько USDT спишется с баланса мерчанта, amount_rub —
сколько получит получатель (сумма зашита в самом QR). Одна оплата — 1 … 400 000 ₽.
Если USDT на балансе мерчанта не хватает на эту цену, проверка отвечает
409 insufficient_funds («Недостаточно USDT на балансе мерчанта») — цены нет,
ничего не списано; пополните баланс и проверьте QR снова. Оплата QR, временно
недоступная, — 409 payout_unavailable.
Оплатить QR
Оплатить проверенный QR с баланса мерчанта.
curl -X POST "https://<домен>/merchant/v1/qr/payments" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"quote_id": "qrq_5f0c2a8e-…", "idempotency_key": "order-4665"}'
payment = await lp.qr.pay(quote.quote_id, idempotency_key="order-4665") # или проверка и оплата одним вызовом payment = await lp.qr.pay_link("https://qr.nspk.ru/AD10006M…", idempotency_key="order-4665")
{
"id": "qr_9b1e44d0-…",
"status": "completed",
"payee": "ООО «Ромашка»",
"amount_rub": "1500",
"debit": "18.35",
"currency": "USDT",
"rate": "81.74",
"error": null,
"returned": null,
"returned_at": null,
"idempotency_key": "order-4665",
"replayed": false,
"created_at": "2026-10-02T20:31:05+00:00",
"completed_at": "2026-10-02T20:31:07+00:00"
}
| Поле | Тип | Что это |
|---|---|---|
quote_id обяз. | строка | из ответа POST /qr/check |
idempotency_key обяз. | до 64 | повтор с тем же ключом вернёт ту же оплату (replayed: true) и второй раз не заплатит; тот же ключ с другим QR — 409 idempotency_conflict |
Ответ приходит в течение ~3 секунд: completed, failed или
processing — оплата ещё проводится, исход придёт вебхуком
qr_payment.completed / qr_payment.failed (или дождитесь его
запросом статуса с ?wait=). Если цена истекла не больше минуты назад, QR
проверяется заново, и оплата проходит, только если не стала дороже; иначе —
409 quote_expired, а новая цена в error.details.quote.
completed, на баланс мерчанта
возвращается сумма покупки (returned), комиссия сервиса не возвращается.
У оплаты появляются returned и returned_at, приходит
qr_payment.reversed.
Статус оплаты QR
/qr/payments/{id}
Одна оплата — по qr_… или по вашему
idempotency_key. ?wait=N&known=<status> держит ответ,
пока статус не уйдёт из known, но не дольше N секунд (≤ 25).
# ждать исхода, пока оплата в работе curl "https://<домен>/merchant/v1/qr/payments/order-4665?wait=25&known=processing" \ -H "X-Api-Key: $LAVE_KEY"
payment = await lp.qr.get("order-4665", wait=25, known="processing")
/qr/payments
Список: status (pending|processing|completed|failed), limit, before.
Вывод на карту
Рубли на банковскую карту с USDT-баланса мерчанта — одной командой, без подтверждения:
цена по курсу этой минуты, холд и отправка за один вызов. Если мерчанту назначена
персональная комиссия на вывод на карту, с ней уже считаются GET /card/rate,
его price и сама выплата.
/card/rate
Курс на 10 000 ₽ (rate), примеры цен (examples),
лимиты и max_affordable_rub — сколько рублей покрывает баланс мерчанта.
С ?amount_rub=5000 или ?amount_usdt=50 ответ добавляет
price — точную цену этой суммы прямо сейчас (не замораживается).
{
"available": true,
"reason": null,
"currency": "USDT",
"rate": "83.57",
"examples": [{ "amount_rub": "1000", "debit": "12.12", "rate": "82.50" }],
"min_rub": "100",
"max_rub": "294000",
"max_affordable_rub": "41850",
"price": { "amount_rub": "5000", "debit": "59.83", "rate": "83.57" }
}
Вывести на карту
curl -X POST "https://<домен>/merchant/v1/card/payouts" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"card_number": "2200 7034 5678 9012", "amount_rub": "5000", "idempotency_key": "payout-4665"}'
payout = await lp.card.payout( "2200 7034 5678 9012", amount_rub="5000", max_debit="61", idempotency_key="payout-4665", )
{
"id": "cp_7d2a90b1-…",
"status": "processing",
"amount_rub": "5000",
"debit": "59.83",
"currency": "USDT",
"rate": "83.57",
"card": { "brand": "mir", "last4": "9012", "masked": "2200 70•• •••• 9012" },
"error": null,
"idempotency_key": "payout-4665",
"replayed": false,
"created_at": "2026-10-02T20:40:11+00:00",
"completed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
card_number обяз. | строка | номер карты; пробелы и дефисы допустимы |
amount_rub | целые ₽ | 100 … 300 000 на карту. Или amount_usdt |
amount_usdt | USDT | «потратить не больше»: уйдут наибольшие целые рубли, которые укладываются в эти USDT |
max_debit | USDT | страховка — и с amount_rub, и с amount_usdt: если списание выше, 409 rate_changed, ничего не списано |
idempotency_key обяз. | до 64 | повтор вернёт ту же выплату и цену заново не считает; тот же ключ с другой картой или суммой — 409 idempotency_conflict |
Обычно ответ — processing: рубли ушли в банк, исход придёт вебхуком
card_payout.completed или card_payout.failed. Если курс изменился
между ценой и отправкой, цена один раз пересчитывается сама. Бывает и
pending — USDT удержаны, рубли ещё не ушли в банк; исход придёт тем же
вебхуком.
Статус выплаты
/card/payouts/{id}
Одна выплата — по cp_… или по вашему idempotency_key.
/card/payouts
Список: status (pending|processing|completed|failed), limit, before.
Вывод по СБП
Рубли по номеру телефона в любой банк СБП с USDT-баланса мерчанта — так же, как на карту: одной командой, без подтверждения, по курсу этой минуты. Получателя задают три поля: телефон, банк и имя в виде «Имя Ф» — имя и первая буква фамилии, как их видит банк получателя.
/sbp/banks
Банки, на которые можно перевести. bank_id передаётся в выплату как есть.
Список меняется редко — держите его у себя до часа.
{
"items": [
{ "bank_id": "100000000111", "name": "Сбербанк" },
{ "bank_id": "100000000004", "name": "Т-Банк" },
…
]
}
/sbp/rate
Как GET /card/rate: курс на 10 000 ₽
(rate), примеры цен, лимиты (500 … 100 000 ₽) и
max_affordable_rub. С ?amount_rub=5000 или
?amount_usdt=50 — ещё price, точная цена этой суммы прямо сейчас.
Вывести по СБП
curl -X POST "https://<домен>/merchant/v1/sbp/payouts" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"phone": "+79991234567", "bank_id": "100000000004", "recipient_name": "Дмитрий Р", "amount_rub": "5000", "idempotency_key": "sbp-4665"}'
{
"id": "sbp_3f61c0d2-…",
"status": "processing",
"amount_rub": "5000",
"debit": "59.94",
"currency": "USDT",
"rate": "83.41",
"recipient": {
"phone": "+7 999 •••-45-67", "last4": "4567",
"bank_id": "100000000004", "bank_name": "Т-Банк", "name": "Дмитрий Р"
},
"error": null,
"idempotency_key": "sbp-4665",
"replayed": false,
"created_at": "2026-10-16T18:40:11+00:00",
"completed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
phone обяз. | строка | мобильный номер РФ: +79991234567, 89991234567 или 9991234567; пробелы, скобки и дефисы допустимы |
bank_id обяз. | строка | банк получателя из GET /sbp/banks |
recipient_name обяз. | строка | имя и первая буква фамилии: Дмитрий Р. Фамилия целиком, отчество или латиница — 422 validation_error с подсказкой |
amount_rub | целые ₽ | 500 … 100 000 получателю. Или amount_usdt |
amount_usdt | USDT | «потратить не больше»: уйдут наибольшие целые рубли, которые укладываются в эти USDT |
max_debit | USDT | страховка — и с amount_rub, и с amount_usdt: если списание выше, 409 rate_changed, ничего не списано |
idempotency_key обяз. | до 64 | повтор вернёт тот же перевод и цену заново не считает; тот же ключ с другим получателем или суммой — 409 idempotency_conflict, как и ключ, которым уже выводили на карту |
Обычно ответ — processing: перевод ушёл в банк, исход придёт вебхуком
sbp_payout.completed или sbp_payout.failed. Если банк получателя
не принял перевод — номер не привязан к этому банку или имя не совпало — перевод
failed, USDT вернулись на баланс мерчанта. Если курс изменился между ценой
и отправкой, цена один раз пересчитывается сама.
Статус перевода
/sbp/payouts/{id}
Один перевод — по sbp_… или по вашему idempotency_key.
/sbp/payouts
Список: status (pending|processing|completed|failed), limit, before.
Steam, Stars и Premium
Пополнение Steam
Пополнить кошелёк Steam по логину с USDT-баланса мерчанта — одной командой, как вывод на карту. Точная цена называется в момент заказа; любой отказ возвращает списанное на баланс мерчанта.
/steam/rate
min_usd/max_usd одного заказа, currencies — валюты
Steam с курсом к доллару, price_factor — ожидаемые USDT за 1 $ пополнения
и max_affordable_usd — сколько долларов покрывает баланс мерчанта.
С ?amount=1000&steam_currency=RUB ответ добавляет estimate —
доллары пополнения и ожидаемое списание.
curl -X POST "https://<домен>/merchant/v1/steam/orders" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"account": "gamer123", "amount": "1000", "steam_currency": "RUB", "max_debit": "12.50", "idempotency_key": "steam-4665"}'
order = await lp.steam.order( "gamer123", amount="1000", steam_currency="RUB", max_debit="12.50", idempotency_key="steam-4665", )
{
"id": "st_3a9c51e2-…",
"status": "processing",
"account": "gamer123",
"amount_usd": "11.90",
"steam_currency": "RUB",
"amount_local": "1000.00",
"debit": "12.32",
"currency": "USDT",
"waiting": false,
"error": null,
"idempotency_key": "steam-4665",
"replayed": false,
"created_at": "2026-10-02T21:05:11+00:00",
"completed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
account обяз. | строка | логин Steam — не ник профиля; неподходящий логин — 422 steam_account_rejected, ничего не списано |
amount | строка | сумма в валюте аккаунта (steam_currency, по умолчанию USD); в доллары — по курсу Steam вверх до цента. Или amount_usdt |
amount_usdt | USDT | «потратить не больше»: наибольшие доллары, которые укладываются в эти USDT |
max_debit | USDT | страховка: если цена выше, 409 rate_changed, ничего не списано |
idempotency_key обяз. | до 64 | повтор вернёт тот же заказ; тот же ключ с другим логином или суммой — 409 idempotency_conflict |
Ответ — в течение ~25 секунд: completed, failed или
processing (исход придёт вебхуком steam_order.*).
waiting: true — заказ идёт дольше обычного, но в пути. Один заказ —
1 … 1000 $.
Статус заказа Steam
/steam/orders/{id}
Один заказ — по st_… или по вашему idempotency_key.
/steam/orders
Список: status, limit, before.
Stars и Premium
Telegram Stars или подарочный Premium на @юзернейм с USDT-баланса мерчанта — одной командой. Сколько назвал курс, столько и спишется.
/telegram/rate
stars: price_per_unit, min_quantity/max_quantity,
presets; premium: options на 3, 6 и 12 месяцев;
max_purchase — потолок одной покупки. С ?service=stars&quantity=500
ответ добавляет price — списание за эту покупку.
curl -X POST "https://<домен>/merchant/v1/telegram/orders" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"service": "stars", "recipient": "@durov", "quantity": 500, "idempotency_key": "stars-4665"}'
order = await lp.telegram.order( "stars", "@durov", 500, idempotency_key="stars-4665", )
{
"id": "tg_0e7b33c4-…",
"status": "completed",
"service": "stars",
"recipient": "durov",
"quantity": 500,
"debit": "8.03",
"currency": "USDT",
"refunded": false,
"error": null,
"idempotency_key": "stars-4665",
"replayed": false,
"created_at": "2026-10-02T21:10:02+00:00",
"completed_at": "2026-10-02T21:10:04+00:00"
}
| Поле | Тип | Что это |
|---|---|---|
service обяз. | строка | stars или premium |
recipient обяз. | @юзернейм | не найден — 404 not_found, ничего не списано |
quantity обяз. | число | звёзды или месяцы Premium (3, 6, 12) |
max_debit | USDT | страховка: цена выше — 409 rate_changed, ничего не списано |
idempotency_key обяз. | до 64 | повтор вернёт ту же покупку; тот же ключ с другой покупкой — 409 idempotency_conflict |
failed с refunded: true: деньги уже на балансе мерчанта.
Fragment не ответил — processing, сверка доведёт покупку сама (вебхук
telegram_order.*). Не ответил до списания — 502 provider_error,
ничего не списано.
Статус покупки
/telegram/orders/{id}
Одна покупка — по tg_… или по вашему idempotency_key.
/telegram/orders
Список: status, service (stars|premium), limit, before.
Вывод в блокчейн
Как это устроено
Ваш сервер выводит монеты на адрес в сети тем же выводом, что человек делает из
приложения Laveshka: те же сети, комиссии и минимумы. USDT на любой сети списываются с
USDT-баланса мерчанта, GRAM, TRX, SOL, BNB и ETH — с баланса в этой монете
(balances в GET /me; USDT там — под ключом USD).
- Одна команда: цена по курсу этой минуты, удержание с баланса мерчанта и отправка в сеть — за один вызов.
- Сумма — то, что получит адрес, в монете вывода; наша комиссия — сверху, в монете баланса.
- Только адрес в сети: USDT по TRC-20, BEP-20, ERC-20 (Ethereum), GRAM и Solana, монеты GRAM, TRX и SOL — каждая по своей сети, BNB — по BSC, ETH — по Ethereum; весь список —
GET /withdrawals/networks. Выплаты кошельком Telegram и чеком у этого API нет — для них перевод и чек. - USDT в Ethereum (
"network": "ETH", так же и монета ETH): адрес0x…выглядит так же, как в BSC и в L2, — убедитесь, что получатель принимает эту монету именно в сети Ethereum. Когда сеть перегружена, вывод может уйти позже — сам, деньги удержаны. Пока он в работе, перевод может быть переотправлен с другой ценой газа, иtx_hashсменится: окончательный хеш — вwithdrawal.completed.
withdrawal.completed /
withdrawal.failed.
Сети и комиссии
/withdrawals/networks
Куда можно вывести: монета и сеть, наша комиссия, наименьшая сумма и сколько баланс мерчанта выведет прямо сейчас.
{
"available": true,
"reason": null,
"items": [
{
"asset": "USDT",
"network": "TRX",
"name": "Tether USDT (TRC-20)",
"currency": "USDT",
"fee": "4",
"min_amount": "1",
"max_amount": "1528.2",
"decimals": 6,
"requires_memo": false,
"address_regex": "^T[1-9A-HJ-NP-Za-km-z]{33}$"
},
{
"asset": "BNB",
"network": "BSC",
"name": "BNB (BNB Smart Chain)",
"currency": "BNB",
"fee": "0.000166667",
"min_amount": "0.001666667",
"max_amount": "0.024833333",
"decimals": 9,
"requires_memo": false,
"address_regex": "^0x[0-9a-fA-F]{40}$"
},
…
]
}
| Поле | Тип | Что это |
|---|---|---|
currency | строка | баланс, с которого спишется вывод, и единица fee, min_amount, max_amount: USDT у USDT на любой сети, сама монета у GRAM, TRX, SOL, BNB и ETH |
fee | строка | наша комиссия за вывод — сверху суммы, в currency. У каждой сети своя и может меняться, у монет к тому же пересчитывается по курсу: берите её отсюда или из котировки, не зашивайте в код |
min_amount | строка | наименьшая сумма на адрес, в монете по курсу сейчас |
max_amount | строка | сколько баланс мерчанта выведет прямо сейчас, комиссия уже вычтена; не больше 10 000 $ в монете. Меньше min_amount — выводить нечего |
decimals | число | сколько знаков после точки принимает amount: 6 у USDT и TRX, 9 у GRAM, SOL, BNB и ETH (у BNB и ETH это гвей: точнее 0.000000001 монеты вывод не принимает) |
requires_memo, address_regex | bool, строка | нужен ли memo и какой формат адреса; адрес проверяется по нему до списания |
available: false с причиной в reason — выплаты по API выключены,
мерчант на паузе или вывод временно выключен на сервере; список при этом всё равно
приходит. Пары, по которым вывод временно закрыт, в списке нет: вывод в них получает
409 payout_unavailable или 422, ничего не списано.
Цена вывода
/withdrawals/quote
Цена вывода прямо сейчас — то же списание, что сделал бы POST /withdrawals,
но ничего не удерживается и цена не закрепляется. Параметры: asset,
network, amount.
curl "https://<домен>/merchant/v1/withdrawals/quote?asset=USDT&network=TRX&amount=25" \ -H "X-Api-Key: $LAVE_KEY"
{
"asset": "USDT",
"network": "TRX",
"amount": "25",
"fee": "4",
"debit": "29",
"currency": "USDT",
"amount_usd": "25.00",
"balance": "1532.2",
"enough": true
}
Сумма проверяется так же, как у вывода: лишние знаки, ниже минимума или выше потолка —
422 (details.minimum, details.maximum).
enough — хватает ли balance.
Вывести
Вывести монеты на адрес в сети с баланса мерчанта — одной командой, без подтверждения.
curl -X POST "https://<домен>/merchant/v1/withdrawals" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{ "asset": "USDT", "network": "TRX", "address": "TXr4…9mQp", "amount": "25", "max_debit": "29", "idempotency_key": "wd-4665" }' # вывод BNB — то же самое в монете: до девяти знаков в amount, # удержание и комиссия — с BNB-баланса мерчанта; ETH — так же, # {"asset": "ETH", "network": "ETH", …} с ETH-баланса curl -X POST "https://<домен>/merchant/v1/withdrawals" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"asset": "BNB", "network": "BSC", "address": "0x3f5C…9aB1", "amount": "0.02", "max_debit": "0.020166667", "idempotency_key": "wd-4666"}'
{
"id": "wd_48213",
"status": "processing",
"asset": "USDT",
"network": "TRX",
"address": "TXr4…9mQp",
"memo": null,
"amount": "25",
"fee": "4",
"debit": "29",
"currency": "USDT",
"amount_usd": "25.00",
"tx_hash": null,
"tx_url": null,
"error": null,
"idempotency_key": "wd-4665",
"replayed": false,
"created_at": "2026-10-08T12:00:11+00:00",
"completed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
asset обяз. | строка | монета пары из GET /withdrawals/networks: USDT, GRAM, TRX, SOL, BNB, ETH |
network обяз. | строка | код сети оттуда же: TRX, BSC, ETH, GRAM, SOL; читаются и TRC20/TRON, BEP20/BNB, ERC20/ERC-20/ETHEREUM, TON. BNB здесь — сеть BSC; монета BNB пишется в asset: {"asset": "BNB", "network": "BSC"} |
address обяз. | до 128 | адрес получателя в этой сети; неверный формат — 422, ничего не списано |
memo | строка | только если у пары requires_memo; у остальных пар не принимается — 422 |
amount обяз. | строка | сколько получит адрес, в монете пары; не больше decimals знаков, от min_amount до потолка — иначе 422. Комиссия — сверху |
max_debit | строка | страховка: если списание (amount + fee, в currency) выше, 409 rate_changed, ничего не списано |
idempotency_key обяз. | до 64 | повтор вернёт тот же вывод и второй раз не спишет; тот же ключ с другой монетой, сетью, адресом, memo или суммой — 409 idempotency_conflict. Не networks, не quote и не вида wd_<число> — так GET /withdrawals/{id} прочитал бы не ваш ключ (422) |
amount — сколько получит адрес, fee — наша комиссия,
debit — их сумма, списанная с баланса currency;
amount_usd — оценка в долларах на момент вывода. Комиссия 4 в
примере — условная: актуальная — в fee ответа
GET /withdrawals/networks.
status | Что значит |
|---|---|
pending | деньги удержаны, вывод ещё не ушёл в сеть |
processing | деньги удержаны, транзакция у сети — обычный ответ на создание |
completed | сеть подтвердила перевод: tx_hash и tx_url (ссылка на обозреватель) заполнены, приходит withdrawal.completed |
failed | вывод не состоялся, причина в error, вся сумма с комиссией уже на балансе мерчанта, приходит withdrawal.failed. completed_at — время исхода, у failed тоже |
error | Что значит |
|---|---|
| «Не прошёл AML» | вывод не прошёл AML-проверку |
| «Сеть отказала — деньги вернулись на баланс» | сеть не приняла перевод |
| «Неверный адрес получателя» | сеть не приняла адрес — проверьте его; у BNB и ETH так же отвечает адрес контракта, который не принимает монету |
| «Сумма больше допустимой для одного вывода», «Максимальная сумма вывода — $X» | потолок одного вывода по этой сети — уменьшите сумму или разделите вывод |
| «Вывод … сейчас недоступен — деньги остались на балансе. …» | направление временно закрыто; выберите другую сеть из GET /withdrawals/networks |
| «Мерчант на паузе», «Выплаты по API у мерчанта выключены», «Мерчант удалён» | состояние мерчанта изменилось до отправки вывода |
| «Вывод отклонён», «Вывод не прошёл» | отклонён; любая другая причина |
Отказ до списания — ошибка ответа: ничего не списано, вывода нет, ключ не
израсходован. Это 422 (пара, адрес, сумма), 409 insufficient_funds
(«Недостаточно USDT на балансе мерчанта», у монет — GRAM, TRX, SOL, BNB, ETH),
409 rate_changed, 409 payout_unavailable,
403 forbidden, 429 и 503 provider_not_configured.
Отказ после списания — не ошибка: ответ 200 с выводом
failed, деньги уже вернулись, приходит withdrawal.failed.
Повтор с тем же idempotency_key возвращает тот же вывод
(replayed: true): сумма сравнивается числом — 25 и
25.00 одно и то же, сеть — кодом (TRC20 и TRX — одно).
Повтор узнаётся раньше всех проверок: даже если выплаты по API с тех пор выключили
или сети больше нет в списке, ответ — уже сделанный вывод, а не отказ.
Статус и список
/withdrawals/{id}
Один вывод — по wd_… или по вашему idempotency_key.
/withdrawals
Список: status (pending|processing|completed|failed),
limit, before. Выводов — не больше 30 в минуту на мерчанта
(429), повтор с тем же ключом не в счёт.
Пополнение
Покупка USDT за рубли
Обратное направление: рубли → USDT на баланс мерчанта. Ваш сервер заводит покупку, отдаёт плательщику ссылку на оплату по СБП, и после оплаты USDT приходят на USDT-баланс мерчанта. Платить может кто угодно — вы сами или ваш клиент, кошелёк Laveshka ему не нужен. Переключатель Выплаты по API не нужен: покупка ничего не тратит.
- Одна команда: цена по курсу этой минуты, заявка и счёт на оплату за один вызов — ссылка на оплату уже в ответе.
- Цена — как в приложении Laveshka: все комиссии внутри курса, единственный курс в ответах — «₽ за 1 USDT». Курс закреплён за покупкой: сколько USDT названо, столько и придёт.
- Оплата — по QR-коду СБП на счёт юридического лица, без переводов частным лицам.
buy_order.completed, buy_order.cancelled),
а data в нём — тот же объект, что отдаёт GET.
Закрытая покупка ещё может стать completed: если плательщик успел
заплатить в последнюю секунду, поздний платёж зачислится, и придёт
buy_order.completed.
Курс покупки
/buy/rate
rate — ₽ за 1 USDT, min_rub/max_rub одной покупки,
payment_minutes — сколько минут на оплату по ссылке,
open_orders/max_open_orders — покупок в работе сейчас и сколько
можно сразу, blocked_until — до какого момента покупка закрыта.
С ?amount_rub=10000 или ?amount_usdt=100 ответ добавляет
price — точную цену этой суммы прямо сейчас (не замораживается).
{
"available": true,
"reason": null,
"currency": "USDT",
"rate": "100.55",
"min_rub": "500",
"max_rub": "20000",
"payment_minutes": 15,
"open_orders": 0,
"max_open_orders": 10,
"blocked_until": null,
"price": { "amount_rub": "10000", "amount_usdt": "99.45", "rate": "100.55" }
}
Купить USDT
/buy/orders
Купить USDT за рубли на баланс мерчанта — одной командой. Ответ обычно приходит за пару секунд; иногда ссылку приходится подождать до 10 секунд — таймаут запроса ставьте не меньше 30 секунд.
curl -X POST "https://<домен>/merchant/v1/buy/orders" \ -H "X-Api-Key: $LAVE_KEY" \ -H "Content-Type: application/json" \ -d '{"amount_rub": "10000", "max_rate": "102", "idempotency_key": "topup-4665"}'
order = await lp.buy.order( amount_rub="10000", max_rate="102", idempotency_key="topup-4665", ) print(order.payment_url) # дождаться исхода опросом, если вебхука нет order = await lp.buy.wait(order.id, timeout=900)
{
"id": "buy_2c8e61f0-…",
"status": "awaiting_payment",
"amount_rub": "10000",
"amount_usdt": "99.45",
"currency": "USDT",
"rate": "100.55",
"payment_url": "https://qr.nspk.ru/BS1A004TIU6…",
"pay_until": "2026-10-07T10:14:41+00:00",
"cancel_reason": null,
"can_cancel": true,
"idempotency_key": "topup-4665",
"replayed": false,
"created_at": "2026-10-07T09:59:41+00:00",
"closed_at": null
}
| Поле | Тип | Что это |
|---|---|---|
amount_rub | целые ₽ | к оплате, от min_rub до max_rub из GET /buy/rate. Или amount_usdt |
amount_usdt | USDT | сколько USDT купить, до двух знаков: рубли округляются вверх, придёт не меньше |
max_rate | ₽ за 1 USDT | страховка: если курс выше, 409 rate_changed, ничего не создано |
idempotency_key обяз. | до 64 | повтор с тем же ключом вернёт ту же покупку и второго счёта не откроет; тот же ключ с другой суммой — 409 idempotency_conflict |
payment_url отдайте плательщику ссылкой (на телефоне она откроет оплату в
его банке) или QR-кодом. Платить — точную сумму одним платежом и до pay_until.
Ссылка видна, пока покупка awaiting_payment.
status | Что значит |
|---|---|
pending | ссылки пока нет — она готовится, обычно секунды (не дольше ~15 с); повтор запроса с тем же ключом отправит покупку |
awaiting_payment | ссылка выдана, ждём оплату до pay_until |
overdue | время на оплату вышло, итога ещё нет: по ссылке больше не платят, но платёж последней секунды зачислится |
completed | оплачено — amount_usdt на балансе мерчанта |
cancelled | закрыта без денег; cancel_reason: unpaid — не оплатили вовремя, client — вы отменили, unreachable — счёт не выдан |
Статус и отмена
/buy/orders/{id}
Одна покупка — по buy_… или по вашему idempotency_key.
/buy/orders
Список: status (pending|awaiting_payment|overdue|completed|cancelled), limit, before.
/buy/orders/{id}/cancel
Снять неоплаченную покупку: счёт снимается, по старой ссылке уже не заплатить. Если оплата пришла в эту же секунду, отмены не будет — покупка зачислится. Пока платёж проверяется, отменить нельзя.
409 limit_exceeded).
Если неоплаченных и снятых покупок слишком много, покупка временно закрывается:
403 buy_blocked до details.until (blocked_until в курсе).
Вебхуки
Настройка
URL задаётся в приложении (Lave Pay → мерчант → Вебхук); там же —
секрет подписи whsec_…, кнопка «Тест» и журнал всех доставок. Вебхук —
быстрый путь узнать об исходе, но не единственный источник правды: держите рядом опрос
GET объекта.
В продакшене — только https и только публичный хост; локальные и приватные
адреса отклоняются — и при сохранении, и прямо перед каждой отправкой. Попытка на
адрес, который проверка не пропустила, помечается blocked address
(приватный или служебный адрес) или invalid response (URL, на который
отправить нельзя) и считается обычной неудачной попыткой: она тратит попытку, идёт в
здоровье endpoint'а и в суточное автоотключение. Редиректы не выполняются.
События
POST <ваш URL> с Content-Type: application/json:
{
"id": "evt_3c0d8e7f1f5c4a3e9a6b2b0d0f1e2a3b",
"type": "invoice.paid",
"created_at": "2026-08-22T09:15:02+00:00",
"merchant_id": 12,
"data": { …объект, как в его GET… }
}
X-LavePay-Event-Id: evt_3c0d8e7f… # один и тот же при всех повторах X-LavePay-Event: invoice.paid X-LavePay-Attempt: 1 # номер попытки X-LavePay-Timestamp: 1787479200 # unix-секунды этой попытки X-LavePay-Signature: v1=9f2c… # см. ниже User-Agent: LavePay-Webhooks/1.0
type | Когда | data |
|---|---|---|
invoice.paid | покупатель оплатил | счёт со status: paid, payer, paid_at |
invoice.expired | вышел ttl, перевод не найден | счёт |
invoice.cancelled | вы отменили счёт | счёт |
invoice.refunded | вы сделали возврат | счёт с refunded_at |
invoice.underpaid | бесшовный счёт оплачен не полностью — пришедшее на балансе мерчанта | счёт (active) и underpayment |
deposit.pending | перевод на адрес клиента увиден | депозит, как в GET /deposits/{id} |
deposit.completed | депозит зачислен на баланс мерчанта | депозит с fee и net |
deposit.failed | депозит не будет зачислен | депозит с error |
qr_payment.completed | оплата QR прошла | оплата, как в GET /qr/payments/{id} |
qr_payment.failed | банк не принял оплату — USDT вернулись на баланс мерчанта | оплата с error |
qr_payment.reversed | магазин вернул прошедшую оплату | оплата с returned, returned_at |
card_payout.completed | рубли дошли до карты | выплата, как в GET /card/payouts/{id} |
card_payout.failed | выплата не прошла или отклонена — USDT вернулись на баланс мерчанта | выплата с error |
sbp_payout.completed | рубли дошли до получателя по СБП | перевод, как в GET /sbp/payouts/{id} |
sbp_payout.failed | перевод не прошёл — USDT вернулись на баланс мерчанта | перевод с error |
steam_order.completed | Steam зачислил пополнение | заказ, как в GET /steam/orders/{id} |
steam_order.failed | Steam не принял заказ — USDT вернулись на баланс мерчанта | заказ с error |
telegram_order.completed | Stars или Premium доставлены | покупка, как в GET /telegram/orders/{id} |
telegram_order.failed | Fragment отклонил или не подтвердил покупку — USDT вернулись на баланс мерчанта | покупка с error, refunded |
withdrawal.completed | вывод в блокчейн подтверждён сетью | вывод, как в GET /withdrawals/{id}, с tx_hash и tx_url |
withdrawal.failed | вывод не прошёл (причина — в error) — вся сумма с комиссией вернулась на баланс мерчанта | вывод с error |
buy_order.completed | рубли оплачены — USDT на балансе мерчанта (в том числе поздний платёж по закрытой покупке) | покупка, как в GET /buy/orders/{id} |
buy_order.cancelled | покупка закрыта без денег: не оплачена, снята вами или счёт не выдан | покупка с cancel_reason |
webhook.test | кнопка «Тест» / POST /webhook/test | {"message": "…", "merchant_id": 12} |
На промежуточные состояния — pending, processing,
confirming — вебхуков нет, только на исход.
Проверка подписи
Подпись считается по сырому телу запроса — байты как есть, не пересериализованный JSON:
signature = "v1=" + hex( HMAC_SHA256( secret, timestamp + "." + raw_body ) )
secret — секрет подписи из настроек вебхука (whsec_…).
Сравнивайте константным временем и отбрасывайте запросы, чей
X-LavePay-Timestamp старше 5 минут — это защита от повторного
воспроизведения перехваченного запроса. Дальше — дедупликация по id
события: повтор несёт тот же id и то же тело, подпись при этом другая
(другой timestamp).
from fastapi import FastAPI from lavepay import LavePay from lavepay.integrations.fastapi import install app = FastAPI() lp = LavePay("LAVE-12-…", webhook_secret="whsec_…") @lp.invoice_paid() async def paid(invoice): if invoice.matches(order_id=invoice.order_id): mark_order_paid(invoice.order_id) # идемпотентно по заказу install(app, lp) # POST /lavepay: подпись, срок и дедупликация — внутри; 200 уходит сразу
import hmac, hashlib, time from fastapi import FastAPI, Request, HTTPException SECRET = "whsec_…" app = FastAPI() @app.post("/lavepay") async def lavepay(request: Request): body = await request.body() ts = request.headers.get("x-lavepay-timestamp", "") sig = request.headers.get("x-lavepay-signature", "") if not ts.isdigit() or abs(time.time() - int(ts)) > 300: raise HTTPException(400, "stale") expected = "v1=" + hmac.new(SECRET.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): raise HTTPException(401, "bad signature") event = await request.json() if event["type"] == "invoice.paid": mark_order_paid(event["data"]["order_id"], event["id"]) # идемпотентно по event id return {"ok": True}
import crypto from "node:crypto"; import express from "express"; const SECRET = "whsec_…"; const app = express(); app.post("/lavepay", express.raw({ type: "application/json" }), (req, res) => { const ts = req.get("x-lavepay-timestamp") ?? ""; const sig = req.get("x-lavepay-signature") ?? ""; if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const expected = "v1=" + crypto.createHmac("sha256", SECRET).update(`${ts}.`).update(req.body).digest("hex"); if (expected.length !== sig.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) { return res.sendStatus(401); } const event = JSON.parse(req.body); if (event.type === "invoice.paid") markOrderPaid(event.data.order_id, event.id); res.sendStatus(200); });
Ответы и повторы
- 2xx — событие доставлено; больше не придёт.
- Любой другой ответ или его отсутствие (таймаут 10 с) — мы повторим.
- Отвечайте быстро: примите событие, поставьте в очередь, верните 200. Обработку делайте после ответа.
- Дедуплицируйте по
idсобытия (evt_…). - Редиректы не выполняются:
3xxсчитается неудачей.
| Ваш ответ | Что делаем |
|---|---|
2xx | доставлено |
нет ответа, таймаут, 408, 425, 5xx | повтор по расписанию: через 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 3 ч, 6 ч — всего 8 попыток, ±20 % джиттера |
429, 503 с Retry-After | ждём столько, сколько сказано (не меньше расписания, не больше часа) |
3xx и прочие 4xx | ошибка маршрута или авторизации на вашей стороне: 3 попытки (хватает пережить деплой), потом событие помечается failed |
410 Gone | endpoint отключается немедленно |
Здоровье endpoint'а
Видно в приложении и в GET /webhook → state:
- healthy — последние события доставлены;
- degraded — были неудачи, повторяем;
- down — 3+ неудачи подряд. Новые события не летят сразу, а становятся в очередь за уже запланированной попыткой. Первый же
2xxсбрасывает счётчик и отправляет всю очередь разом, по порядку; - disabled — 24 часа без единого
2xx(или410): отправка остановлена, владелец получает уведомление. События за это время сохраняются со статусомfailed. Кнопка «Включить и отправить заново» (или смена URL) сбрасывает здоровье и ставит в очередь всё, что накопилось за последние 7 дней.
Смена URL считается новым endpoint'ом: здоровье обнуляется, ожидающие события переезжают на новый адрес.
API вебхуков
/webhook
Состояние endpoint'а: state (off — URL не задан,
healthy, degraded, down, disabled),
url, failure_streak — неудач подряд, failing_since,
last_success_at, last_failure_at, disabled_at и
disabled_reason — почему отправка остановлена:
endpoint answered 410 Gone, no successful delivery in 24 hours
или switched off.
/webhook/test
Отправить webhook.test на ваш URL — подписанный, как любое настоящее событие. Удобно проверить интеграцию до первого счёта.
curl -X POST "https://<домен>/merchant/v1/webhook/test" -H "X-Api-Key: $LAVE_KEY"
ref = await lp.webhooks.test() health = await lp.webhooks.get()
/webhook/deliveries
Журнал отправок: status (pending|delivered|failed), limit, before.
У каждой отправки — число попыток, last_status_code, начало тела ответа
(last_response), время ответа и last_error — причина последней
неудачи общей категорией: timeout, connection failed,
TLS error, invalid response, blocked address,
HTTP <код> (у 3xx — HTTP <код>: redirects are not followed)
или webhook disabled: <причина> — у событий, отложенных выключенным
endpoint'ом. Хостов, адресов и текста ошибок соединения там нет.
/webhook/deliveries/{event_id}/retry
Отправить событие ещё раз на текущий URL.
Справочник
История баланса
/history
Всё, что двигало баланс мерчанта, новое первым: оплаты и возвраты счетов, депозиты на адреса клиентов, переводы, чеки, оплаты QR, выводы на карту, Steam и Telegram, выводы в блокчейн, покупка USDT, вывод на баланс владельца. Одна строка — одна проводка. В приложении то же самое — на вкладке «История» мерчанта.
curl "https://<домен>/merchant/v1/history?limit=50&before=18234" \ -H "X-Api-Key: $LAVE_KEY"
page = await lp.history.list(limit=50) async for item in lp.history.iter(): # все страницы подряд print(item.type, item.amount, item.asset)
{
"items": [
{
"id": "op_5d0c9a1e-…",
"type": "buy_order",
"direction": "in",
"amount": "6.03",
"asset": "USDT",
"balance_after": "123.70",
"object_id": "buy_2c8e61f0-…",
"status": "completed",
"created_at": "2026-10-07T10:24:51+00:00"
},
{
"id": "op_91aa07b3-…",
"type": "card_payout",
"direction": "out",
"amount": "-4.31",
"asset": "USDT",
"balance_after": "117.67",
"object_id": "cp_7f8b89a5-…",
"status": "completed",
"created_at": "2026-10-07T09:58:02+00:00"
}
],
"next_cursor": 18234
}
type | Что это | object_id |
|---|---|---|
invoice | in — оплата счёта (и недоплата бесшовного счёта), out — возврат | inv_… |
deposit | депозит на постоянный адрес клиента | dep_… |
transfer | перевод пользователю | tr_… |
cheque | out — чек создан, in — невостребованное вернулось | chq_… |
qr_payment | out — оплата QR, in — не прошла или магазин вернул | qr_… |
card_payout | out — вывод на карту, in — не прошёл, деньги вернулись | cp_… |
sbp_payout | out — вывод по СБП, in — не прошёл, деньги вернулись | sbp_… |
steam_order | out — пополнение Steam, in — возврат | st_… |
telegram_order | out — Stars или Premium, in — возврат | tg_… |
withdrawal | out — вывод в блокчейн (сумма с комиссией, в монете вывода), in — не прошёл, деньги вернулись | wd_… |
buy_order | покупка USDT за рубли | buy_… |
settlement | вывод на баланс владельца из приложения | — |
other | ручная корректировка | — |
amount — со знаком, в единицах asset (стейбл — USDT):
сумма строк по активу — его баланс, balance_after — остаток после строки.
status — статус объекта сейчас, тот же, что отдаёт его GET: у
выплаты на карту, которая не прошла, и списание, и возврат покажут failed,
у недоплаты бесшовного счёта — статус самого счёта (active, пока он не оплачен).
Пагинация — как везде.
Все эндпоинты
База — https://<домен>/merchant/v1.
Выплаты по API — нужен переключатель.
| Эндпоинт | Что делает |
|---|---|
GET /me | мерчант и его балансы |
GET /history | история баланса |
POST /invoices | выставить счёт; с method — бесшовный |
GET /invoices | список счетов |
GET /invoices/{id} | один счёт |
POST /invoices/{id}/cancel | отменить |
POST /invoices/{id}/refund | вернуть покупателю |
POST /addresses | постоянный адрес клиента |
GET /addresses, /addresses/{id} | адреса |
GET /deposits, /deposits/{id} | депозиты на адреса |
POST /transfers | перевод пользователю Выплаты по API |
POST /cheques | чек по ссылке Выплаты по API |
GET /cheques, /cheques/{id} | чеки |
POST /cheques/{id}/cancel | отозвать чек |
GET /qr/rate | ориентир курса QR |
POST /qr/check | проверить QR, заморозить цену Выплаты по API |
POST /qr/payments | оплатить QR Выплаты по API |
GET /qr/payments, /qr/payments/{id} | оплаты QR; ?wait= ждёт исхода |
GET /card/rate | курс вывода на карту, ?amount_rub= — цена |
POST /card/payouts | вывести на карту Выплаты по API |
GET /card/payouts, /card/payouts/{id} | выплаты на карту |
GET /sbp/banks | банки СБП для вывода |
GET /sbp/rate | курс вывода по СБП, ?amount_rub= — цена |
POST /sbp/payouts | вывести по СБП на телефон Выплаты по API |
GET /sbp/payouts, /sbp/payouts/{id} | переводы по СБП |
GET /steam/rate | курс Steam, ?amount= — оценка |
POST /steam/orders | пополнить Steam Выплаты по API |
GET /steam/orders, /steam/orders/{id} | заказы Steam |
GET /telegram/rate | курс Stars и Premium, ?service=&quantity= — цена |
POST /telegram/orders | купить Stars или Premium Выплаты по API |
GET /telegram/orders, /telegram/orders/{id} | покупки Stars и Premium |
GET /withdrawals/networks | сети, комиссии, минимумы |
GET /withdrawals/quote | цена вывода |
POST /withdrawals | вывести в блокчейн Выплаты по API |
GET /withdrawals, /withdrawals/{id} | выводы |
GET /buy/rate | курс покупки USDT, ?amount_rub= — цена |
POST /buy/orders | купить USDT за рубли |
GET /buy/orders, /buy/orders/{id} | покупки |
POST /buy/orders/{id}/cancel | снять неоплаченную покупку |
GET /webhook | здоровье endpoint'а |
POST /webhook/test | тестовое событие |
GET /webhook/deliveries | журнал отправок |
POST /webhook/deliveries/{event_id}/retry | отправить событие ещё раз |
Изменения
Что появилось в API, по датам выкладки. Старые поля и ответы не меняются — только добавляются.
- Монета ETH по сети Ethereum: оплата счетов, бесшовные счета, адреса клиентов, вывод в блокчейн и чеки.
- USDT по сети Ethereum (ERC-20): сеть
ETH(иERC20,ERC-20,ETHEREUM) в оплате счетов, бесшовных счетах, адресах клиентов и выводе в блокчейн; ход подтверждений —confirmationsиconfirmations_required. - Бесшовный счёт меньше минимального перевода в сети метода не выдаётся:
422 validation_errorсdetails.minimum. - Нативный BNB по сети BSC: оплата счетов, бесшовные счета, адреса клиентов, вывод в блокчейн.
- Бесшовный счёт с найденным переводом (
payment.status: confirming) не истекает по часам. - Вывод в блокчейн с баланса мерчанта:
GET /withdrawals/networks,/withdrawals/quote,POST /withdrawals, вебхукиwithdrawal.*. - Бесшовные счета (
methodвPOST /invoices) и постоянные адреса клиентов:POST /addresses,GET /deposits, вебхукиinvoice.underpaid,deposit.*. - Покупка USDT за рубли:
GET /buy/rate,POST /buy/orders, вебхукиbuy_order.*.GET /history— история баланса. Библиотекаlavepayна PyPI. - Steam, Stars и Premium по API:
/steam/*,/telegram/*. Оплата QR СБП и вывод на карту:/qr/*,/card/*. - Чек — до 100 000 активаций и до 1 000 000 $ на чек.
- Чеки:
POST /cheques, статус и отзыв; чек читается по своемуidempotency_key. - Комиссия с оплаты зависит от способа (
paid_via); на экране оплаты — CryptoBot и xRocket. Бот стал@laveshka. - Первый выпуск: счета, возвраты, переводы, вебхуки с подписью.