Sell transaction API integration (Stablecoins -> NGN)
Use the Off-Ramp API to let customers sell stablecoins, such as USDT, and receive NGN in a Nigerian bank account.
The customer does not need a Quidax or custodial account. Quidax generates a crypto deposit address for each transaction. After the customer sends crypto to that address, Quidax converts it and processes the NGN payout.
Base URL
https://ramp-be.quidax.io
Authentication
Send your private key in the x-private-key header for all requests.
x-private-key: sk_test
Keep your private key on your backend. Do not expose it in frontend or mobile application code.
How the flow works
A typical off-ramp transaction follows these steps:
- Initiate the off-ramp transaction.
- Refresh the transaction if you need a new quote.
- Fetch supported Nigerian banks.
- Add the customer's bank account.
- Confirm the transaction to generate a crypto deposit address.
- Ask the customer to send the exact crypto amount to the generated address.
- Requery the transaction to check its latest status.
- Listen for webhook events.
- Use the final webhook to confirm whether the transaction succeeded or failed.
1. Initiate an off-ramp transaction
Create an off-ramp transaction by providing the crypto amount, fiat currency, blockchain network, customer details, and a unique merchant reference.
The merchant_reference must be unique for each transaction. Do not reuse the same reference for another transaction.
Endpoint
POST https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/initiate
Request
curl --request POST \
--url https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/initiate \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-private-key: sk_test' \
--data '{
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3",
"network": "bep20",
"customer": {
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe"
},
"merchant_reference": "ngn-sell-0001"
}'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from_currency | string | Yes | Cryptocurrency the customer wants to sell. For example, usdt. |
to_currency | string | Yes | Fiat currency the customer will receive. Use ngn. |
from_amount | string | Yes | Crypto amount the customer wants to sell. |
network | string | Yes | Blockchain network the customer will use. For example, bep20. |
customer.email | string | Yes | Customer's unique email address. |
customer.first_name | string | Yes | Customer's first name. |
customer.last_name | string | Yes | Customer's last name. |
merchant_reference | string | Yes | Your unique reference for the transaction. |
The customer does not need an existing custodial account. The supplied customer information identifies them during the transaction.
Response
{
"status": "ok",
"message": "Transaction successfully initiated",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"from_currency": "usdt",
"to_currency": "ngn",
"to_amount": "4500.0",
"from_amount": "3.0",
"reference": "TRX-ZTIKAIJPFA",
"merchant_reference": "ngn-sell-0001",
"status": "pending",
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:01.000Z",
"stamp_charge": "0.0"
}
}
Response fields
| Field | Description |
|---|---|
data.public_id | Public ID of the off-ramp transaction. |
data.from_currency | Cryptocurrency the customer is selling. |
data.to_currency | Fiat currency the customer will receive. |
data.from_amount | Crypto amount the customer must send. |
data.to_amount | Estimated NGN amount the customer will receive. |
data.reference | Quidax transaction reference. |
data.merchant_reference | Your unique transaction reference. |
data.status | Current transaction status. |
data.stamp_charge | Stamp charge applied to the transaction, if any. |
Error responses
Invalid amount
{
"status": "bad_request",
"message": "Invalid amount",
"data": null
}
Missing customer information
{
"status": "bad_request",
"message": "customer first name is missing",
"data": null
}
Invalid private key
{
"status": "forbidden",
"message": "Invalid private key",
"data": null
}
Invalid customer
{
"status": "bad_request",
"error": "Invalid customer",
"data": null
}
2. Refresh an off-ramp transaction (optional)
Refresh an existing transaction when you need to recalculate the quote before the customer sends crypto.
Use the same merchant_reference supplied when initiating the transaction.
Do not refresh a transaction after the customer has sent crypto.
Endpoint
PUT https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/{merchant_reference}/refresh
Request
curl --request PUT \
--url https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/ngn-sell-0001/refresh \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-private-key: sk_test' \
--data '{
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3"
}'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from_currency | string | Yes | Cryptocurrency the customer wants to sell. |
to_currency | string | Yes | Fiat currency the customer will receive. |
from_amount | string | Yes | Updated crypto amount for the transaction. |
The currencies must match the currencies used when the transaction was initiated.
Response
{
"status": "ok",
"message": "Transaction successfully refreshed",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"from_currency": "usdt",
"to_currency": "ngn",
"to_amount": "4510.0",
"from_amount": "3.0",
"reference": "TRX-ZTIKAIJPFA",
"merchant_reference": "ngn-sell-0001",
"status": "pending",
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:05:00.000Z",
"stamp_charge": "0.0"
}
}
Error response
{
"status": "bad_request",
"message": "Failed to refresh off-ramp transaction",
"data": null
}
3. Fetch supported Nigerian banks
Fetch the latest list of supported Nigerian banks before adding the customer's payout account.
Use the bank's code as the bank_code when adding the account.
Endpoint
GET https://ramp-be.quidax.io/api/v1/merchants/custodial/banks?country=NG
Request
curl --request GET \
--url 'https://ramp-be.quidax.io/api/v1/merchants/custodial/banks?country=NG' \
--header 'accept: application/json' \
--header 'x-private-key: sk_test'
Response
{
"status": "ok",
"message": "Banks successfully fetched",
"data": [
{
"public_id": "bf4a4feb-3131-4737-a7be-69ee821e83f5",
"code": "058",
"name": "Guaranty Trust Bank",
"created_at": "2026-01-10T10:15:14.157Z",
"updated_at": "2026-01-10T10:15:14.157Z"
},
{
"public_id": "9024ec7a-1394-45d8-8e1a-3df32b4392ba",
"code": "044",
"name": "Access Bank",
"created_at": "2026-01-10T10:15:14.176Z",
"updated_at": "2026-01-10T10:15:14.176Z"
}
]
}
Response fields
| Field | Description |
|---|---|
data[].public_id | Public ID of the bank. |
data[].code | Bank code to use as bank_code. |
data[].name | Bank name. |
data[].created_at | Date and time the bank record was created. |
data[].updated_at | Date and time the bank record was last updated. |
Do not permanently hard-code the returned bank list. Fetch it periodically so your integration uses currently supported institutions.
4. Add a payout account
Attach the customer's Nigerian bank account to the off-ramp transaction.
The resolved account name must match the customer name supplied when the transaction was initiated. If the names do not match, the account may not be attached.
Endpoint
POST https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/{merchant_reference}/bank_account
Request
curl --request POST \
--url https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/ngn-sell-0001/bank_account \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-private-key: sk_test' \
--data '{
"currency_code": "ngn",
"bank_code": "058",
"account_number": "0123456789"
}'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
currency_code | string | Yes | Payout currency. Use ngn. |
bank_code | string | Yes | Code returned by the supported banks endpoint. |
account_number | string | Yes | Customer's Nigerian bank account number. |
Response
{
"status": "ok",
"message": "Bank account successfully created and attached to off-ramp transaction",
"data": {
"public_id": "f5bf2c47-9581-4285-ab9d-124b8d89516d",
"metadata": {
"account_number": "0123456789",
"bank_code": "058",
"account_name": "John Doe"
},
"status": "active",
"created_at": "2026-09-10T12:08:00.000Z",
"updated_at": "2026-09-10T12:08:00.000Z"
}
}
Error responses
Failed to create account
{
"status": "bad_request",
"message": "Failed to create account",
"data": null
}
Account name does not match
{
"status": "bad_request",
"message": "Name does not match",
"data": null
}
5. Confirm the off-ramp transaction
Confirm the transaction to generate a crypto deposit address.
After receiving the address, ask the customer to send the exact crypto amount using the currency and blockchain network returned in the response.
Endpoint
POST https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/{merchant_reference}/confirm
Request
curl --request POST \
--url https://ramp-be.quidax.io/api/v1/merchants/custodial/off_ramp_transactions/ngn-sell-0001/confirm \
--header 'accept: application/json' \
--header 'x-private-key: sk_test'
Response
{
"status": "ok",
"message": "Address has been successfully created",
"data": {
"id": "1075784d-79ff-4e9c-816e-74a5eb2eb1ff",
"address": "0x1234567890abcdef1234567890abcdef12345678",
"tag": null,
"network": "bep20",
"currency": "usdt",
"assigned_at": "2026-09-10T12:10:00.000Z",
"status": "assigned"
}
}
Response fields
| Field | Description |
|---|---|
data.id | ID of the generated address record. |
data.address | Address where the customer must send the crypto. |
data.tag | Destination tag, memo, or extra ID. This can be null. |
data.network | Blockchain network the customer must use. |
data.currency | Cryptocurrency the customer must send. |
data.assigned_at | Date and time the address was assigned. |
data.status | assigned means the address was successfully assigned. |
The customer must:
- Send the exact
from_amount. - Send the correct cryptocurrency.
- Use the returned blockchain network.
- Send only to the returned address.
- Include the destination tag or memo when one is returned.
Sending the wrong currency or amount, or using the wrong network, may cause the transaction to fail or require manual review.
Error responses
Invalid private key
{
"status": "bad_request",
"message": "Invalid private key",
"data": null
}
Transaction does not exist
{
"status": "not_found",
"message": "Transaction does not exist",
"data": null
}
Payout account is missing
{
"status": "bad_request",
"message": "Bank Account is required on off-ramp transaction",
"data": null
}
6. Requery the off-ramp transaction
Fetch an off-ramp transaction when you need to check its latest status.
Use your merchant_reference in the request URL.
Endpoint
GET https://ramp-be.quidax.io/api/v1/merchants/off_ramp_transaction/{merchant_reference}
Request
curl --request GET \
--url https://ramp-be.quidax.io/api/v1/merchants/off_ramp_transaction/ngn-sell-0001 \
--header 'accept: application/json' \
--header 'x-private-key: sk_test'
Response
{
"status": "ok",
"message": "Transaction found",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"status": "completed",
"mode": "sell",
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3.0",
"to_amount": "4500.0",
"network": "bep20",
"merchant_reference": "ngn-sell-0001",
"crypto_deposit": {
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"txid": "0x1234567890abcdef",
"status": "accepted",
"amount": "3.0",
"currency": "usdt",
"network": "bep20",
"created_at": "2026-09-10T12:15:00.000Z",
"updated_at": "2026-09-10T12:16:00.000Z"
},
"fiat_payout": {
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"amount": "4500.0",
"status": "completed",
"currency": "ngn",
"processor_fee": "50.0",
"completed_at": "2026-09-10T12:20:00.000Z",
"created_at": "2026-09-10T12:17:00.000Z",
"updated_at": "2026-09-10T12:20:00.000Z"
},
"error_message": null,
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:20:00.000Z"
}
}
Error response
{
"status": "not_found",
"message": "Transaction not found",
"data": null
}
7. Webhooks
Quidax sends webhook events to your configured webhook URL as the transaction moves from crypto deposit to NGN payout.
| Event | Description |
|---|---|
sell_transaction.processing | The crypto deposit was received or the NGN payout is processing. |
sell_transaction.successful | The transaction and NGN payout were completed successfully. |
sell_transaction.failed | The transaction failed or requires attention. |
Sell transaction processing
This event is sent after the crypto deposit has been received and while the NGN payout is being processed.
{
"event": "sell_transaction.processing",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"status": "processing",
"mode": "sell",
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3.0",
"to_amount": "4500.0",
"network": "bep20",
"merchant_reference": "ngn-sell-0001",
"crypto_deposit": {
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"txid": "0x1234567890abcdef",
"status": "accepted",
"amount": "3.0",
"currency": "usdt",
"network": "bep20",
"created_at": "2026-09-10T12:15:00.000Z",
"updated_at": "2026-09-10T12:16:00.000Z"
},
"fiat_payout": {
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"amount": "4500.0",
"status": "processing",
"currency": "ngn",
"processor_fee": "50.0",
"completed_at": null,
"created_at": "2026-09-10T12:17:00.000Z",
"updated_at": "2026-09-10T12:17:00.000Z"
},
"error_message": null,
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:17:00.000Z"
}
}
At an earlier processing stage, crypto_deposit may be present while fiat_payout is still null.
Sell transaction successful
This is the final success event. It means the crypto deposit was accepted and the NGN payout was completed.
{
"event": "sell_transaction.successful",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"status": "completed",
"mode": "sell",
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3.0",
"to_amount": "4500.0",
"network": "bep20",
"merchant_reference": "ngn-sell-0001",
"crypto_deposit": {
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"txid": "0x1234567890abcdef",
"status": "accepted",
"amount": "3.0",
"currency": "usdt",
"network": "bep20",
"created_at": "2026-09-10T12:15:00.000Z",
"updated_at": "2026-09-10T12:16:00.000Z"
},
"fiat_payout": {
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"amount": "4500.0",
"status": "completed",
"currency": "ngn",
"processor_fee": "50.0",
"completed_at": "2026-09-10T12:20:00.000Z",
"created_at": "2026-09-10T12:17:00.000Z",
"updated_at": "2026-09-10T12:20:00.000Z"
},
"error_message": null,
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:20:00.000Z"
}
}
Sell transaction failed
This event is sent when the transaction fails or requires manual review.
A transaction may require attention if the customer sends the wrong crypto amount or the NGN payout cannot be completed.
{
"event": "sell_transaction.failed",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"status": "needs_attention",
"mode": "sell",
"from_currency": "usdt",
"to_currency": "ngn",
"from_amount": "3.0",
"to_amount": "4500.0",
"network": "bep20",
"merchant_reference": "ngn-sell-0001",
"crypto_deposit": {
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"txid": "0x1234567890abcdef",
"status": "accepted",
"amount": "2.5",
"currency": "usdt",
"network": "bep20",
"created_at": "2026-09-10T12:15:00.000Z",
"updated_at": "2026-09-10T12:16:00.000Z"
},
"fiat_payout": null,
"error_message": "The received crypto amount does not match the expected amount.",
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:18:00.000Z"
}
}
Contact support when a transaction enters needs_attention and cannot be resolved automatically.
Webhook fields
Main fields
| Field | Description |
|---|---|
event | Webhook event name. |
data.public_id | Public ID of the off-ramp transaction. |
data.status | Current transaction status. |
data.mode | Transaction mode. For off-ramp transactions, this is sell. |
data.from_currency | Cryptocurrency the customer is selling. |
data.to_currency | Fiat currency the customer will receive. |
data.from_amount | Expected crypto deposit amount. |
data.to_amount | Expected NGN payout amount. |
data.network | Blockchain network used for the crypto deposit. |
data.merchant_reference | Your unique transaction reference. |
data.crypto_deposit | Crypto deposit details. This may initially be null. |
data.fiat_payout | Fiat payout details. This may initially be null. |
data.error_message | Transaction error, when available. |
data.created_at | Date and time the transaction was created. |
data.updated_at | Date and time the transaction was last updated. |
Crypto deposit fields
| Field | Description |
|---|---|
public_id | Public ID of the crypto deposit. |
txid | Blockchain transaction ID or hash. |
status | Current crypto deposit status. |
amount | Crypto amount received. |
currency | Cryptocurrency received. |
network | Blockchain network used for the deposit. |
created_at | Date and time the deposit was created. |
updated_at | Date and time the deposit was last updated. |
Fiat payout fields
| Field | Description |
|---|---|
public_id | Public ID of the fiat payout. |
amount | NGN payout amount. |
status | Current payout status. |
currency | Fiat payout currency. |
processor_fee | Fee charged by the payout processor. |
completed_at | Date and time the payout was completed. |
created_at | Date and time the payout was created. |
updated_at | Date and time the payout was last updated. |
Webhook handling recommendations
Your webhook endpoint should:
- Accept HTTPS
POSTrequests. - Verify the webhook signature.
- Return a successful HTTP response quickly.
- Process the event asynchronously when possible.
- Handle duplicate webhook deliveries safely.
- Store the event and transaction references.
- Requery the transaction before updating critical records.
- Avoid marking a transaction successful from a processing event.
Use sell_transaction.successful as the final confirmation that the customer's NGN payout was completed.
Best practices
- Use a unique
merchant_referencefor every transaction. - Store both the
merchant_referenceand Quidaxreference. - Use the same
merchant_referencewhen refreshing, adding an account, confirming, and requerying. - Fetch the supported Nigerian bank list instead of hard-coding bank codes.
- Make sure the bank account name matches the customer's name.
- Add the bank account before confirming the transaction.
- Refresh the quote if the customer delays before confirmation.
- Ask the customer to send the exact crypto amount.
- Make sure the customer uses the correct currency, network, address, and destination tag.
- Do not reuse a generated address for another transaction.
- Use webhooks as the source of truth for final transaction outcomes.
- Requery transactions when you need to verify their latest state.
- Store transaction statuses and webhook events for reconciliation and support.
- Contact support when a transaction requires manual review.
Updated about 19 hours ago

