Назад на главную

BOTONPAY Public API

REST API для приёма платежей. Все запросы должны содержать заголовок Authorization: Bearer <API_KEY>. Ключи начинаются с bp_live_ (production) или bp_test_ (sandbox).

Содержание

Быстрый старт за 5 минут#

Минимальный путь до первой оплаченной сделки. Всё, что нужно — API-ключ и HTTPS-endpoint для вебхуков.

1. Получите test-ключ bp_test_… в личном кабинете мерчанта → раздел API-ключи. Test-ключ создаёт sandbox-сделки и не трогает реальные балансы.

2. Проверьте связь GET /api/public/v1/health должен вернуть HTTP 200.

3. Создайте локальный заказ в своей системе до обращения к BotonPay. Сгенерируйте и сохраните merchant_order_id и Idempotency-Key.

4. Создайте сделку POST /api/public/v1/deals с полями fiat, amount_fiat, merchant_order_id и callback_url.

5. Сохраните ответ HTTP 201 deal_uuid, merchant_order_id, реквизиты (payment_details), payment_url, status и expires_at.

6. Примите webhook deal.created — сразу после создания сделки BotonPay отправляет на callback_url событие deal.created со статусом waiting_payment. Это первый, а не последний webhook.

7. Принимайте последующие события — при каждом изменении статуса приходит отдельный webhook: deal.processing, deal.completed, deal.cancelled, deal.expired, deal.failed.

8. Проверяйте подпись — HMAC-SHA256 по сырому телу запроса, и обрабатывайте повторные доставки идемпотентно по X-Webhook-Id либо по связке deal_uuid + event + status_version.

9. Окончательный успех — только webhook deal.completed со статусом completed. События deal.cancelled, deal.expired, deal.failed — закрывайте локальный заказ как неуспешный.

10. Проверка статуса в любой момент GET /api/public/v1/deals/{deal_uuid} или GET /api/public/v1/deals/by-merchant-order/{merchant_order_id}.

Переход на live — тот же код, ключ bp_live_….

ВАЖНО — локальный заказ создаётся первым

Мерчант обязан создать локальный заказ и сохранить merchant_order_id до вызова POST /deals. Нельзя создавать локальную запись только после получения webhook deal.created — тогда callback-обработчик не найдёт платёж и ответит 404.

  1. Создать локальный payment/order.
  2. Сохранить merchant_order_id.
  3. Сгенерировать Idempotency-Key.
  4. Вызвать POST /deals.
  5. После HTTP 201 сохранить deal_uuid.
  6. Обработать webhook deal.created.

Первый запрос

# 1) проверка доступности API
curl -s https://botonpay.org/api/public/v1/health

# 2) создание сделки на 5000 RUB
curl -X POST https://botonpay.org/api/public/v1/deals \
  -H "Authorization: Bearer bp_test_xxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a" \
  -d '{
    "merchant_order_id": "019fd47b-3e40-746f-b7dd-96aaf5971190",
    "fiat": "RUB",
    "amount_fiat": 5000,
    "callback_url": "https://merchant.example.com/webhooks/botonpay"
  }'

# ответ: HTTP 201 Created
Четыре правила, чтобы не сломать интеграцию:
  • Всегда передавайте Idempotency-Key при создании сделок и выплат.
  • Локальный заказ и merchant_order_id создаются до вызова POST /deals.
  • Источник истины по статусу — вебхук + GET /deals/:id, а не редирект клиента.
  • Суммы в фиате — как есть (не в копейках); суммы в USDT приходят отдельными полями.

Жизненный цикл PayIn-сделки#

Полный контракт: локальный заказ → создание сделки → синхронный ответ HTTP 201 → асинхронные webhook-события.

Локальный заказ (merchant_order_id + Idempotency-Key)
  → POST /api/public/v1/deals
  → HTTP 201 Created   (deal_uuid, реквизиты, payment_url, expires_at)
  → webhook deal.created    / status = waiting_payment
  → webhook deal.processing / status = processing
  → webhook deal.completed  / status = completed        ← финальный успех

Отмена:
  → HTTP 201 → deal.created / waiting_payment → deal.cancelled / cancelled

Истечение:
  → HTTP 201 → deal.created / waiting_payment → deal.expired / expired
  • Webhook не блокирует Create Deal — ответ 201 отдаётся сразу после COMMIT.
  • Callback отправляется отдельным worker.
  • Ошибка callback не откатывает сделку, не меняет её статус и не освобождает реквизит.
  • Каждая версия статуса (status_version) создаёт отдельное событие.

Authentication#

Каждый запрос требует заголовок Authorization: Bearer <API_KEY>. Live-ключи (bp_live_…) работают с боевыми реквизитами; test-ключи (bp_test_…) — с sandbox-сделками, которые не блокируют баланс трейдеров и возвращают is_test: true. Ошибочный или отсутствующий ключ →401 invalid_api_key.

Scopes (права ключа)

Каждому ключу назначается набор прав. При нехватке права API вернёт 403 forbidden с сообщением Missing scope: ….

deals:readЧтение PayIn-сделок
deals:writeСоздание PayIn-сделок
deals:cancelОтмена PayIn-сделок
payouts:readЧтение выплат
payouts:writeСоздание и отмена выплат
webhooks:readЧтение настроек вебхуков
webhooks:writeИзменение настроек вебхуков
webhooks:testОтправка тестового вебхука
rates:readКурсы и список валют

Управление самими API-ключами (/api/public/v1/api-keys) доступно только root-ключу мерчанта.

API Versioning#

Текущая версия — v1. Все эндпоинты работают через префикс /api/public/v1.

Base URL: https://botonpay.org/api/public/v1

Мажорные версии не ломают обратную совместимость внутри себя. Новая мажорная версия (например /v2) будет опубликована отдельно; /v1 продолжит работать.

Supported currencies#

Поддерживаемые фиатные валюты: RUB, AED, TRY,KZT, UZS, KGS, BHD, VND,EGP, BDT, INR, ARS, SAR. Расчёт в USDT. Курс автоматически подтягивается с P2P-агрегаторов.

Для TRY и EGP обязательно передавать client_name — ФИО плательщика. Если сделка создаётся без суммы (payment link), клиент вводит ФИО самостоятельно на странице оплаты. ФИО отображается трейдеру и в карточке сделки для сверки входящего платежа.

Sandbox#

Для интеграции без риска используйте ключи bp_test_…. Sandbox-сделки:

  • не выделяют реквизит и не блокируют баланс трейдера;
  • не смешиваются с live: GET /deals по live-ключу возвращает только live-сделки;
  • возвращают полный набор полей (payment_url, deal_url) для проверки клиента;
  • помечены is_test: true в теле и в webhook-payload.

Rate limits#

500 запросов в минуту на API-ключ. При превышении — HTTP 429 с телом{ code: "rate_limited" } и заголовком Retry-After: 60. Каждый успешный ответ содержит:

  • X-RateLimit-Limit — лимит окна (500);
  • X-RateLimit-Remaining — сколько осталось;
  • X-RateLimit-Reset — ISO-время сброса счётчика.

Idempotency#

Для production-интеграций PayIn заголовок Idempotency-Key обязателен. Один логический заказ = один merchant_order_id = один Idempotency-Key.

Повтор запроса с тем же ключом и тем же телом:

  • не создаёт новую сделку;
  • возвращает тот же deal_uuid;
  • возвращает тот же merchant_order_id;
  • возвращает те же реквизиты;
  • возвращает тот же HTTP-код и то же тело ответа;
  • добавляет заголовок Idempotent-Replay: true (дублируется как Idempotent-Replayed: true).

Повтор с тем же ключом и другим телом — HTTP 409:

{
  "success": false,
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key was already used with different request parameters"
  }
}

Если запрос с тем же ключом ещё выполняется, API дожидается результата первого запроса и возвращает его же. Рекомендуется генерировать UUID v4 и хранить ключ минимум 24 часа.

POST https://botonpay.org/api/public/v1/deals
Authorization: Bearer bp_live_...
Idempotency-Key: 8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a
Content-Type: application/json

{ "merchant_order_id": "ORDER-124", "fiat": "RUB", "amount_fiat": 5000 }

Пример для гео с обязательным ФИО (TRY, EGP):

POST https://botonpay.org/api/public/v1/deals
Authorization: Bearer bp_live_...
Idempotency-Key: 2f3c1a55-6b21-4c6e-9d1f-2b18cf0a71cd
Content-Type: application/json

{ "merchant_order_id": "ORDER-125", "fiat": "TRY", "amount_fiat": 2500, "client_name": "Ahmet Yilmaz" }

// 400 если client_name не передан:
{ "success": false, "error": { "code": "invalid_request",
  "message": "client_name is required for TRY deals" } }

Неизвестный результат Create Deal (timeout, разрыв связи)#

Если вы не получили ответ на POST /deals из-за таймаута, разрыва соединения, 502/503 или сетевой ошибки клиента — не генерируйте новый Idempotency-Key. Сделка могла быть создана.

Правильное действие — одно из двух:

  1. Повторить POST /deals с тем же Idempotency-Key и тем же телом — вернётся исходный результат.
  2. Проверить сделку через GET /api/public/v1/deals/by-merchant-order/{merchant_order_id} или GET /api/public/v1/deals/by-idempotency-key/{key}.
GET https://botonpay.org/api/public/v1/deals/by-idempotency-key/8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a
Authorization: Bearer bp_live_...

// 200 — исходный ответ Create Deal (Idempotent-Replayed: true)
// 202 — запрос ещё обрабатывается:
{ "success": false, "processing": true,
  "error": { "code": "DEAL_CREATION_PROCESSING",
             "message": "Deal creation is still processing. Retry shortly." } }

Не создавайте второй заказ с новым ключом, пока не проверен результат первого запроса.

Deal object#

ПолеТипОписание
iduuidUUID сделки в BotonPay
deal_uuiduuidЯвный синоним id. Используйте его для хранения и сверки
external_idstringВнутренний номер сделки BOTONPAY (D-XXXX)
statusenumСм. таблицу статусов ниже
merchant_order_idstringID заказа в системе мерчанта
client_namestring?ФИО плательщика. Обязательно для TRY, EGP
fiatstringФиатная валюта (ISO 4217)
amount_fiatnumberСумма в фиате
amount_usdtnumberСумма в USDT
ratenumberКурс fiat→USDT на момент сделки
exchange_ratestringЗафиксированный курс (строка, 8 знаков)
gross_amount_usdtstringСумма в USDT до удержания комиссии
merchant_fee_percentstringКомиссия платформы, % (100 − доля мерчанта по ключу)
merchant_fee_usdtstringСумма комиссии в USDT
merchant_amount_usdtstringИтог к зачислению мерчанту в USDT после удержания комиссии
merchant_net_amount_usdtstringСиноним merchant_amount_usdt
calculation_statusenumlocked — расчёт зафиксирован; legacy_unavailable — блок расчёта недоступен (старые сделки)
created_atiso8601Дата создания
expires_atiso8601Дата истечения (обычно +10 мин)
completed_atiso8601?Дата завершения
payment_urlstringСсылка на страницу оплаты для клиента
deal_urlstringСиноним payment_url
requisites_iduuid?ID выделенного реквизита
payment_detailsobject?Реквизиты оплаты (см. ниже)
metadataobjectСвободные поля мерчанта
is_testbooleanSandbox-сделка

payment_details

Возможные поля (набор зависит от страны и метода оплаты, часть может отсутствовать):

  • bank — банк
  • holder — владелец
  • card — номер карты
  • account — номер счёта
  • iban — IBAN
  • phone — номер телефона

Идентификаторы: deal_uuid и merchant_order_id#

В интеграции участвуют два независимых идентификатора. Их нельзя путать и нельзя подставлять один вместо другого.

ПолеКто выдаётНазначение
deal_uuidBotonPayUUID сделки на стороне BotonPay. Уникален глобально. Присутствует во всех webhook-payload
merchant_order_idМерчантID заказа в системе мерчанта. Уникален в рамках API-ключа. Возвращается как есть, без изменений
external_idBotonPayЧеловекочитаемый номер вида D-1042. Только для отображения и поддержки
  • Ищите локальный платёж по merchant_order_id, а deal_uuid храните как ссылку на сделку BotonPay.
  • Не используйте deal_uuid как первичный ключ вашего заказа.
  • merchant_order_id должен быть уникален; повтор с другими параметрами вернёт ошибку.

Deal statuses#

СтатусЗначение
waiting_paymentОжидает оплаты клиентом
processingКлиент отметил оплату, ждём подтверждения трейдера
completedСделка успешно завершена
cancelledСделка отменена
expiredВремя оплаты истекло
failedСделка завершилась ошибкой
disputedОткрыта апелляция, идёт разбор

Финальные статусы: completed, cancelled, expired, failed. После них статус сделки не меняется.

1. Create Deal — POST /api/public/v1/deals#

Создание сделки на приём оплаты. Успешный ответ — HTTP 201 Created.

Гарантия полного ответа. Уже первый успешный ответ HTTP 201 содержит полный объект сделки: deal_uuid, merchant_order_id, status, amount_fiat, currency, requisites_id, requisites(snapshot реквизитов), payment_details, payment_url и expires_at. Ждать webhook deal.created, чтобы получить реквизиты, не нужно — snapshot заморожен в момент создания и не меняется при повторных запросах, GET-сделке и идемпотентных повторах. Если реквизит назначить нельзя — возвращается HTTP 422 no_available_requisites и сделка не создаётся. Полный ответ HTTP 201 с реквизитами гарантированно отдаётся не позднее 3 секунд с момента получения запроса. Если уложиться в этот срок невозможно, сделка не создаётся (а уже созданная — отменяется, webhook deal.created не отправляется) и возвращается HTTP 503 allocation_timeout. Повторите запрос с тем же Idempotency-Key: тот же ключ вернёт ровно этот же сохранённый ответ, пока вы не смените ключ. Если реквизит назначен, но snapshot неполный — возвращается HTTP 500 requisites_snapshot_incomplete, сделка автоматически отменяется, HTTP 201 не отправляется.
ПолеТипОбязательностьОписание
fiatstringобязательноВалюта ISO 4217, напр. RUB
amount_fiatnumberобязательноСумма в фиате, не в копейках
merchant_order_idstringобязательно (production)ID заказа мерчанта, уникален в рамках ключа
callback_urlstring (https)обязательно, если не задан в ключеURL для webhook. Используется буквально, без нормализации
client_namestringусловноФИО плательщика. Обязательно для TRY, EGP
success_urlstringопциональноРедирект клиента после оплаты
cancel_urlstringопциональноРедирект клиента при отмене
customer_idstringопциональноИдентификатор клиента мерчанта
metadataobjectопциональноСвободные поля, возвращаются как есть

Заголовок Idempotency-Key — обязателен для production.

POST https://botonpay.org/api/public/v1/deals
Authorization: Bearer bp_live_...
Idempotency-Key: 8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a
Content-Type: application/json

{
  "merchant_order_id": "ORDER-124",
  "fiat": "RUB",
  "amount_fiat": 5000,
  "callback_url": "https://merchant.com/webhook",
  "success_url": "https://merchant.com/success",
  "cancel_url": "https://merchant.com/cancel",
  "customer_id": "USER-123"
}
Примеры кода
POST https://botonpay.org/api/public/v1/deals
Authorization: Bearer bp_live_...
Idempotency-Key: 8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a
Content-Type: application/json

{
  "merchant_order_id": "ORDER-124",
  "fiat": "RUB",
  "amount_fiat": 5000,
  "callback_url": "https://merchant.com/webhook",
  "success_url": "https://merchant.com/success",
  "cancel_url": "https://merchant.com/cancel",
  "customer_id": "USER-123"
}

Ответ HTTP 201 Created:

{
  "success": true,
  "deal": {
    "id": "b3a1c9e0-1234-4abc-9def-1234567890ab",
    "deal_uuid": "b3a1c9e0-1234-4abc-9def-1234567890ab",
    "external_id": "D-1042",
    "status": "waiting_payment",
    "merchant_order_id": "ORDER-124",
    "fiat": "RUB",
    "amount_fiat": 5000,
    "amount_usdt": 62.81,
    "rate": 79.60,
    "exchange_rate": "79.60000000",
    "exchange_rate_direction": "fiat_to_usdt",
    "gross_amount_usdt": "62.81407035",
    "merchant_fee_percent": "5.0000",
    "merchant_fee_usdt": "3.14070352",
    "merchant_net_amount_usdt": "59.67336683",
    "merchant_amount_usdt": "59.67336683",
    "calculation_status": "locked",
    "created_at": "2026-01-01T00:00:00Z",
    "expires_at": "2026-01-01T00:10:00Z",
    "payment_url": "https://botonpay.org/pay/d/abc123xyz",
    "deal_url": "https://botonpay.org/pay/d/abc123xyz",
    "requisites_id": "9f1e2b45-aaaa-bbbb-cccc-1234567890ab",
    "payment_details": {
      "bank": "T-Bank",
      "holder": "Ivan Ivanov",
      "card": "2200123412341234"
    }
  }
}

Ошибочные ответы:

HTTPКодКогда
400invalid_requestНекорректное тело запроса или отсутствует обязательное поле
401unauthorizedНеверный или отозванный API-ключ
403forbiddenУ ключа нет scope deals:write или запрещён origin/IP
409idempotency_conflictТот же Idempotency-Key с другим телом
422NO_AVAILABLE_REQUISITESНет свободных реквизитов под сумму и валюту
422CURRENCY_DISABLEDВалюта отключена или недоступна ключу
429rate_limitedПревышен лимит запросов, см. Retry-After
503allocation_timeoutНе удалось выдать полный ответ с реквизитами за 3 секунды. Сделка не создана. Повторите с новым Idempotency-Key

Формула расчёта: gross_amount_usdt = amount_fiat / exchange_rate, merchant_fee_usdt = gross_amount_usdt × merchant_fee_percent / 100, merchant_amount_usdt = gross_amount_usdt − merchant_fee_usdt — именно эта сумма зачисляется на баланс мерчанта при завершении сделки. Все значения — строки с 8 знаками после точки.

2. Get Deal — GET /api/public/v1/deals/:id#

iddeal_uuid, external_id или merchant_order_id.

GET https://botonpay.org/api/public/v1/deals/b3a1c9e0-1234-4abc-9def-1234567890ab
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/deals/b3a1c9e0-1234-4abc-9def-1234567890ab
Authorization: Bearer bp_live_...

Ответ:

{
  "success": true,
  "deal": {
    "id": "b3a1c9e0-1234-4abc-9def-1234567890ab",
    "deal_uuid": "b3a1c9e0-1234-4abc-9def-1234567890ab",
    "external_id": "D-1042",
    "status": "completed",
    "merchant_order_id": "ORDER-124",
    "fiat": "RUB",
    "amount_fiat": 5000,
    "amount_usdt": 62.81,
    "gross_amount_usdt": "62.81407035",
    "merchant_fee_percent": "5.0000",
    "merchant_fee_usdt": "3.14070352",
    "merchant_amount_usdt": "59.67336683",
    "calculation_status": "locked",
    "created_at": "...",
    "completed_at": "..."
  }
}

2.1. Lookup — GET /api/public/v1/deals/by-merchant-order/:merchant_order_id#

Поиск сделки по вашему merchant_order_id. Используйте при сверке, восстановлении после таймаута и при получении webhook по неизвестному заказу.

GET https://botonpay.org/api/public/v1/deals/by-merchant-order/ORDER-124
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/deals/by-merchant-order/ORDER-124
Authorization: Bearer bp_live_...

Ответ — тот же объект сделки. 404 с кодом not_found, если заказ не найден по этому API-ключу.

3. List Deals — GET /api/public/v1/deals#

Пагинированный список сделок мерчанта. Фильтры: status,date_from, date_to, merchant_order_id,page, limit (max 100).

GET https://botonpay.org/api/public/v1/deals?status=completed&page=1&limit=20
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/deals?status=completed&page=1&limit=20
Authorization: Bearer bp_live_...

Ответ:

{
  "success": true,
  "data": [ /* Deal[] */ ],
  "page": 1,
  "limit": 20,
  "total": 137,
  "pages": 7,
  "has_next": true,
  "has_prev": false
}

4. Cancel Deal — POST /api/public/v1/deals/:id/cancel#

POST https://botonpay.org/api/public/v1/deals/b3a1c9e0-1234-4abc-9def-1234567890ab/cancel
Authorization: Bearer bp_live_...
Примеры кода
POST https://botonpay.org/api/public/v1/deals/b3a1c9e0-1234-4abc-9def-1234567890ab/cancel
Authorization: Bearer bp_live_...

Ответ:

{ "success": true, "status": "cancelled" }

После отмены отправляется webhook deal.cancelled.

5. Health Check — GET /api/public/v1/health#

Публичный endpoint для мониторинга — не требует API-ключа.

GET https://botonpay.org/api/public/v1/health
Примеры кода
GET https://botonpay.org/api/public/v1/health

Ответ:

{
  "status": "ok",
  "version": "1.3.0",
  "database": "ok",
  "webhooks": "ok",
  "exchange_rate": "ok",
  "webhook_worker": "ok",
  "telegram_webhook": "configured",
  "timestamp": "2026-01-01T00:00:00.000Z"
}

6. Webhooks#

Webhook lifecycle. POST /v1/deals возвращает HTTP 201 сразу после создания сделки и назначения реквизитов — доставка callback выполняется отдельным worker и никогда не задерживает и не отменяет создание сделки. Ошибка вашего endpoint (4xx, 5xx, timeout, DNS/SSL) не откатывает сделку и не меняет её статус.

  • deal.created — сделка создана, статус waiting_payment. Это первый, а не последний webhook.
  • На каждое изменение статуса приходит отдельное событие.
  • deal.completed — окончательное подтверждение успешной оплаты.
  • deal.cancelled, deal.expired, deal.failed — сделка закрыта неуспешно.
  • Отвечайте любым HTTP 2xx (200 / 201 / 202 / 204) — тело ответа не важно и не парсится.
  • Обрабатывайте повторные доставки идемпотентно по X-Webhook-Id либо по связке deal_uuid + event + status_version.
  • Проверяйте подпись X-Signature по сырому телу запроса до JSON.parse.
  • Ищите свой платёж по merchant_order_id, а deal_uuid храните как внешний ID провайдера.
  • Проверить состояние сделки в любой момент: GET /v1/deals/{deal_uuid} или GET /v1/deals/by-merchant-order/{merchant_order_id}.

Куда отправляется callback

Приоритет URL: callback_url из тела запроса → webhook URL, привязанный к API-ключу. Если ни то, ни другое не задано — webhook не отправляется. URL используется буквально: мы не добавляем и не убираем слэш, не меняем регистр, не переписываем путь. Редиректы (301/302/307/308) не выполняются — указывайте конечный URL.

Заголовки запроса

ЗаголовокЗначение
Content-Typeapplication/json
X-SignatureHMAC-SHA256 (hex) по сырому телу запроса
X-Webhook-EventИмя события, напр. deal.created
X-Webhook-IdUUID доставки. Ключ идемпотентности на вашей стороне
X-Webhook-AttemptНомер попытки доставки, начиная с 1
X-Request-IdID запроса для обращений в поддержку

События

СобытиеЗначениеВозможные previous_statusstatus
deal.createdСделка созданаwaiting_payment
deal.processingКлиент отметил оплатуwaiting_paymentprocessing
deal.completedСделка завершенаprocessingcompleted
deal.cancelledСделка отменена (в том числе автоотмена по таймеру: cancellation_reason: "timeout")waiting_payment, processing, assignedcancelled
deal.expiredСделка истеклаwaiting_paymentexpired
deal.failedСделка завершилась ошибкойprocessingfailed
deal.dispute_createdОткрыта апелляцияprocessingdisputed
deal.dispute_resolvedАпелляция закрытаdisputedcompleted, cancelled

Поле event содержит только тип события — без скобок, стрелок и статусов. Переход статусов передаётся отдельными полями: previous_status (статус до изменения) и status (текущий статус сделки). Например: event = deal.cancelled, previous_status = waiting_payment, status = cancelled.

Структура payload

ПолеОписание
eventТип события. Только имя, без транзишена
deal_uuidUUID сделки BotonPay (дублируется как deal_id)
merchant_order_idВаш ID заказа, без изменений
previous_statusСтатус до изменения; null для deal.created
statusТекущий статус сделки
status_versionМонотонная версия статуса. Используйте для упорядочивания и дедупликации
fiat / currencyВалюта сделки
amount_fiat, amount_usdt, rateСумма и курс
merchant_amount_usdtИтог к зачислению мерчанту после комиссии
client_nameФИО плательщика. Всегда заполнено для TRY, EGP
cancelled_at, cancellation_reasonЗаполнены для deal.cancelled / deal.expired
environment, is_testlive или test
metadataВаши свободные поля
event_created_at, timestampМомент формирования события (ISO 8601, UTC)

Пример: deal.created

POST https://merchant.com/webhook
X-Signature: <hmac_sha256_hex>
X-Webhook-Event: deal.created
X-Webhook-Id: 6f0a5e1b-8f2a-4b6d-8f2e-95a1d7c0f3a2
X-Webhook-Attempt: 1
Content-Type: application/json

{
  "event": "deal.created",
  "deal_uuid": "b3a1c9e0-1234-4abc-9def-1234567890ab",
  "deal_id": "b3a1c9e0-1234-4abc-9def-1234567890ab",
  "merchant_order_id": "ORDER-124",
  "previous_status": null,
  "status": "waiting_payment",
  "status_version": 1,
  "fiat": "RUB",
  "amount_fiat": 5000,
  "amount_usdt": 62.81,
  "rate": 79.60,
  "client_name": null,
  "environment": "live",
  "is_test": false,
  "metadata": { "product_id": "sku_42" },
  "event_created_at": "2026-01-01T00:00:00.000Z",
  "timestamp": "2026-01-01T00:00:00.000Z"
}

Пример: deal.cancelled

{
  "event": "deal.cancelled",
  "deal_uuid": "8f1c2f6e-1d5a-4a1b-9f30-3a2f9b7c1e44",
  "deal_id": "8f1c2f6e-1d5a-4a1b-9f30-3a2f9b7c1e44",
  "merchant_order_id": "ORDER-1042",
  "previous_status": "waiting_payment",
  "status": "cancelled",
  "status_version": 2,
  "cancellation_reason": "merchant_cancelled",
  "cancelled_at": "2026-08-06T00:00:00.000Z",
  "event_created_at": "2026-08-06T00:00:00.500Z",
  "timestamp": "2026-08-06T00:00:00.500Z"
}

Финальные события (deal.completed, deal.cancelled, deal.expired, deal.failed) отправляются всегда — независимо от источника отмены (API, панель, трейдер, таймер, апелляция). В payload приходят previous_status, status_version, cancelled_at и cancellation_reason(merchant_cancelled, admin_cancelled, operator_cancelled, trader_cancelled, timeout, dispute_resolved и т.д.). Повторная доставка одного и того же status_version идемпотентна — обрабатывайте её безопасно.

Пример: deal.completed

POST https://merchant.com/webhook
X-Signature: <hmac_sha256_hex>
X-Webhook-Event: deal.completed
Content-Type: application/json

{
  "event": "deal.completed",
  "deal_uuid": "b3a1c9e0-1234-4abc-9def-1234567890ab",
  "deal_id": "b3a1c9e0-1234-4abc-9def-1234567890ab",
  "merchant_order_id": "ORDER-124",
  "previous_status": "processing",
  "status": "completed",
  "status_version": 3,
  "fiat": "RUB",
  "amount_fiat": 5000,
  "amount_usdt": 62.81,
  "rate": 79.60,
  "merchant_amount_usdt": "59.67336683",
  "client_name": null,
  "metadata": { "product_id": "sku_42" },
  "is_test": false,
  "event_created_at": "2026-01-01T00:00:00.000Z",
  "timestamp": "2026-01-01T00:00:00.000Z"
}

Pay-in для TRY и EGP: client_name в payload

Для гео TRY и EGP ФИО плательщика обязательно при создании сделки и всегда приходит в каждом webhook-уведомлении в поле client_name — трейдер сверяет платёж именно по нему. Для остальных валют поле равно null, если ФИО не передавалось.

POST https://merchant.com/webhook
X-Signature: <hmac_sha256_hex>
Content-Type: application/json

{
  "event": "deal.created",
  "deal_uuid": "1c9b7d40-52f1-4d0a-9d75-1f3d0c8a44e1",
  "deal_id": "1c9b7d40-52f1-4d0a-9d75-1f3d0c8a44e1",
  "merchant_order_id": "ORDER-TRY-5501",
  "previous_status": null,
  "status": "waiting_payment",
  "status_version": 1,
  "fiat": "TRY",
  "amount_fiat": 4500,
  "amount_usdt": 129.31,
  "rate": 34.80,
  "client_name": "Mehmet Yilmaz",
  "metadata": {},
  "is_test": false,
  "timestamp": "2026-01-01T00:00:00.000Z"
}
{
  "event": "deal.completed",
  "deal_uuid": "7a2e5f11-6c30-4b8e-a1f9-c0a4b2d8e5f6",
  "deal_id": "7a2e5f11-6c30-4b8e-a1f9-c0a4b2d8e5f6",
  "merchant_order_id": "ORDER-EGP-7712",
  "previous_status": "processing",
  "status": "completed",
  "status_version": 3,
  "fiat": "EGP",
  "amount_fiat": 12000,
  "amount_usdt": 243.90,
  "rate": 49.20,
  "client_name": "Ahmed Hassan Ali",
  "metadata": {},
  "is_test": false,
  "timestamp": "2026-01-01T00:05:00.000Z"
}

Проверка подписи PayIn (Node.js)

PayIn: X-Signature = HMAC-SHA256(raw_body, webhook_secret) в hex. Считайте HMAC по raw body, а не по распарсенному и заново сериализованному JSON — иначе подпись не совпадёт.

const crypto = require("crypto");

// rawBody — оригинальный Buffer/строка тела запроса.
// В Express: app.use(express.json({ verify: (req, _res, buf) => (req.rawBody = buf) }));
const expected = crypto
  .createHmac("sha256", WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");

const signature = req.headers["x-signature"];
if (
  !signature ||
  expected.length !== signature.length ||
  !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
) {
  throw new Error("Invalid signature");
}

PayOut использует собственную схему подписи с меткой времени — см. раздел PayOut webhooks. Секреты PayIn и PayOut различаются, не используйте один для проверки другого.

6.1. Доставка, SLA и retry#

  • Успех доставки = любой HTTP 2xx. Тело ответа игнорируется.
  • Любой другой код, timeout, DNS- или SSL-ошибка = неуспех и постановка в retry.
  • Таймаут ожидания вашего ответа — 10 секунд.
  • Редиректы не выполняются: 301/302/307/308 считаются неуспехом.
  • deal.created отправляется сразу после COMMIT сделки; цель — доставка в пределах 3 секунд с момента создания.

График повторов

СценарийИнтервалы
Быстрый retry (deal.created, а также ответ 404 — трактуется как гонка «мерчант ещё не сохранил заказ»)2 с → 2 с → 5 с → 10 с → 30 с, затем обычный график
Обычный retry (все прочие события и ошибки)1 мин → 5 мин → 15 мин → 1 ч → 6 ч → 24 ч

После исчерпания графика доставка помечается как failed и видна в панели мерчанта. Сделка при этом остаётся в своём фактическом статусе — сверяйте её через GET /v1/deals/by-merchant-order/{merchant_order_id}.

6.2. Webhook troubleshooting#

СимптомПричинаЧто делать
404 Payment not found на deal.createdЛокальный заказ создаётся после ответа API, а webhook пришёл раньшеСоздавайте локальный заказ до POST /deals. Если записи всё же нет — верните 202, поставьте событие во внутреннюю очередь и повторите поиск
Ищем платёж по deal_uuid и не находимПерепутаны идентификаторыИщите по merchant_order_id; deal_uuid — внешний ID провайдера
Подпись не совпадаетHMAC считается по пересериализованному JSON или не тем секретомСчитайте HMAC по raw body; используйте секрет PayIn для PayIn-событий
Webhook не приходит вовсеНе задан callback_url ни в запросе, ни в API-ключеПередайте callback_url (https) или укажите webhook URL в настройках ключа
Доставка помечена неуспешной, хотя обработчик отработалВозвращается редирект или код вне 2xxУказывайте конечный URL без редиректов и отвечайте 200/204
События обработаны в неверном порядкеПараллельная обработка доставокУпорядочивайте по status_version, игнорируйте версии меньше уже применённой
Одно и то же событие пришло дваждыRetry после таймаута на вашей сторонеДедуплицируйте по X-Webhook-Id, отвечайте 200

7. Error codes#

КодHTTPЗначение
invalid_request400Invalid or missing parameters
invalid_api_key401Missing or wrong API key
origin_not_allowed403Origin not in allow-list
forbidden403Action forbidden
not_found404Deal not found
conflict409State conflict (e.g. already completed)
idempotency_conflict409Тот же Idempotency-Key отправлен с другим телом запроса
unprocessable422Business rule rejected the request
NO_AVAILABLE_REQUISITES422Нет свободных реквизитов под сумму и валюту
CURRENCY_DISABLED422Валюта отключена или недоступна для этого ключа
rate_limited429Too many requests
internal_error500Server error
allocation_timeout503Полный ответ не успевал уложиться в 3 с — сделка не создана (или отменена до отправки webhook). Повторите с новым Idempotency-Key; тот же ключ вернёт этот же ответ
DEAL_CREATION_PROCESSING202Создание сделки по этому Idempotency-Key ещё выполняется

Пример ошибки:

{
  "success": false,
  "error": {
    "code": "invalid_api_key",
    "message": "Invalid API key"
  }
}

8. Examples#

cURL

curl -X POST https://botonpay.org/api/public/v1/deals \
  -H "Authorization: Bearer bp_live_..." \
  -H "Idempotency-Key: 8b3f4c02-3f6f-4c88-9e02-0b3e9b3e0e7a" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_order_id": "ORDER-124",
    "fiat": "RUB",
    "amount_fiat": 5000,
    "callback_url": "https://merchant.com/webhook"
  }'

JavaScript (fetch)

const res = await fetch("https://botonpay.org/api/public/v1/deals", {
  method: "POST",
  headers: {
    "Authorization": "Bearer bp_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    merchant_order_id: "ORDER-124",
    fiat: "RUB",
    amount_fiat: 5000,
    callback_url: "https://merchant.com/webhook",
  }),
});
const data = await res.json();
if (!data.success) throw new Error(data.error.message);
console.log(data.deal.payment_url);

Axios

import axios from "axios";
const { data } = await axios.post("https://botonpay.org/api/public/v1/deals", {
  merchant_order_id: "ORDER-124",
  fiat: "RUB",
  amount_fiat: 5000,
}, { headers: { Authorization: "Bearer bp_live_..." } });
console.log(data.deal.payment_url);

Node.js (native fetch)

const res = await fetch("https://botonpay.org/api/public/v1/deals", {
  method: "POST",
  headers: { "Authorization": "Bearer bp_live_...", "Content-Type": "application/json" },
  body: JSON.stringify({ merchant_order_id: "ORDER-124", fiat: "RUB", amount_fiat: 5000 }),
});
console.log(await res.json());

Python (requests)

import requests
r = requests.post(
    "https://botonpay.org/api/public/v1/deals",
    json={"merchant_order_id": "ORDER-124", "fiat": "RUB", "amount_fiat": 5000},
    headers={"Authorization": "Bearer bp_live_..."},
    timeout=30,
)
print(r.json()["deal"]["payment_url"])

PHP (cURL)

<?php
$ch = curl_init("https://botonpay.org/api/public/v1/deals");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer bp_live_...",
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "merchant_order_id" => "ORDER-124",
    "fiat" => "RUB",
    "amount_fiat" => 5000,
  ]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data["deal"]["payment_url"];

Go (net/http)

package main

import (
  "bytes"; "encoding/json"; "net/http"
)

func main() {
  body, _ := json.Marshal(map[string]any{
    "merchant_order_id": "ORDER-124",
    "fiat": "RUB",
    "amount_fiat": 5000,
  })
  req, _ := http.NewRequest("POST", "https://botonpay.org/api/public/v1/deals", bytes.NewReader(body))
  req.Header.Set("Authorization", "Bearer bp_live_...")
  req.Header.Set("Content-Type", "application/json")
  http.DefaultClient.Do(req)
}

C# (HttpClient)

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
  new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", "bp_live_...");
var payload = new StringContent(
  System.Text.Json.JsonSerializer.Serialize(new {
    merchant_order_id = "ORDER-124", fiat = "RUB", amount_fiat = 5000
  }),
  System.Text.Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://botonpay.org/api/public/v1/deals", payload);
Console.WriteLine(await res.Content.ReadAsStringAsync());

8.1. SDK snippets — проверка webhook и массовые выплаты#

Проверка подписи webhook (HMAC-SHA256 по сырому телу запроса, ключ — ваш webhook secret).

// Node.js / Express
import crypto from "crypto";

app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.header("X-Signature") || "";
  const expected = crypto
    .createHmac("sha256", process.env.BOTONPAY_WEBHOOK_SECRET)
    .update(req.body)              // именно сырой Buffer, не JSON.parse
    .digest("hex");

  const a = Buffer.from(signature), b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // event.event: deal.paid | deal.completed | deal.cancelled | payout.completed ...
  res.send("ok");                  // отвечайте 2xx, иначе будет retry
});
# Python / FastAPI
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhook")
async def webhook(request: Request):
    raw = await request.body()
    signature = request.headers.get("X-Signature", "")
    expected = hmac.new(
        os.environ["BOTONPAY_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="invalid signature")
    event = await request.json()
    return {"ok": True}

Массовые выплаты: реестр создаётся последовательными вызовами PayOut API с уникальным merchant_order_id на каждую строку — это гарантирует отсутствие дублей при повторной отправке файла.

# Python — массовая выплата из CSV
import csv, os, requests

API = "https://botonpay.org/api/public/v1/payouts"
HEADERS = {"Authorization": f"Bearer {os.environ['BOTONPAY_API_KEY']}"}

with open("payouts.csv", newline="", encoding="utf-8") as f:
    for i, row in enumerate(csv.DictReader(f), start=1):
        body = {
            "currency": row["currency"],
            "amount": float(row["amount"]),
            "payment_method": row["payment_method"],
            "merchant_order_id": row.get("merchant_order_id") or f"BULK-{i}",
            "recipient": {
                "full_name": row["full_name"],
                "card_number": row.get("card_number") or None,
                "phone": row.get("phone") or None,
                "iban": row.get("iban") or None,
                "bank_name": row.get("bank_name") or None,
            },
        }
        r = requests.post(API, json=body, headers={
            **HEADERS, "Idempotency-Key": body["merchant_order_id"]
        }, timeout=30)
        print(body["merchant_order_id"], r.status_code, r.json())
// Node.js — массовая выплата из массива
const rows = require("./payouts.json");

for (const [i, row] of rows.entries()) {
  const orderId = row.merchant_order_id ?? `BULK-${i + 1}`;
  const res = await fetch("https://botonpay.org/api/public/v1/payouts", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.BOTONPAY_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": orderId,
    },
    body: JSON.stringify({ ...row, merchant_order_id: orderId }),
  });
  console.log(orderId, res.status, await res.json());
}

В личном кабинете тот же реестр можно загрузить файлом: раздел «Массовые выплаты» — шаблон Excel, предпросмотр с валидацией каждой строки и отчёт по результату.

9. Changelog#

v1.3.0 — текущая

  • Create Deal возвращает HTTP 201 Created; контракт полей — fiat и amount_fiat
  • В ответах и webhook-payload явно присутствует deal_uuid отдельно от merchant_order_id
  • Idempotency-Key обязателен для production; повтор возвращает исходный ответ с Idempotent-Replay: true, конфликт — 409 idempotency_conflict
  • Восстановление результата: GET /deals/by-idempotency-key/{key} и GET /deals/by-merchant-order/{id}
  • Webhook deal.created отправляется первым, сразу после создания сделки
  • Заголовки доставки: X-Signature, X-Webhook-Event, X-Webhook-Id, X-Webhook-Attempt, X-Request-Id
  • Payload содержит previous_status, status_version, event_created_at
  • Быстрый retry-график для deal.created и ответов 404: 2с, 2с, 5с, 10с, 30с
  • Разделены схемы подписи PayIn и PayOut

v1.2.0

  • Idempotency-Key на POST-эндпоинтах
  • Health Check /api/public/v1/health
  • Расширенная документация: Deal object, Statuses, Webhook events, Versioning, Changelog
  • List Deals возвращает pages, has_next, has_prev

v1.1.0

  • Sandbox-режим и ключи bp_test_*
  • Rate limits (500 req/min) с заголовками X-RateLimit-*
  • List Deals с фильтрами и пагинацией
  • Единый формат ошибок { success:false, error:{ code, message } }

v1.0.0

  • Create Deal
  • Get Deal
  • Cancel Deal
  • Webhooks (HMAC-SHA256, retry-стратегия)

10. Pay out API#

PayOut API позволяет мерчанту создавать заявки на выплату денежных средств получателям в фиатных валютах.

Base URL: https://botonpay.org/api/public/v1
Авторизация: Authorization: Bearer <API_KEY>
Для POST /payouts дополнительно обязателен уникальный заголовок Idempotency-Key: <UNIQUE_KEY>.

10.1. Объект выплаты#

Поддерживаемые валюты

RUB, AED, TRY, KZT, UZS, KGS, BHD, VND, EGP, BDT, INR, ARS, SAR.

Для каждой валюты в административной панели BotonPay настраиваются: доступность PayOut, минимальная и максимальная суммы, процентная и фиксированная комиссии, доступные способы выплаты, список поддерживаемых банков и время выполнения заявки.

Способы выплаты

Поле payment_method. Поддерживаемые значения:

ЗначениеОписание
cardВыплата на банковскую карту
bank_accountВыплата на банковский счёт
phoneВыплата по номеру телефона
sbpВыплата через СБП
upiВыплата через UPI
ibanВыплата по IBAN
mobile_walletВыплата на мобильный/электронный кошелёк

Для каждой валюты разрешается отдельный набор способов выплаты.

10.2. Статусы#

  • Ожидает обработки: created — создана; pending — ожидает; searching_trader — поиск трейдера.
  • Выполняется: assigned — назначена трейдеру; processing — трейдер выполняет; proof_uploaded — загружено подтверждение.
  • Успех: completed — финальный, выплата успешно выполнена.
  • Неуспешные финальные: cancelled, rejected, failed, expired, refunded. При переходе в эти статусы зарезервированная сумма возвращается на баланс мерчанта.
  • Апелляция: dispute — открыта апелляция, требуется ручное рассмотрение.

10.3. POST /payouts — Создание#

POST https://botonpay.org/api/public/v1/payouts
Authorization: Bearer bp_live_...
Content-Type: application/json
Idempotency-Key: payout-merchant-10001

{
  "merchant_order_id": "PAYOUT-10001",
  "currency": "KZT",
  "amount": 150000,
  "payment_method": "card",
  "bank_code": "kaspi",
  "recipient": {
    "full_name": "IVAN IVANOV",
    "card_number": "4400123412341234",
    "phone": "+77001234567"
  },
  "callback_url": "https://merchant.example.com/webhooks/botonpay",
  "customer_id": "customer-789",
  "metadata": { "user_id": "789" },
  "is_test": false
}
Примеры кода
POST https://botonpay.org/api/public/v1/payouts
Authorization: Bearer bp_live_...
Idempotency-Key: payout-merchant-10001
Content-Type: application/json

{
  "merchant_order_id": "PAYOUT-10001",
  "currency": "KZT",
  "amount": 150000,
  "payment_method": "card",
  "bank_code": "kaspi",
  "recipient": {
    "full_name": "IVAN IVANOV",
    "card_number": "4400123412341234",
    "phone": "+77001234567"
  },
  "callback_url": "https://merchant.example.com/webhooks/botonpay",
  "customer_id": "customer-789",
  "metadata": {
    "user_id": "789"
  },
  "is_test": false
}

Параметры запроса

ПолеТипОбязательноеОписание
merchant_order_idstringДаУникальный ID выплаты на стороне мерчанта
currencystringДаВалюта выплаты
amountnumberДаСумма, которую получит получатель
payment_methodstringДаСпособ выплаты
bank_codestringЗависит от методаКод банка
recipientobjectДаРеквизиты получателя
callback_urlstringНетURL для webhook-уведомлений
customer_idstringНетID клиента на стороне мерчанта
metadataobjectНетДополнительные данные мерчанта
is_testbooleanНетПризнак тестовой выплаты

Объект recipient

Набор обязательных полей зависит от payment_method.

card — обязательные: full_name, card_number.

{
  "full_name": "IVAN IVANOV",
  "card_number": "4400123412341234",
  "phone": "+77001234567"
}

phone — обязательные: full_name, phone.

{ "full_name": "IVAN IVANOV", "phone": "+77001234567" }

sbp — дополнительно в корне запроса требуется bank_code.

{ "full_name": "IVAN IVANOV", "phone": "+79991234567" }

upi

{ "full_name": "RAHUL SHARMA", "upi_id": "rahul@upi" }

iban

{
  "full_name": "JOHN SMITH",
  "iban": "GE29NB0000000101904917",
  "bank_name": "Bank of Georgia",
  "swift": "BAGAGE22"
}

Успешный ответ: HTTP 201 Created

{
  "success": true,
  "payout": {
    "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
    "merchant_order_id": "PAYOUT-10001",
    "status": "pending",
    "currency": "KZT",
    "amount": 150000,
    "fee": 3000,
    "total_debit": 153000,
    "payment_method": "card",
    "bank_code": "kaspi",
    "recipient": { "full_name": "IVAN IVANOV", "card_number_masked": "440012******1234" },
    "expires_at": "2026-07-16T10:30:00.000Z",
    "is_test": false
  }
}

amount — сумма получателю; fee — комиссия BotonPay; total_debit = amount + fee — общая сумма резервирования; expires_at — срок выполнения выплаты.

Идемпотентность

Для каждого запроса создания выплаты необходимо передавать уникальный Idempotency-Key. Если запрос с таким ключом уже был успешно обработан — новая выплата не создаётся, API возвращает первоначальный результат. Если тот же ключ передан с другим телом запроса:

{
  "success": false,
  "error": {
    "code": "idempotency_conflict",
    "message": "Idempotency-Key was already used with different request parameters"
  }
}

merchant_order_id также должен быть уникальным в рамках мерчанта.

10.4. GET /payouts/:id#

GET https://botonpay.org/api/public/v1/payouts/927f5c36-57bd-4f90-a817-a1b5b67af421
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/payouts/927f5c36-57bd-4f90-a817-a1b5b67af421
Authorization: Bearer bp_live_...
{
  "success": true,
  "payout": {
    "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
    "merchant_order_id": "PAYOUT-10001",
    "status": "processing",
    "currency": "KZT",
    "amount": 150000,
    "fee": 3000,
    "total_debit": 153000,
    "payment_method": "card",
    "bank_code": "kaspi",
    "recipient": { "full_name": "IVAN IVANOV", "card_number_masked": "440012******1234" },
    "customer_id": "customer-789",
    "created_at": "2026-07-16T10:00:00.000Z",
    "updated_at": "2026-07-16T10:05:00.000Z",
    "expires_at": "2026-07-16T10:30:00.000Z",
    "is_test": false
  }
}

В ответах API полный номер карты не возвращается — только маскированные реквизиты.

10.5. GET /payouts/by-merchant-order/:id#

GET https://botonpay.org/api/public/v1/payouts/by-merchant-order/PAYOUT-10001
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/payouts/by-merchant-order/PAYOUT-10001
Authorization: Bearer bp_live_...

Ответ аналогичен запросу по системному ID.

10.6. GET /payouts — список с фильтрами#

Параметры запроса

ПараметрОписание
statusФильтр по статусу
currencyФильтр по валюте
customer_idФильтр по ID клиента
merchant_order_idПоиск по ID мерчанта
date_fromНачальная дата
date_toКонечная дата
pageНомер страницы
per_pageКоличество записей на странице
GET https://botonpay.org/api/public/v1/payouts?status=completed&currency=KZT&page=1&per_page=20
Authorization: Bearer bp_live_...
Примеры кода
GET https://botonpay.org/api/public/v1/payouts?status=completed&currency=KZT&page=1&per_page=20
Authorization: Bearer bp_live_...
{
  "success": true,
  "data": [
    {
      "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
      "merchant_order_id": "PAYOUT-10001",
      "status": "completed",
      "currency": "KZT",
      "amount": 150000,
      "fee": 3000,
      "total_debit": 153000,
      "payment_method": "card",
      "created_at": "2026-07-16T10:00:00.000Z",
      "completed_at": "2026-07-16T10:10:00.000Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 20, "total": 1, "total_pages": 1 }
}

10.7. POST /payouts/:id/cancel#

POST /payouts/{id}/cancel

Отмена доступна только в статусах created, pending, searching_trader. После назначения трейдера или начала выполнения отмена через публичный API недоступна.

POST https://botonpay.org/api/public/v1/payouts/927f5c36-57bd-4f90-a817-a1b5b67af421/cancel
Authorization: Bearer bp_live_...
Content-Type: application/json

{ "reason": "customer_cancelled" }
Примеры кода
POST https://botonpay.org/api/public/v1/payouts/927f5c36-57bd-4f90-a817-a1b5b67af421/cancel
Authorization: Bearer bp_live_...
Content-Type: application/json

{
  "reason": "customer_cancelled"
}

Допустимые причины:

customer_cancelled · duplicate · incorrect_details · incorrect_amount · merchant_request · other

{
  "success": true,
  "payout": {
    "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
    "merchant_order_id": "PAYOUT-10001",
    "status": "cancelled",
    "cancel_reason": "customer_cancelled",
    "cancelled_at": "2026-07-16T10:03:00.000Z"
  }
}

Если отмена невозможна:

{
  "success": false,
  "error": {
    "code": "conflict",
    "message": "Payout is already being processed",
    "details": {
      "code": "payout_cannot_be_cancelled",
      "status": "processing"
    }
  }
}

10.8. Webhooks, ошибки и примеры#

Sandbox

Sandbox используется для тестирования интеграции без проведения реальных выплат. Тестовый режим активируется API-ключом с префиксом bp_test_или передачей "is_test": true. В sandbox: реальный баланс не резервируется, заявка не передаётся реальному трейдеру, банковская операция не выполняется; формат API-ответов и webhook-уведомлений соответствует production.

Симуляция статуса (только для тестовых выплат и ключей bp_test_*):

POST https://botonpay.org/api/public/v1/sandbox/payouts/{id}/status
Authorization: Bearer bp_test_...
Content-Type: application/json

{ "status": "completed" }
{
  "success": true,
  "payout": {
    "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
    "status": "completed",
    "is_test": true
  }
}

Webhooks

BotonPay отправляет POST на callback_url при каждом изменении статуса.

Content-Type: application/json
X-BotonPay-Timestamp: 1784196600
X-BotonPay-Signature: sha256=...
X-BotonPay-Event: payout.completed
X-BotonPay-Event-Id: evt_18e2f0a0a8

Webhook-секрет уникален для каждого мерчанта и доступен в разделе PayOut → Настройки.

События:

  • payout.created, payout.pending, payout.assigned
  • payout.processing, payout.proof_uploaded
  • payout.completed, payout.cancelled, payout.rejected
  • payout.failed, payout.expired, payout.refunded
  • payout.dispute_opened, payout.amount_changed
{
  "event": "payout.completed",
  "event_id": "evt_18e2f0a0a8",
  "created_at": "2026-07-16T10:10:00.000Z",
  "payout": {
    "id": "927f5c36-57bd-4f90-a817-a1b5b67af421",
    "merchant_order_id": "PAYOUT-10001",
    "status": "completed",
    "amount": 150000,
    "fee": 3000,
    "total_debit": 153000,
    "currency": "KZT",
    "completed_at": "2026-07-16T10:10:00.000Z"
  }
}

Проверка подписи

Алгоритм: HMAC-SHA256. Строка для подписи — timestamp + "." + raw_body.

signature = HMAC_SHA256(webhook_secret, timestamp + "." + raw_body)
X-BotonPay-Signature: sha256=<SIGNATURE>

Важно использовать исходное тело HTTP-запроса без повторной сериализации JSON. Мерчанту рекомендуется проверять: (1) X-BotonPay-Signature, (2) X-BotonPay-Timestamp, (3) уникальность X-BotonPay-Event-Id, (4) соответствие merchant_order_id, (5) актуальный статус через GET /payouts/{id} для критически важных операций.

Повторная отправка

Если сервер мерчанта не вернул HTTP 2xx, BotonPay повторяет отправку по графику: 1 мин → 5 мин → 15 мин → 1 ч → 4 ч → 12 ч → 24 ч. Максимум 8 попыток. Успехом считается любой HTTP 200–299. Обработка webhook на стороне мерчанта должна быть идемпотентной по event_id.

Комиссия и баланс

Комиссия состоит из процентной и фиксированной частей:

fee = amount × percentage + fixed_fee
total_debit = amount + fee

// Пример:
amount       = 150000 KZT
percentage   = 2%
fixed_fee    = 0 KZT
fee          = 3000 KZT
total_debit  = 153000 KZT

При создании PayOut total_debit резервируется на балансе мерчанта в соответствующей валюте. При completed — окончательно списывается, при cancelled/rejected/failed/expired/refunded — возвращается.

При недостаточном балансе:

{
  "success": false,
  "error": {
    "code": "insufficient_balance",
    "message": "Insufficient merchant balance",
    "details": { "currency": "KZT", "required": 153000, "available": 120000 }
  }
}

Формат ошибок

{
  "success": false,
  "error": {
    "code": "validation_error",
    "message": "Invalid request parameters",
    "details": { "field": "recipient.card_number", "reason": "invalid_card_number" }
  }
}

Коды ошибок

codeHTTPОписание
unauthorized401Неверный API-ключ
forbidden403Нет доступа к PayOut
validation_error422Ошибка валидации
unsupported_currency422Валюта не поддерживается
unsupported_payment_method422Метод не поддерживается
invalid_recipient_details422Неверные реквизиты
amount_below_minimum422Сумма ниже минимальной
amount_above_maximum422Сумма выше максимальной
insufficient_balance409Недостаточно средств
duplicate_merchant_order_id409Дублирующий ID
idempotency_conflict409Конфликт ключа идемпотентности
payout_not_found404Выплата не найдена
payout_cannot_be_cancelled409Нельзя отменить
rate_limit_exceeded429Превышен лимит
internal_error500Внутренняя ошибка сервера
service_unavailable503Сервис PayOut временно недоступен

Пример cURL

curl -X POST "https://botonpay.org/api/public/v1/payouts" \
  -H "Authorization: Bearer bp_test_xxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payout-test-10001" \
  -d '{
    "merchant_order_id": "PAYOUT-10001",
    "currency": "KZT",
    "amount": 150000,
    "payment_method": "card",
    "bank_code": "kaspi",
    "recipient": {
      "full_name": "IVAN IVANOV",
      "card_number": "4400123412341234"
    },
    "callback_url": "https://merchant.example.com/webhooks/botonpay",
    "is_test": true
  }'

Пример JavaScript

const response = await fetch(
  "https://botonpay.org/api/public/v1/payouts",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer bp_test_xxxxxxxxx",
      "Content-Type": "application/json",
      "Idempotency-Key": "payout-test-10001",
    },
    body: JSON.stringify({
      merchant_order_id: "PAYOUT-10001",
      currency: "KZT",
      amount: 150000,
      payment_method: "card",
      bank_code: "kaspi",
      recipient: {
        full_name: "IVAN IVANOV",
        card_number: "4400123412341234",
      },
      callback_url: "https://merchant.example.com/webhooks/botonpay",
      is_test: true,
    }),
  }
);

const result = await response.json();
if (!response.ok) {
  throw new Error(result?.error?.message || "Failed to create PayOut");
}
console.log(result.payout);

Рекомендуемый порядок интеграции

  1. Получить тестовый API-ключ bp_test_*.
  2. Настроить callback_url.
  3. Получить webhook secret.
  4. Реализовать проверку HMAC-подписи.
  5. Создать тестовый PayOut через POST /payouts.
  6. Сохранить системный payout.id.
  7. Симулировать статус через sandbox endpoint.
  8. Проверить получение webhook.
  9. Проверить повторную обработку одинакового event_id.
  10. После успешного тестирования получить production-ключ bp_live_*.

Основной сценарий

created
  → pending
  → searching_trader
  → assigned
  → processing
  → proof_uploaded
  → completed

// Отмена до назначения трейдера:
created / pending / searching_trader → cancelled

// С апелляцией:
processing / proof_uploaded → dispute → completed / rejected / refunded