Приём и выплаты через один согласованный контракт
P2P-платёжная система: создавайте заявки на приём и выплаты, сверяйте статусы, получайте вебхуки с единым каноническим форматом. Все суммы — в обычных единицах, 2 знака после запятой.
Обзор
Каждый запрос подписывается и идёт на версионированный путь /api/v1/…. Версия живёт в пути: когда появится v2, ваша интеграция на v1 продолжит работать без изменений. Внутри версии мы лишь добавляем поля — никогда не убираем и не переименовываем.
| Метод | Путь | Назначение |
|---|---|---|
| POST | /orders | Создать заявку на приём, получить реквизит для плательщика |
| GET | /orders/{uuid} | Текущее состояние заявки |
| POST | /payouts | Создать выплату на карту получателя |
| GET | /payouts/{uuid} | Текущее состояние выплаты |
| POST | /payouts/{uuid}/cancel | Отменить выплату, которую ещё не взял трейдер |
| GET | /balance | Баланс и оборот в разрезе валют |
Аутентификация & подпись
К каждому запросу добавьте два заголовка. Порядок проверки на сервере: лимит частоты → целостность тела → ключ → подпись, поэтому ошибка называет именно свою причину.
| Заголовок | Значение |
|---|---|
| X-API-Key | Ваш api_key (публичный идентификатор мерчанта) |
| X-Signature | HMAC-SHA256(<сырое тело запроса>, api_secret) в hex |
| Content-Type | application/json |
Подписываются точные байты тела, которые вы отправляете — сериализуйте JSON один раз и подпишите именно эту строку. У GET-запросов тело пустое, поэтому X-Signature — это HMAC пустой строки с api_secret.
api_secret подписывает ваши запросы к нам. webhook_secret нужен, чтобы вы проверяли наши колбеки. Проверка колбека не тем секретом — самая дорогая ошибка интеграции.
# тело, которое отправляем
BODY='{"order_id":"A-1001","amount":1500.00,"currency":"UAH"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.*= //')
curl -X POST https://yuppipay.net/api/v1/orders \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Signature: $SIG" \
-d "$BODY"
Дополнительно: если в профиле задан белый список IP, запросы принимаются только с него (проверяется REMOTE_ADDR, X-Forwarded-For игнорируется).
Идемпотентность
Заявки и выплаты идемпотентны по паре (мерчант, order_id). Точный повтор того же запроса возвращает оригинальный объект (не создаёт дубль). Тот же order_id с изменённым телом (сумма, валюта, плательщик / карта получателя) — это конфликт 409 idempotency_conflict, в ответе будет существующий объект.
Ставьте собственный order_id и безопасно ретрайте сетевые сбои тем же запросом — дубля не будет. callback_url не входит в отпечаток: сменить адрес доставки и повторить — это та же заявка.
Заявки на приём
Создаёт заявку payin и возвращает реквизит (карту), который показываете плательщику. Комиссия удерживается из суммы (inclusive): мерчанту зачисляется amount − commission.
| Поле | Тип | Описание | |
|---|---|---|---|
| order_id | string | required | Ваш внешний номер. Ключ идемпотентности. До 255 символов. |
| amount | number | required | Сумма в обычных единицах, ≥ 0.01, 2 знака. |
| currency | string(3) | required | ISO-код валюты. Должна быть активна и включена для приёма. |
| payer_id | string | optional | Идентификатор плательщика. Участвует в роутинге и идемпотентности. |
| payer_bank | string | optional | Банк плательщика. Используется только когда включено правило совпадения банка. |
| callback_url | string(url) | optional | Адрес колбека для этой заявки. Пусто → адрес из кабинета. |
{
"order_id": "A-1001",
"amount": 1500.00,
"currency": "UAH",
"payer_id": "user-42"
}
{
"id": "7af7c1df-0629-42d4-9068-df6500b3acd6",
"order_id": "A-1001",
"status": "awaiting_payment",
"amount": 1500.00,
"amount_settled": null,
"commission": 30.00,
"currency": "UAH",
"exchange_rate": 45.58,
"settlement_currency": "USDT",
"payer_id": "user-42",
"requisite": {
"card_number": "5375 4141 0000 1234",
"card_holder": "ПЕТРЕНКО ІВАН",
"bank": "PrivatBank",
"type": "card"
},
"created_at": "2026-08-09T10:00:00+00:00",
"expires_at": "2026-08-09T10:15:00+00:00",
"completed_at": null
}
Показывайте requisite.card_number плательщику; ждите вебхук order.completed (не поллинг). Поле id — это {uuid} для запроса статуса.
Возвращает текущее состояние заявки (тот же объект, что и POST /orders). Заявка другого мерчанта невидима даже по известному UUID.
| awaiting_payment | Реквизит выдан, ждём оплату плательщика |
| processing | Оплата видна, идёт сверка зачисления |
| completed | Успешно зачислено — вебхук order.completed |
| expired | Не оплачено вовремя — вебхук order.expired |
| cancelled | Отменена оператором — вебхук order.cancelled |
| disputed | Открыт спор; решение закроет заявку — вебхук order.disputed |
Снять заявку, которую ещё не оплатили: плательщик передумал или оператор закрывает её с вашей стороны. Карта трейдера сразу освобождается под следующую заявку. В ответ приходит тот же объект заявки со статусом cancelled, а на ваш callback_url идёт событие order.cancelled.
Тело не обязательно. Можно передать reason — причина попадёт в вебхук, в журнал и в карточку заявки, где её видит оператор платёжной системы, поэтому пишите понятный текст, а не служебный код; пустое тело {} тоже принимается, тогда запишется «Cancelled by merchant». Подписывайте ровно ту строку, которую отправляете.
Отменить можно только неоплаченную заявку — статусы pending и awaiting_payment. Как только плательщик перевёл деньги (processing) или открыт спор, отмена через API уже не работает: средства могли поступить на карту трейдера, и такой случай закрывает поддержка. Ответ — 409 order_not_cancellable и текущее состояние заявки в поле order.
Выплаты
Создаёт выплату на карту получателя. Комиссия добавляется сверху (on-top): с мерчанта списывается amount + commission = amount_charged.
| Поле | Тип | Описание | |
|---|---|---|---|
| order_id | string | required | Ваш внешний номер выплаты. Ключ идемпотентности. |
| amount | number | required | Сумма перевода, ≥ 0.01, 2 знака. |
| currency | string(3) | required | ISO-код. Активна и включена для выплат. |
| recipient_card | string | required | Номер карты получателя (до 32). |
| recipient_name | string | optional | Имя получателя. Участвует в идемпотентности. |
| recipient_bank | string | optional | Банк получателя. |
| callback_url | string(url) | optional | Адрес колбека для этой выплаты. |
{
"id": "b1e2c3d4-5678-90ab-cdef-1234567890ab",
"order_id": "P-2001",
"status": "pending",
"amount": 1000.00,
"commission": 15.00,
"amount_charged": 1015.00,
"currency": "UAH",
"exchange_rate": 45.58,
"settlement_currency": "USDT",
"recipient": { "card_number": "4149 •••• •••• 3179", "name": "IVAN P.", "bank": "A-Bank" },
"receipt_attached": false,
"created_at": "2026-08-09T10:00:00+00:00",
"expires_at": "2026-08-09T14:00:00+00:00",
"completed_at": null
}
Состояние выплаты (тот же объект, что и при создании), scoped к вашему мерчанту.
| pending | В общем пуле, ждёт трейдера |
| assigned / processing | Взято в работу — вебхук payout.processing |
| completed | Выполнено — вебхук payout.completed, есть tx_receipt |
| expired | Срок исчерпан |
| cancelled | Отменено |
| disputed | Открыт спор |
Отозвать выплату, пока её никто не взял. Тело пустое — отправьте {} и подпишите ровно эти два байта. В ответ приходит тот же объект выплаты со статусом cancelled, а на ваш callback_url уходит событие payout.cancelled.
Как только выплату взял трейдер, отменить её через API уже нельзя: перевод мог начаться. Такой случай закрывает поддержка — ответ 409 payout_not_cancellable и текущее состояние выплаты в поле payout.
Баланс
Баланс и оборот в разрезе валют, считаются так же, как в кабинете: зачислено − выдано − холд под незавершённые выплаты − заявки на вывод + правки (заявки — в резервирующих статусах; правки — ручные корректировки администратора, поле adjustments).
{
"merchant_id": 1,
"balances": [
{
"currency": "UAH",
"balance": 598229.84,
"settled": 6449109.84,
"paid_out": 5849865.00,
"held": 1015.00,
"withdrawn": 0.00,
"adjustments": 0.00,
"turnover": 7010100.00,
"commission": 560792.16,
"orders": 438,
"payouts": 56,
"withdrawals": 0
}
],
"generated_at": "2026-08-09T10:15:00+00:00"
}
Созданная выплата сразу откладывает сумму с комиссией: она уходит в held и уменьшает balance. Выполнение переносит её в paid_out, отмена или протухание возвращает в доступные.
Вебхуки
Мы отправляем POST на ваш адрес доставки на ключевых переходах — завершение и просрочка заявки, коррекция суммы, и изменения выплат — а не буквально на каждое изменение статуса. Отвечайте 2xx быстро; тело обрабатывайте идемпотентно.
| Content-Type | application/json |
| X-Event | Тип события, напр. order.completed — можно роутить обработчики, не разбирая тело |
| X-Signature | HMAC-SHA256(<сырое тело>, webhook_secret) — сверяйте до обработки |
Считайте HMAC над сырыми байтами тела (до JSON-парсинга) и секретом webhook_secret — не api_secret. Сравнивайте константным временем.
События заявок
Канонический payload одинаков для всех событий order.*: неизменный порядок и названия полей.
| Событие | Когда отправляется |
|---|---|
| order.completed | Заявка успешно закрыта — зачисляйте как депозит |
| order.amount_corrected | Сумму уже завершённой заявки исправили. Приходит вместо повторного order.completed |
| order.expired | Заявку не оплатили вовремя |
| order.cancelled | Заявку отменил оператор, холд возвращён. Дополнительное поле: reason |
| order.disputed | По заявке открыт спор, финальный статус ещё не определён. Дополнительное поле: dispute_id |
Ручной повтор из админки отправляет тот же payload с дополнительным флагом "resent": true.
{
"event": "order.completed",
"order_id": "7af7c1df-0629-42d4-9068-df6500b3acd6", // наш uuid
"merchant_order_id": "A-1001", // ваш номер
"status": "completed",
"currency": "UAH",
"amount": 1500.00, // заявленная (не меняется)
"amount_received": 1500.00, // фактически полученная
"amount_settled": 1470.00, // к зачислению (минус комиссия)
"exchange_rate": 45.58,
"settlement_currency": "USDT"
}
Зачисляйте игроку/клиенту по amount_received (фактически полученная), а amount — это первоначальная заявка, она никогда не меняется. amount_settled — то, что ляжет на ваш баланс после комиссии.
{
"event": "order.amount_corrected",
"order_id": "7af7c1df-0629-42d4-9068-df6500b3acd6",
"merchant_order_id": "A-1001",
"status": "completed",
"currency": "UAH",
"amount": 1500.00,
"amount_received": 1400.00,
"amount_settled": 1372.00,
"exchange_rate": 45.58,
"settlement_currency": "USDT",
"correction_id": 42,
"reason": "partial_payment",
"amount_original": 1500.00, // сумма до коррекции
"amount_corrected": 1400.00 // новая сумма
}
Если сумму исправили после того, как заявка уже была completed (и вы уже получили order.completed), повторный order.completed привёл бы к двойному зачислению. Поэтому приходит order.amount_corrected — обновите сумму, не зачисляйте повторно.
События выплат
| Событие | Когда |
|---|---|
| payout.processing | Выплату взяли в работу |
| payout.completed | Выплата выполнена (есть tx_receipt) |
| payout.expired | Срок выплаты исчерпан |
| payout.cancelled | Выплата отменена |
| payout.disputed | По выплате открыт спор |
{
"event": "payout.completed",
"payout_id": "b1e2c3d4-5678-90ab-cdef-1234567890ab", // наш uuid
"order_id": "P-2001", // ВАШ внешний номер
"amount": 1000.00,
"amount_charged": 1015.00, // списано с мерчанта
"currency": "UAH",
"exchange_rate": 45.58,
"settlement_currency": "USDT",
"status": "completed",
"tx_receipt": "https://.../receipt.jpg",
"completed_at": "2026-08-09T10:15:00+00:00"
}
exchange_rate - курс settlement_currency к валюте документа, зафиксированный в момент создания заявки или выплаты и дальше неизменный. По нему считайте свой средний курс. У старых документов без курса поле отдаётся как null.
В payout-событиях order_id — это ваш внешний номер, а uuid выплаты лежит в payout_id. В order-событиях наоборот: order_id — это наш uuid, а ваш номер — merchant_order_id. Маппьте аккуратно.
Доставка & ретраи
Доставка асинхронная через очередь. Успех — любой 2xx. Не-2xx или таймаут (connect 5с / read 15с) планирует повтор с нарастающей задержкой.
| Попытка | 1 | 2 | 3 | 4 | 5+ |
|---|---|---|---|---|---|
| Задержка | 30с | 60с | 5 мин | 15 мин | 1 ч |
До 5 попыток; после исчерпания колбек помечается failed. Обрабатывайте идемпотентно по order_id/payout_id — тот же колбек может прийти повторно.
Коды ошибок
Все ошибки — JSON с полем error (машинный код) и message. Делайте switch по error, не по тексту.
| HTTP | error | Когда |
|---|---|---|
| 400 | empty_body | Пустое тело в POST |
| 400 | malformed_json | Тело — не валидный JSON |
| 400 | content_type_missing | Не отправлен Content-Type: application/json |
| 401 | api_key_missing | Нет заголовка X-API-Key |
| 401 | api_key_invalid | Ключ не совпадает ни с одним мерчантом |
| 401 | signature_missing | Нет заголовка X-Signature |
| 401 | signature_invalid | Подпись не совпала с HMAC тела |
| 403 | merchant_suspended | Мерчант приостановлен/заблокирован |
| 403 | ip_not_allowed | IP не в белом списке |
| 404 | order_not_found | Заявка не найдена (или чужая) |
| 404 | payout_not_found | Выплата не найдена (или чужая) |
| 404 | unsupported_api_version | Путь без версии или неизвестная версия |
| 404 | endpoint_not_found | Версия есть, пути нет (в ответе — список доступных) |
| 405 | method_not_allowed | Путь есть, но не для этого метода |
| 409 | idempotency_conflict | Тот же order_id с изменённым телом |
| 422 | validation_failed | Поля не прошли валидацию (детали — в details) |
| 422 | amount_out_of_range | Сумма вне min/max (есть в теле) |
| 422 | currency_not_supported | Валюта не активна на площадке |
| 422 | currency_not_enabled | Приём этой валюты не включён для мерчанта |
| 422 | payout_not_enabled | Выплаты этой валюты не включены |
| 422 | insufficient_balance | Баланс мерчанта вместе с лимитом минуса не покрывает сумму выплаты с комиссией. В теле - required, balance, overdraft_limit |
| 429 | merchant_limit | Дневной лимит суммы или количества |
| 429 | — | Лимит частоты: 600 запросов в минуту на ключ. Ответ стандартный для сервера и поля error не содержит — проверяйте код 429 до разбора тела. Заголовок Retry-After указывает, сколько ждать |
| 503 | no_requisite_available | Нет свободного реквизита (есть Retry-After: 5) |
| 503 | rate_unavailable | Для валюты не настроен курс - заявка/выплата не создана |
| 503 | accepting_suspended | Kill-switch блокирует приём |
| 503 | payouts_suspended | Kill-switch блокирует выплаты |
YuppiPay · Merchant API v1 · все суммы в обычных единицах (2 знака), время в ISO 8601 (UTC). Секреты управляются в кабинете мерчанта → «Интеграция».