Off-Ramp — Integrator API Reference
1. Conventions
- Base URL: provided by the platform.
- All Off-Ramp endpoints accept and return
application/json. - All authenticated endpoints use
POST, including read-only operations such asratesorwithdrawals/list, because the request body must carry a signed envelope. - Numeric monetary values are returned as strings to preserve decimal precision, for example
"25.18"or"39.7059". - Timestamps are ISO-8601 UTC strings, for example
"2026-05-04T10:00:00.000Z".
2. Authentication and Request Signing
Every authenticated request must be sent as a signed envelope:data is the base64 representation of the JSON payload. signature is the base64 representation of the cryptographic signature computed over the base64 data string. The exact algorithm depends on the API key type, see section 2.3.
2.1. Required Headers
2.2. Where to Put the Public Key — Header or Payload
The server resolves the public key in this order:- HTTP header
x-public-key, case-insensitive. If the header is present, this value is used and the body is not consulted for the public key. - Otherwise, the
publicKeyfield from the decoded JSON payload, meaning insidedataafter base64 decoding.
401 Missing public key.
Both placements are valid. Pick one option per request. Passing the public key in the header is the recommended default: it keeps the signed payload focused on business fields and makes log correlation easier.
Header form (recommended):
publicKey field together with DTO fields:
publicKey must be added to the JSON before encoding and signing. Adding publicKey after signing breaks verification.
2.3. Key Types
A merchant API key has one of twokeyType values. The server chooses the verification algorithm from the merchant record linked to the public key. The integrator does not pass the key type explicitly.
ED25519 is the default for new keys. LEGACY is supported for historical integrations. The two algorithms produce different signatures for the same payload, so use the selected algorithm consistently.
2.4. Algorithm A — ED25519 (recommended)
Steps:- Build the JSON payload with DTO fields, see endpoint sections below.
json = JSON.stringify(payload)data = base64(utf8_bytes(json))signatureBytes = ed25519.sign(utf8_bytes(data), privateKeyBytes)— the input is the base64 string, not the original JSON or raw JSON bytes.signature = base64(signatureBytes)- Send
POST { data, signature }with headerx-public-key: <hex>.
2.5. Algorithm B — LEGACY (SHA-256 with shared secret)
Steps:- Build the JSON payload.
data = base64(utf8_bytes(JSON.stringify(payload)))hexDigest = sha256_hex(privateKey + data)— string concatenation of the secret and base64 payload.signature = base64(utf8_bytes(hexDigest))— base64 encoding is applied to the string of hex characters, not to the raw 32-byte digest.- Send
POST { data, signature }with headerx-public-key: <hex>.
2.6. API Key Permissions and IP Allowlist
The API key issued in the merchant portal contains a list of permissions. Off-Ramp uses one of them:POST /merchant/api/v1/express/withdrawalsrequires the Fiat Withdraw permission.- All other Off-Ramp endpoints require only a valid signature with an active, non-revoked key.
2.7. Error Codes
Error responses use a numericcode field. The most relevant codes for the authentication and validation layer are:
3. Request / Response Envelope
Every response from authenticated endpoints is wrapped:200 responses return a standard NestJS error envelope:
4. Endpoint Reference
All endpoints below use the same authentication model: signed{ data, signature } envelope, x-public-key header, and Content-Type: application/json. Only POST /merchant/api/v1/express/withdrawals additionally requires the Fiat Withdraw permission and is checked against the API key IP allowlist.
4.1. POST /merchant/api/v1/express/rates
Returns the best available merchant-facing rates per bank link.
Decoded payload:
Response:
id is the rateId that must be passed to withdrawals when creating an order. rate is the fiat amount per 1 USDT. For a given fiatAmount, the merchant will be charged fiatAmount / rate USDT. This amount is returned as usdtTotal from withdrawals.
4.2. POST /merchant/api/v1/express/banks
Returns the list of banks supported for the specified fiat currency.
Decoded payload:
Response:
4.3. POST /merchant/api/v1/express/currencies
Returns the list of fiat currencies supported by the specified bank.
Decoded payload:
Response:
4.4. POST /merchant/api/v1/express/bank-link
Returns the recipient-data field schema required for an order against a bank link. Use this endpoint before withdrawals to understand which fields, for example card number or phone, must be collected from the end user.
Decoded payload:
Response:
fieldType can be TEXT, NUMBER, or MASKED. The name of each field is the key the integrator must use in recipientData when calling withdrawals.
4.5. POST /merchant/api/v1/express/withdrawals
Creates a fiat withdrawal. The system locks USDT on the merchant balance and dispatches the order to a P2P partner.
Required permission: Fiat Withdraw. The endpoint is checked against the API key IP allowlist.
Decoded payload:
Example payload before signing:
available -= usdtTotal, locked += usdtTotal. Funds are released only when the transaction reaches COMPLETED (consumed) or CANCELLED (refunded). The full balance flow is described in section 5.
4.6. POST /merchant/api/v1/express/withdrawals/list
Paginated list of the merchant’s Off-Ramp withdrawals.
Decoded payload:
Response:
4.7. POST /merchant/api/v1/express/withdrawals/detail
Details of a single withdrawal.
Decoded payload:
Response:
5. Withdrawal Status Lifecycle
Merchant-visible status flow for an Off-Ramp transaction:usdtTotal and exchangeRate returned by withdrawals are fixed when the transaction is created and do not change during the transaction lifecycle.
6. Outbound Webhooks
The platform notifies the merchant about status changes by sending HTTPPOST requests to the merchant-configured webhook URL. Delivery is asynchronous and retried on failure.
6.1. Event Types
Events
express::order.* and express::partner.* are internal and are not delivered to merchant webhooks.
6.2. Webhook Payload
6.3. Verifying the Webhook
The signature covers the payload object without thesignature field, meaning { id, delivered_at, event }, and is produced with the same algorithm as the request signature for the merchant key, see section 2.3.
A correct verification implementation must:
- Check replay protection. Reject the delivery if
idhas already been processed. Store processed IDs in a database; an in-memory set is lost after restart. - Check the timestamp. Reject the delivery if
Math.abs(now - delivered_at) > 16 minutes. The 16-minute window covers maximum delivery time including retries. - Recompute and compare the signature over
{ id, delivered_at, event }. - Store
idonly after all three checks pass successfully.