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 для Off-Ramp 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только после успешного прохождения всех трех проверок.