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:

  1. Initiate the off-ramp transaction.
  2. Refresh the transaction if you need a new quote.
  3. Fetch supported Nigerian banks.
  4. Add the customer's bank account.
  5. Confirm the transaction to generate a crypto deposit address.
  6. Ask the customer to send the exact crypto amount to the generated address.
  7. Requery the transaction to check its latest status.
  8. Listen for webhook events.
  9. 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

ParameterTypeRequiredDescription
from_currencystringYesCryptocurrency the customer wants to sell. For example, usdt.
to_currencystringYesFiat currency the customer will receive. Use ngn.
from_amountstringYesCrypto amount the customer wants to sell.
networkstringYesBlockchain network the customer will use. For example, bep20.
customer.emailstringYesCustomer's unique email address.
customer.first_namestringYesCustomer's first name.
customer.last_namestringYesCustomer's last name.
merchant_referencestringYesYour 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

FieldDescription
data.public_idPublic ID of the off-ramp transaction.
data.from_currencyCryptocurrency the customer is selling.
data.to_currencyFiat currency the customer will receive.
data.from_amountCrypto amount the customer must send.
data.to_amountEstimated NGN amount the customer will receive.
data.referenceQuidax transaction reference.
data.merchant_referenceYour unique transaction reference.
data.statusCurrent transaction status.
data.stamp_chargeStamp 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

ParameterTypeRequiredDescription
from_currencystringYesCryptocurrency the customer wants to sell.
to_currencystringYesFiat currency the customer will receive.
from_amountstringYesUpdated 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

FieldDescription
data[].public_idPublic ID of the bank.
data[].codeBank code to use as bank_code.
data[].nameBank name.
data[].created_atDate and time the bank record was created.
data[].updated_atDate 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

ParameterTypeRequiredDescription
currency_codestringYesPayout currency. Use ngn.
bank_codestringYesCode returned by the supported banks endpoint.
account_numberstringYesCustomer'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

FieldDescription
data.idID of the generated address record.
data.addressAddress where the customer must send the crypto.
data.tagDestination tag, memo, or extra ID. This can be null.
data.networkBlockchain network the customer must use.
data.currencyCryptocurrency the customer must send.
data.assigned_atDate and time the address was assigned.
data.statusassigned 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.

EventDescription
sell_transaction.processingThe crypto deposit was received or the NGN payout is processing.
sell_transaction.successfulThe transaction and NGN payout were completed successfully.
sell_transaction.failedThe 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

FieldDescription
eventWebhook event name.
data.public_idPublic ID of the off-ramp transaction.
data.statusCurrent transaction status.
data.modeTransaction mode. For off-ramp transactions, this is sell.
data.from_currencyCryptocurrency the customer is selling.
data.to_currencyFiat currency the customer will receive.
data.from_amountExpected crypto deposit amount.
data.to_amountExpected NGN payout amount.
data.networkBlockchain network used for the crypto deposit.
data.merchant_referenceYour unique transaction reference.
data.crypto_depositCrypto deposit details. This may initially be null.
data.fiat_payoutFiat payout details. This may initially be null.
data.error_messageTransaction error, when available.
data.created_atDate and time the transaction was created.
data.updated_atDate and time the transaction was last updated.

Crypto deposit fields

FieldDescription
public_idPublic ID of the crypto deposit.
txidBlockchain transaction ID or hash.
statusCurrent crypto deposit status.
amountCrypto amount received.
currencyCryptocurrency received.
networkBlockchain network used for the deposit.
created_atDate and time the deposit was created.
updated_atDate and time the deposit was last updated.

Fiat payout fields

FieldDescription
public_idPublic ID of the fiat payout.
amountNGN payout amount.
statusCurrent payout status.
currencyFiat payout currency.
processor_feeFee charged by the payout processor.
completed_atDate and time the payout was completed.
created_atDate and time the payout was created.
updated_atDate and time the payout was last updated.

Webhook handling recommendations

Your webhook endpoint should:

  1. Accept HTTPS POST requests.
  2. Verify the webhook signature.
  3. Return a successful HTTP response quickly.
  4. Process the event asynchronously when possible.
  5. Handle duplicate webhook deliveries safely.
  6. Store the event and transaction references.
  7. Requery the transaction before updating critical records.
  8. 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

  1. Use a unique merchant_reference for every transaction.
  2. Store both the merchant_reference and Quidax reference.
  3. Use the same merchant_reference when refreshing, adding an account, confirming, and requerying.
  4. Fetch the supported Nigerian bank list instead of hard-coding bank codes.
  5. Make sure the bank account name matches the customer's name.
  6. Add the bank account before confirming the transaction.
  7. Refresh the quote if the customer delays before confirmation.
  8. Ask the customer to send the exact crypto amount.
  9. Make sure the customer uses the correct currency, network, address, and destination tag.
  10. Do not reuse a generated address for another transaction.
  11. Use webhooks as the source of truth for final transaction outcomes.
  12. Requery transactions when you need to verify their latest state.
  13. Store transaction statuses and webhook events for reconciliation and support.
  14. Contact support when a transaction requires manual review.

Did this page help you?