Sell transaction API integrations (Stablecoins to GHS)
Use the Off-Ramp API to let your customers sell stablecoins, such as USDT, and receive GHS in there mobile money account.
An off-ramp transaction is also called a sell transaction. The customer sends crypto to a generated deposit address, and Quidax processes the fiat payout to the customer’s account.
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
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 GHS institutions.
- Add the customer’s mobile money 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 confirm the latest status.
- Listen for webhook events.
- Use the final webhook to confirm if the transaction succeeded or failed.
1. Initiate an off-ramp transaction
Create an off-ramp transaction by providing the crypto amount the customer wants to sell, the fiat currency they want to receive, the crypto 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_tes' \
--data '
{
"from_currency": "usdt",
"to_currency": "ghs",
"from_amount": "3",
"network": "bep20",
"customer": {
"email": "[email protected]",
"first_name": "test",
"last_name": "user"
},
"merchant_reference": "ghs-transaction-1234"
}
'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from_currency | string | Yes | The crypto currency the customer wants to sell. For example, usdt. |
to_currency | string | Yes | The fiat currency the customer wants to receive. For example, ghs. |
from_amount | string | Yes | The crypto amount the customer wants to sell. |
network | string | Yes | The blockchain network the customer will use to send the crypto. For example, bep20. |
customer.email | string | Yes | The customer’s email address. You may use a unique, pseudorandom email address with your company’s domain, as each customer must have a unique email address. |
customer.first_name | string | Yes | The customer’s first name. |
customer.last_name | string | Yes | The customer’s last name. |
merchant_reference | string | Yes | Your unique reference for the transaction. |
Response
{
"status": "ok",
"message": "Transaction successfully initiated",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"from_currency": "usdt",
"to_currency": "ghs",
"to_amount": "33.64684",
"from_amount": "3.0",
"reference": "TRX-ZTIKAIJPFA",
"merchant_reference": "ghs-transaction-1234",
"status": "pending",
"created_at": "2026-07-20T21:39:35.727Z",
"updated_at": "2026-07-20T21:39:36.029Z",
"stamp_charge": "0.0"
}
}
Response fields
| Field | Description |
|---|---|
status | The response status. ok means the request was successful. |
message | A short description of the result. |
data.public_id | The public ID of the off-ramp transaction. |
data.from_currency | The crypto currency the customer is selling. |
data.to_currency | The fiat currency the customer will receive. |
data.to_amount | The estimated fiat amount the customer will receive. |
data.from_amount | The crypto amount the customer is selling. |
data.reference | The Quidax transaction reference. |
data.merchant_reference | Your unique reference for the transaction. |
data.status | The transaction status. pending means the transaction has not been completed. |
data.created_at | The date and time the transaction was created. |
data.updated_at | The date and time the transaction was last updated. |
data.stamp_charge | The stamp charge applied to the transaction, if any. |
Error responses
Invalid amount
{
"status": "bad_request",
"message": "Invalid amount",
"data": null
}
Customer first name is missing
{
"status": "bad_request",
"message": "customer first name is missing",
"data": null
}
Customer last name is missing
{
"status": "bad_request",
"message": "customer last 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
Refresh an existing transaction when you need to recalculate the quote before the customer sends crypto.
Use the same merchant_reference that was used to initiate the transaction.
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/ghs-transaction-1234/refresh \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-private-key: sk_tes' \
--data '
{
"from_currency": "usdt",
"to_currency": "ghs",
"from_amount": "3"
}
'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from_currency | string | Yes | The crypto currency the customer wants to sell. |
to_currency | string | Yes | The fiat currency the customer wants to receive. |
from_amount | string | Yes | The crypto amount for the transaction. |
Response
{
"status": "ok",
"message": "Transaction successfully refreshed",
"data": {
"public_id": "3e9e395c-a8df-4694-97a9-237a0ea21b9b",
"from_currency": "usdt",
"to_currency": "ghs",
"to_amount": "33.64684",
"from_amount": "3.0",
"reference": "TRX-ZTIKAIJPFA",
"merchant_reference": "ghs-transaction-1234",
"status": "pending",
"created_at": "2026-07-20T21:39:35.727Z",
"updated_at": "2026-07-20T22:19:03.996Z",
"stamp_charge": "0.0"
}
}
Error responses
Currency swap not allowed
{
"status": "bad_request",
"message": "Currency swap not allowed",
"data": null
}
Failed to refresh off-ramp transaction
{
"status": "bad_request",
"message": "Failed to refresh off-ramp transaction",
"data": null
}
3. Fetch supported GHS institutions
Fetch the list of supported Ghana banks and mobile money providers before adding the customer’s payout account.
Use the institution code as the bank_code when adding the payout account.
Endpoint
GET /api/v1/merchants/custodial/banks?country=GH
Request
curl --request GET \
--url 'https://ramp-be.quidax.io/api/v1/merchants/custodial/banks?country=GH' \
--header 'accept: application/json' \
--header 'x-private-key: sk_tes'
Response
{
"status": "ok",
"message": "Banks successfully fetched",
"data": [
{
"public_id": "bf4a4feb-3131-4737-a7be-69ee821e83f5",
"code": "0009",
"name": "TIGO",
"created_at": "2025-10-29T10:15:14.157Z",
"updated_at": "2025-10-29T10:15:14.157Z"
},
{
"public_id": "9024ec7a-1394-45d8-8e1a-3df32b4392ba",
"code": "0006",
"name": "VODAFONE",
"created_at": "2025-10-29T10:15:14.176Z",
"updated_at": "2025-11-26T12:14:13.552Z"
},
{
"public_id": "f9cf0f2d-da7b-40cd-be43-e2b2a82f68b4",
"code": "0004",
"name": "MTN",
"created_at": "2025-10-29T10:15:14.140Z",
"updated_at": "2025-11-26T12:14:45.837Z"
},
{
"public_id": "207b1aac-af2a-4da7-832e-2a38c8a38644",
"code": "0005",
"name": "AIRTEL",
"created_at": "2025-10-29T10:15:14.106Z",
"updated_at": "2025-11-26T12:15:01.903Z"
},
{
"public_id": "f36c2688-1509-48b1-8cce-c0239a2aad62",
"code": "030100",
"name": "Absa Bank Ghana Ltd",
"created_at": "2026-05-19T12:26:14.858Z",
"updated_at": "2026-05-19T12:26:14.858Z"
},
{
"public_id": "a1b49f16-2578-49e2-b04c-667231d08316",
"code": "280100",
"name": "Access Bank",
"created_at": "2026-05-19T12:26:14.878Z",
"updated_at": "2026-05-19T12:26:14.878Z"
}
]
}
Response fields
| Field | Description |
|---|---|
data[].public_id | The public ID of the institution. |
data[].code | The institution code. Use this as bank_code. |
data[].name | The institution name. |
data[].created_at | The date and time the institution was created. |
data[].updated_at | The date and time the institution was last updated. |
4. Add a payout account
Add the customer’s Ghana bank account or mobile money account to the transaction.
The account name must match the customer name used when initiating the transaction. 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/ghs-transaction-1234/bank_account \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-private-key: sk_tes' \
--data '
{
"currency_code": "ghs",
"bank_code": "0006",
"account_number": "0123456789"
}
'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
currency_code | string | Yes | The payout currency. Use ghs for Ghana payouts. |
bank_code | string | Yes | The institution code from the supported GHS institutions list. |
account_number | string | Yes | The customer’s mobile money 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": "0209546124",
"bank_code": "0006",
"slug": "VOD",
"account_name": "test user"
},
"status": "active",
"created_at": "2026-07-20T21:46:00.458Z",
"updated_at": "2026-07-20T21:46:00.458Z"
}
}
Response fields
| Field | Description |
|---|---|
data.public_id | The public ID of the payout account. |
data.metadata.account_number | The customer’s bank or mobile money account number. |
data.metadata.bank_code | The institution code used for the account. |
data.metadata.slug | The short name of the institution. |
data.metadata.account_name | The resolved account name. |
data.status | The account status. active means the account was added successfully. |
data.created_at | The date and time the account was created. |
data.updated_at | The date and time the account was last updated. |
Error responses
Failed to create account
{
"status": "bad_request",
"message": "Failed to create account",
"data": null
}
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 the address is generated, ask the customer to send crypto to that address. The customer must send the correct currency, exact amount, and use the correct network.
Sending the wrong currency, wrong amount, or using the wrong network may cause the transaction to fail or require manual review.
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/ghs-transaction-1234/confirm \
--header 'accept: application/json' \
--header 'x-private-key: sk_tes'
Response
{
"status": "ok",
"message": "Address has been successfully created",
"data": {
"id": "1075784d-79ff-4e9c-816e-74a5eb2eb1ff",
"address": "0xaddesss",
"tag": null,
"network": "bep20",
"currency": "usdt",
"assigned_at": "2025-06-16T16:59:24.353Z",
"status": "assigned"
}
}
Response fields
| Field | Description |
|---|---|
data.id | The ID of the generated address record. |
data.address | The wallet address where the customer should send the crypto. |
data.tag | The destination tag, memo, or extra ID. This can be null if the network does not require it. |
data.network | The blockchain network for the crypto deposit. |
data.currency | The cryptocurrency the customer should send. |
data.assigned_at | The date and time the address was assigned. |
data.status | The address status. assigned means the address has been assigned to the transaction. |
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
}
6. Requery the off-ramp transaction
Fetch an off-ramp transaction when you need to confirm its latest status.
Use this endpoint after receiving a webhook, or when you need to check if the transaction status has changed.
Endpoint
GET https://ramp-be.quidax.io/api/v1/merchants/off_ramp_transaction/{reference}
Request
curl --request GET \
--url https://ramp-be.quidax.io/api/v1/merchants/off_ramp_transaction/reference \
--header 'accept: application/json' \
--header 'x-private-key: sk_test'
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
reference | string | Yes | The transaction reference you want to fetch. |
Response
{
"status": "ok",
"message": "Transaction found",
"data": {
"public_id": "80597fec-b337-4e87-a4c4-31da5462231e",
"status": "completed",
"mode": "sell",
"from_currency": "usdt",
"to_currency": "ghs",
"from_amount": "2.0",
"to_amount": "3211.04",
"network": "bep20",
"merchant_reference": "tx12345-raqmddvsp-12221-test21",
"fiat_payout": {
"amount": "20.0",
"status": "completed",
"currency": "ghs",
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"created_at": "2025-04-15T17:21:23.953Z",
"updated_at": "2025-04-15T17:21:23.953Z",
"completed_at": null,
"processor_fee": "2.0"
},
"crypto_deposit": {
"txid": "tx_123",
"amount": "1.5",
"status": "accepted",
"network": "bep20",
"currency": "usdt",
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"created_at": "2025-04-15T17:20:36.007Z",
"updated_at": "2025-04-15T17:20:36.007Z"
},
"created_at": "2025-04-14T17:11:37.513Z",
"updated_at": "2025-04-15T17:22:11.623Z"
}
}
Error responses
Bad request
{
"status": "bad_request",
"message": "Bad request",
"data": null
}
Transaction not found
{
"status": "not_found",
"message": "Transaction not found",
"data": null
}
7. Webhooks
Quidax sends webhook events to your configured webhook URL as the transaction moves through the off-ramp flow.
Listen for these events to track the transaction from crypto deposit to fiat payout.
| Event | Description |
|---|---|
sell_transaction.processing | Sent when the transaction is being processed. |
sell_transaction.successful | Sent when the transaction is completed successfully. |
sell_transaction.failed | Sent when the transaction fails or needs attention. |
Sell transaction processing
This event is sent while the transaction is being processed.
It can be sent after the crypto deposit is accepted, and again when the fiat payout starts processing.
Crypto deposit received
At this stage, crypto_deposit is present and fiat_payout may be null.
{
"event": "sell_transaction.processing",
"data": {
"mode": "sell",
"status": "processing",
"network": "bep20",
"public_id": "80597fec-b337-4e87-a4c4-31da5462231e",
"to_amount": "3211.04",
"created_at": "2025-04-14T17:11:37.513Z",
"updated_at": "2025-04-15T17:22:11.623Z",
"fiat_payout": null,
"from_amount": "2.0",
"to_currency": "ghs",
"from_currency": "usdt",
"crypto_deposit": {
"txid": "tx_123",
"amount": "1.5",
"status": "accepted",
"network": "bep20",
"currency": "usdt",
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"created_at": "2025-04-15T17:20:36.007Z",
"updated_at": "2025-04-15T17:20:36.007Z"
},
"merchant_reference": "tx12345-raqmddvsp-12221-test21"
}
}
Fiat payout processing
At this stage, both crypto_deposit and fiat_payout are present.
{
"event": "sell_transaction.processing",
"data": {
"mode": "sell",
"status": "processing",
"network": "bep20",
"public_id": "80597fec-b337-4e87-a4c4-31da5462231e",
"to_amount": "3211.04",
"created_at": "2025-04-14T17:11:37.513Z",
"updated_at": "2025-04-15T17:22:11.623Z",
"fiat_payout": {
"amount": "20.0",
"status": "processing",
"currency": "ghs",
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"created_at": "2025-04-15T17:21:23.953Z",
"updated_at": "2025-04-15T17:21:23.953Z",
"completed_at": null,
"processor_fee": "2"
},
"from_amount": "2.0",
"to_currency": "ghs",
"from_currency": "usdt",
"crypto_deposit": {
"txid": "tx_123",
"amount": "1.5",
"status": "accepted",
"network": "bep20",
"currency": "usdt",
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"created_at": "2025-04-15T17:20:36.007Z",
"updated_at": "2025-04-15T17:20:36.007Z"
},
"merchant_reference": "tx12345-raqmddvsp-12221-test21"
}
}
Sell transaction successful
This event is sent when the off-ramp transaction is completed.
At this stage, the crypto deposit has been accepted, the fiat payout has been completed, and the transaction status is completed.
{
"event": "sell_transaction.successful",
"data": {
"mode": "sell",
"status": "completed",
"network": "bep20",
"public_id": "80597fec-b337-4e87-a4c4-31da5462231e",
"to_amount": "3211.04",
"created_at": "2025-04-14T17:11:37.513Z",
"updated_at": "2025-04-15T17:22:11.623Z",
"fiat_payout": {
"amount": "20.0",
"status": "completed",
"currency": "ghs",
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"created_at": "2025-04-15T17:21:23.953Z",
"updated_at": "2025-04-15T17:21:23.953Z",
"completed_at": null,
"processor_fee": "2.0"
},
"from_amount": "2.0",
"to_currency": "ghs",
"from_currency": "usdt",
"crypto_deposit": {
"txid": "tx_123",
"amount": "1.5",
"status": "accepted",
"network": "bep20",
"currency": "usdt",
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"created_at": "2025-04-15T17:20:36.007Z",
"updated_at": "2025-04-15T17:20:36.007Z"
},
"merchant_reference": "tx12345-raqmddvsp-12221-test21"
}
}
Sell transaction failed
This event occurs when an off-ramp transaction fails or requires manual review.
This can happen when the crypto deposit is received but the fiat payout cannot be completed. It can also occur when the deposited crypto amount is higher or lower than the expected amount.
Contact support to review and resolve the transaction.
{
"event": "sell_transaction.failed",
"data": {
"mode": "sell",
"status": "needs_attention",
"network": "bep20",
"public_id": "80597fec-b337-4e87-a4c4-31da5462231e",
"to_amount": "31.04",
"created_at": "2025-04-14T17:11:37.513Z",
"updated_at": "2025-04-15T17:22:11.623Z",
"fiat_payout": {
"amount": "20.0",
"status": "failed",
"currency": "ghs",
"public_id": "18b53d2b-8660-4ff4-a19b-0b1c4f965b80",
"created_at": "2025-04-15T17:21:23.953Z",
"updated_at": "2025-04-15T17:21:23.953Z",
"completed_at": null,
"processor_fee": "2.0"
},
"from_amount": "2.0",
"to_currency": "ghs",
"from_currency": "usdt",
"crypto_deposit": {
"txid": "tx_123",
"amount": "1.5",
"status": "accepted",
"network": "bep20",
"currency": "usdt",
"public_id": "b607455c-428b-4637-ba72-baeec2cc65fa",
"created_at": "2025-04-15T17:20:36.007Z",
"updated_at": "2025-04-15T17:20:36.007Z"
},
"merchant_reference": "tx12345-raqmddvsp-12221-test21"
}
}
Webhook fields
Main fields
| Field | Description |
|---|---|
event | The webhook event name. |
data.mode | The transaction mode. For off-ramp transactions, this is sell. |
data.status | The current transaction status. |
data.network | The blockchain network used for the crypto deposit. |
data.public_id | The public ID of the off-ramp transaction. |
data.to_amount | The fiat amount the customer is expected to receive. |
data.from_amount | The crypto amount the customer is selling. |
data.to_currency | The fiat currency the customer will receive. |
data.from_currency | The crypto currency the customer is selling. |
data.fiat_payout | The fiat payout details. This can be null if payout has not started. |
data.crypto_deposit | The crypto deposit details. |
data.merchant_reference | Your unique reference for the transaction. |
Crypto deposit fields
| Field | Description |
|---|---|
data.crypto_deposit.txid | The transaction ID or hash of the crypto deposit. |
data.crypto_deposit.amount | The crypto amount received. |
data.crypto_deposit.status | The crypto deposit status. |
data.crypto_deposit.network | The blockchain network used for the deposit. |
data.crypto_deposit.currency | The crypto currency received. |
data.crypto_deposit.public_id | The public ID of the crypto deposit. |
data.crypto_deposit.created_at | The date and time the crypto deposit was created. |
data.crypto_deposit.updated_at | The date and time the crypto deposit was last updated. |
Fiat payout fields
| Field | Description |
|---|---|
data.fiat_payout.amount | The fiat amount paid out to the customer. |
data.fiat_payout.status | The fiat payout status. |
data.fiat_payout.currency | The fiat payout currency. |
data.fiat_payout.public_id | The public ID of the fiat payout. |
data.fiat_payout.created_at | The date and time the fiat payout was created. |
data.fiat_payout.updated_at | The date and time the fiat payout was last updated. |
data.fiat_payout.completed_at | The date and time the fiat payout was completed. This can be null if it has not completed. |
data.fiat_payout.processor_fee | The processor fee charged for the fiat payout. |
Best practices
-
Use a unique
merchant_referencefor every transaction. -
Store the
merchant_referenceand Quidaxreferencereturned when you initiate the transaction. -
Use the same
merchant_referencewhen refreshing, adding a payout account, and confirming the transaction. -
Fetch the GHS institution list before adding the payout account.
-
Use the correct
bank_codewhen adding the customer’s payout account. -
Make sure the payout account name matches the customer name used when the transaction was initiated.
-
Confirm the transaction only after the payout account has been added successfully.
-
Ask the customer to send the exact crypto amount to the generated wallet address.
-
Make sure the customer sends the correct crypto currency on the correct network.
-
Use webhooks as the source of truth for final transaction status updates.
-
Requery the transaction when you need to confirm the latest transaction state.
-
Store the transaction status for reconciliation and support.
Updated about 22 hours ago

