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
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 явно.
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:
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:
Response:
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:
Response:
4.3. POST /merchant/api/v1/express/currencies
Возвращает список fiat currencies, поддерживаемых указанным bank.
Decoded payload:
Response:
4.4. POST /merchant/api/v1/express/bank-link
Возвращает schema полей recipient-data, которые нужны для order по bank link. Используйте этот endpoint перед withdrawals, чтобы понять, какие поля, например card number или phone, нужно собрать у end-user.
Decoded payload:
Response:
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:
Example payload before signing:
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:
Response:
4.7. POST /merchant/api/v1/express/withdrawals/detail
Детали одного withdrawal.
Decoded payload:
Response:
5. Withdrawal Status Lifecycle
Merchant-visible status flow для Off-Ramp transaction:usdtTotal и exchangeRate, возвращенные withdrawals, фиксируются при создании transaction и не меняются в течение всего lifecycle transaction.
6. Outbound Webhooks
Платформа уведомляет мерчанта об изменениях статуса через HTTPPOST на webhook URL, настроенный мерчантом. Delivery asynchronous и повторяется при failure.
6.1. Event types
Events
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только после успешного прохождения всех трех проверок.