Приймання й виплати через один узгоджений контракт
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). Секрети керуються в кабінеті мерчанта → «Інтеграція».