Off-Ramp — довідник API для інтеграторів
1. Загальні правила
- Base URL надається платформою.
- Усі endpoint-и Off-Ramp приймають і повертають
application/json. - Усі authenticated endpoint-и використовують метод
POST, включно з read-only операціями на кшталтratesабоwithdrawals/list, тому що тіло запиту має містити підписаний envelope. - Грошові числові значення повертаються як рядки, щоб зберегти точність decimal-значень, наприклад
"25.18"або"39.7059". - Часові мітки повертаються як ISO-8601 UTC рядки, наприклад
"2026-05-04T10:00:00.000Z".
2. Автентифікація та підпис запитів
Кожен authenticated request має бути надісланий як підписаний envelope:data — це base64 від JSON payload. signature — це base64 криптографічного підпису, розрахованого за base64-рядком data. Конкретний алгоритм залежить від типу API-ключа, див. розділ 2.3.
2.1. Обов’язкові headers
| Header | Required | Опис |
|---|---|---|
Content-Type | yes | Має бути application/json. |
x-public-key | conditional | Public key мерчанта у hex форматі. Обов’язковий, якщо public key не передано всередині підписаного payload. Див. розділ 2.2. |
X-Request-Id | optional | UUID запиту, згенерований клієнтом для correlation. Відображається у логах. |
2.2. Де передавати public key — у header чи payload
Сервер визначає public key у такому порядку:- HTTP header
x-public-keyбез урахування регістру. Якщо header присутній, використовується він, а тіло запиту для public key не перевіряється. - Якщо header відсутній, використовується поле
publicKeyіз декодованого JSON payload, тобто всерединіdataпісля base64 decode.
401 Missing public key.
Обидва варіанти допустимі. Оберіть один варіант для кожного запиту. Рекомендований варіант — передавати public key у header: так signed payload містить лише бізнес-поля, а correlation у логах стає простішою.
Header form (recommended):
publicKey разом із DTO-полями:
publicKey має бути доданий у JSON до encoding і signing. Якщо додати publicKey після підписання, verification не пройде.
2.3. Типи ключів
API key мерчанта має одне з двох значеньkeyType. Сервер обирає алгоритм verification за merchant record, пов’язаним із public key. Інтегратор не передає key type явно.
| Key type | Алгоритм | Crypto material в інтегратора |
|---|---|---|
ED25519 | Ed25519, asymmetric | Private key у hex, public key у hex. Public key реєструється на платформі. |
LEGACY | HMAC-style SHA-256 за privateKey + base64Data | Спільний secret, який зберігається як privateKey на обох сторонах. |
ED25519 використовується за замовчуванням для нових ключів. LEGACY підтримується для історичних інтеграцій. Ці два алгоритми дають різні підписи для одного й того самого payload, тому використовуйте обраний алгоритм послідовно.
2.4. Algorithm A — ED25519 (recommended)
Кроки:- Зберіть JSON payload із DTO-полями, див. endpoint sections нижче.
json = JSON.stringify(payload)data = base64(utf8_bytes(json))signatureBytes = ed25519.sign(utf8_bytes(data), privateKeyBytes)— входом є саме base64 string, а не початковий JSON і не raw bytes JSON.signature = base64(signatureBytes)- Надішліть
POST { data, signature }з headerx-public-key: <hex>.
2.5. Algorithm B — LEGACY (SHA-256 with shared secret)
Кроки:- Зберіть JSON payload.
data = base64(utf8_bytes(JSON.stringify(payload)))hexDigest = sha256_hex(privateKey + data)— string concatenation secret і base64 payload.signature = base64(utf8_bytes(hexDigest))— base64-encode застосовується до рядка hex-символів, а не до raw 32-byte digest.- Надішліть
POST { data, signature }з headerx-public-key: <hex>.
2.6. API key permissions and IP allowlist
API key, випущений у merchant portal, містить список permissions. Off-Ramp використовує один із них:POST /merchant/api/v1/express/withdrawalsпотребує увімкнений permission Fiat Withdraw.- Усі інші Off-Ramp endpoint-и потребують лише коректного підпису активним і не revoked ключем.
2.7. Error codes
Error responses використовують числове полеcode. Найважливіші коди для authentication і validation layer:
| HTTP | code | Meaning |
|---|---|---|
| 400 | 2010 | Поле data відсутнє або не є валідним base64-encoded JSON. |
| 400 | 2011 | Поле signature відсутнє. |
| 400 | 1110 | Payload validation failed: поле відсутнє, має неправильний тип або порушує constraint із розділу 4. |
| 401 | 2020 | Signature did not verify. Майже завжди це помилка у signing code інтегратора: неправильне encoding input, неправильний key або payload змінено після signing. |
| 401 | 2021 | Public key blocked. Зверніться до підтримки платформи. |
| 401 | 2022 | Public key expired. Випустіть новий ключ у merchant portal. |
| 401 | 2023 | Public key inactive: revoked або ще не activated. |
| 403 | 4003 | На ключі відсутній permission або source IP не входить до allowlist. |
| 404 | 4004 | Resource not found, наприклад невідомий transactionId або bankLinkCode. |
| 400 | 5001 | rateId не існує. |
| 400 | 5002 | rateId більше не пропонується жодним партнером. |
| 400 | 5003 | recipientData не містить required fields або містить invalid values. |
| 400 | 5004 | Балансу мерчанта недостатньо для покриття usdtTotal. |
| 404 | 5007 | Withdrawal transaction not found. |
| 404 | 5008 | Bank link not found. |
3. Request / Response Envelope
Кожен response від authenticated endpoint-ів повертається у wrapper:200 responses повертають стандартний NestJS error envelope:
4. Endpoint Reference
Усі endpoint-и нижче використовують однакову authentication model: signed{ data, signature } envelope, header x-public-key, Content-Type: application/json. Лише POST /merchant/api/v1/express/withdrawals додатково потребує permission Fiat Withdraw і перевіряється за API-key IP allowlist.
4.1. POST /merchant/api/v1/express/rates
Повертає найкращі доступні merchant-facing rates за bank link.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | no | Зарезервовано для майбутнього amount-aware rate selection. Можна не передавати. |
id — це rateId, який потрібно передати в withdrawals під час створення order. rate — fiat amount за 1 USDT. Для заданого fiatAmount з мерчанта буде списано fiatAmount / rate USDT. Ця сума повертається як usdtTotal із withdrawals.
4.2. POST /merchant/api/v1/express/banks
Повертає список банків, що підтримуються для вказаної fiat currency.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
fiatCurrencyCode | string | yes | Наприклад "UAH". |
4.3. POST /merchant/api/v1/express/currencies
Повертає список fiat currencies, що підтримуються вказаним bank.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
bankCode | string | yes | Наприклад "PRIVAT". |
4.4. POST /merchant/api/v1/express/bank-link
Повертає schema полів recipient-data, потрібних для order за bank link. Використовуйте цей endpoint перед withdrawals, щоб зрозуміти, які поля, наприклад card number або phone, потрібно зібрати в end-user.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
bankLinkCode | string | yes | bankLinkCode, повернутий endpoint-ом rates. |
fieldType може бути TEXT, NUMBER або MASKED. name кожного поля — це key, який інтегратор має використати в recipientData під час виклику withdrawals.
4.5. POST /merchant/api/v1/express/withdrawals
Створює fiat withdrawal. Система блокує USDT на балансі мерчанта та надсилає order P2P-партнеру.
Required permission: Fiat Withdraw. Endpoint перевіряється за API-key IP allowlist.
Decoded payload:
| Field | Type | Required | Validation |
|---|---|---|---|
fiatAmount | number | yes | >= 0.01. |
rateId | string (UUID) | yes | id, повернутий endpoint-ом rates. |
recipientData | object<string,string> | yes | Keys мають збігатися з name, які повернув bank-link. Не має бути порожнім. |
externalId | string | no | Ідентифікатор на стороні мерчанта. Унікальний у межах мерчанта: duplicate values викликають помилку. Корисний для idempotent retries. |
available -= usdtTotal, locked += usdtTotal. Funds are released only when transaction reaches COMPLETED (consumed) or CANCELLED (refunded). Повний balance flow описано в розділі 5.
4.6. POST /merchant/api/v1/express/withdrawals/list
Paginated list Off-Ramp withdrawals мерчанта.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
status | string | no | CREATED, PROCESSING, COMPLETED, CANCELLED, FAILED. |
internalId | string (UUID) | no | Off-Ramp transaction id. |
externalId | string | no | Merchant-supplied identifier. |
page | int | no | Default 1. |
pageSize | int | no | Default 50. |
4.7. POST /merchant/api/v1/express/withdrawals/detail
Деталі одного withdrawal.
Decoded payload:
| Field | Type | Required | Notes |
|---|---|---|---|
transactionId | string (UUID) | yes | transactionId, повернутий endpoint-ом withdrawals або webhook payload. |
5. Withdrawal Status Lifecycle
Merchant-visible status flow для Express transaction:| Status | Meaning | Balance impact |
|---|---|---|
CREATED | Order надіслано partner-у; система очікує acceptance. | usdtTotal locked. |
PROCESSING | Partner прийняв order; система очікує payment confirmation. | Lock unchanged. |
COMPLETED | Partner позначив payout як paid; USDT списано. | locked -= usdtTotal, створено ledger OUT entry. |
CANCELLED | Усі partner attempts вичерпано або order скасовано admin-ом. | Full refund: locked -= usdtTotal, available += usdtTotal. |
FAILED | Critical/unrecoverable error. | Operational follow-up; balance reconciled manually or by replay. |
usdtTotal і exchangeRate, повернуті withdrawals, фіксуються під час створення transaction і не змінюються протягом усього lifecycle transaction.
6. Outbound Webhooks
Платформа повідомляє мерчанта про зміни статусу через HTTPPOST на webhook URL, налаштований мерчантом. Delivery asynchronous і повторюється при failure.
6.1. Event types
| Event | Trigger |
|---|---|
express::withdrawal.created | Order створено та надіслано першому partner-у. |
express::withdrawal.processing | Partner прийняв order. |
express::withdrawal.completed | Partner підтвердив fiat payout; USDT consumed. |
express::withdrawal.cancelled | Усі partner attempts вичерпано або order скасовано admin-ом; full refund applied. |
express::withdrawal.failed | Critical/unrecoverable error. |
express::order.* і express::partner.* є internal і не доставляються на merchant webhooks.
6.2. Webhook payload
6.3. Verifying the webhook
Signature covers payload object без поляsignature, тобто { id, delivered_at, event }, і створюється тим самим алгоритмом, що request signature для merchant key, див. розділ 2.3.
Коректна verification implementation має:
- Перевірити replay protection. Відхиляйте delivery, якщо
idвже був оброблений. Зберігайте processed IDs у database; in-memory set буде втрачено після restart. - Перевірити timestamp. Відхиляйте delivery, якщо
Math.abs(now - delivered_at) > 16 minutes. Вікно 16 хвилин покриває maximum delivery time з урахуванням retries. - Recompute and compare signature за
{ id, delivered_at, event }. - Зберегти
idлише після успішного проходження всіх трьох перевірок.