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:

  1. Initiate the off-ramp transaction.
  2. Refresh the transaction if you need a new quote.
  3. Fetch supported GHS institutions.
  4. Add the customer’s mobile money 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 confirm the latest status.
  8. Listen for webhook events.
  9. 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

ParameterTypeRequiredDescription
from_currencystringYesThe crypto currency the customer wants to sell. For example, usdt.
to_currencystringYesThe fiat currency the customer wants to receive. For example, ghs.
from_amountstringYesThe crypto amount the customer wants to sell.
networkstringYesThe blockchain network the customer will use to send the crypto. For example, bep20.
customer.emailstringYesThe 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_namestringYesThe customer’s first name.
customer.last_namestringYesThe customer’s last name.
merchant_referencestringYesYour 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

FieldDescription
statusThe response status. ok means the request was successful.
messageA short description of the result.
data.public_idThe public ID of the off-ramp transaction.
data.from_currencyThe crypto currency the customer is selling.
data.to_currencyThe fiat currency the customer will receive.
data.to_amountThe estimated fiat amount the customer will receive.
data.from_amountThe crypto amount the customer is selling.
data.referenceThe Quidax transaction reference.
data.merchant_referenceYour unique reference for the transaction.
data.statusThe transaction status. pending means the transaction has not been completed.
data.created_atThe date and time the transaction was created.
data.updated_atThe date and time the transaction was last updated.
data.stamp_chargeThe 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

ParameterTypeRequiredDescription
from_currencystringYesThe crypto currency the customer wants to sell.
to_currencystringYesThe fiat currency the customer wants to receive.
from_amountstringYesThe 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

FieldDescription
data[].public_idThe public ID of the institution.
data[].codeThe institution code. Use this as bank_code.
data[].nameThe institution name.
data[].created_atThe date and time the institution was created.
data[].updated_atThe 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

ParameterTypeRequiredDescription
currency_codestringYesThe payout currency. Use ghs for Ghana payouts.
bank_codestringYesThe institution code from the supported GHS institutions list.
account_numberstringYesThe 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

FieldDescription
data.public_idThe public ID of the payout account.
data.metadata.account_numberThe customer’s bank or mobile money account number.
data.metadata.bank_codeThe institution code used for the account.
data.metadata.slugThe short name of the institution.
data.metadata.account_nameThe resolved account name.
data.statusThe account status. active means the account was added successfully.
data.created_atThe date and time the account was created.
data.updated_atThe 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

FieldDescription
data.idThe ID of the generated address record.
data.addressThe wallet address where the customer should send the crypto.
data.tagThe destination tag, memo, or extra ID. This can be null if the network does not require it.
data.networkThe blockchain network for the crypto deposit.
data.currencyThe cryptocurrency the customer should send.
data.assigned_atThe date and time the address was assigned.
data.statusThe 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

ParameterTypeRequiredDescription
referencestringYesThe 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.

EventDescription
sell_transaction.processingSent when the transaction is being processed.
sell_transaction.successfulSent when the transaction is completed successfully.
sell_transaction.failedSent 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

FieldDescription
eventThe webhook event name.
data.modeThe transaction mode. For off-ramp transactions, this is sell.
data.statusThe current transaction status.
data.networkThe blockchain network used for the crypto deposit.
data.public_idThe public ID of the off-ramp transaction.
data.to_amountThe fiat amount the customer is expected to receive.
data.from_amountThe crypto amount the customer is selling.
data.to_currencyThe fiat currency the customer will receive.
data.from_currencyThe crypto currency the customer is selling.
data.fiat_payoutThe fiat payout details. This can be null if payout has not started.
data.crypto_depositThe crypto deposit details.
data.merchant_referenceYour unique reference for the transaction.

Crypto deposit fields

FieldDescription
data.crypto_deposit.txidThe transaction ID or hash of the crypto deposit.
data.crypto_deposit.amountThe crypto amount received.
data.crypto_deposit.statusThe crypto deposit status.
data.crypto_deposit.networkThe blockchain network used for the deposit.
data.crypto_deposit.currencyThe crypto currency received.
data.crypto_deposit.public_idThe public ID of the crypto deposit.
data.crypto_deposit.created_atThe date and time the crypto deposit was created.
data.crypto_deposit.updated_atThe date and time the crypto deposit was last updated.

Fiat payout fields

FieldDescription
data.fiat_payout.amountThe fiat amount paid out to the customer.
data.fiat_payout.statusThe fiat payout status.
data.fiat_payout.currencyThe fiat payout currency.
data.fiat_payout.public_idThe public ID of the fiat payout.
data.fiat_payout.created_atThe date and time the fiat payout was created.
data.fiat_payout.updated_atThe date and time the fiat payout was last updated.
data.fiat_payout.completed_atThe date and time the fiat payout was completed. This can be null if it has not completed.
data.fiat_payout.processor_feeThe processor fee charged for the fiat payout.

Best practices

  1. Use a unique merchant_reference for every transaction.

  2. Store the merchant_reference and Quidax reference returned when you initiate the transaction.

  3. Use the same merchant_reference when refreshing, adding a payout account, and confirming the transaction.

  4. Fetch the GHS institution list before adding the payout account.

  5. Use the correct bank_code when adding the customer’s payout account.

  6. Make sure the payout account name matches the customer name used when the transaction was initiated.

  7. Confirm the transaction only after the payout account has been added successfully.

  8. Ask the customer to send the exact crypto amount to the generated wallet address.

  9. Make sure the customer sends the correct crypto currency on the correct network.

  10. Use webhooks as the source of truth for final transaction status updates.

  11. Requery the transaction when you need to confirm the latest transaction state.

  12. Store the transaction status for reconciliation and support.


Did this page help you?