Off-Ramp (Sell USDC)
Create orders for USDC → GHS conversions. User sends USDC to a merchant address and receives GHS via Mobile Money payout.
The off-ramp flow lets users sell USDC for Ghana Cedis. Your app creates an order, the user sends USDC to the merchant's Stellar address, you confirm the crypto payment, and GHS is automatically disbursed to the user's Mobile Money wallet.
Prerequisites
- User must have completed KYC verification
- Merchant must have an active GHS wallet with sufficient balance for the payout
- User must have USDC on the Stellar network ready to send
Create an off-ramp order
curl -X POST https://api.your-domain.com/api/v1/ramp/orders \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "offramp",
"crypto_amount": 10,
"payment_method": "mobile_money",
"payout_method": "mobile_money",
"idempotency_key": "offramp_xyz_456",
"user": {
"first_name": "Ama",
"last_name": "Asante",
"phone": "0542853417",
"network": "MTN"
}
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "offramp" |
crypto_amount | number | One of | Amount in USDC the user sends (specify this OR fiat_amount) |
fiat_amount | number | One of | Amount in GHS the user wants to receive |
payment_method | string | Yes | "mobile_money" |
payout_method | string | No | "mobile_money" (default) |
idempotency_key | string | No | Unique key to prevent duplicate orders |
user.first_name | string | Yes | User's first name (must match KYC) |
user.last_name | string | Yes | User's last name |
user.phone | string | Yes | User's phone to receive GHS payout |
user.network | string | Yes | MoMo network: MTN, VODAFONE, TELECEL, or AIRTELTIGO |
Response
{
"success": true,
"data": {
"reference": "RAMP-20260810-6664a946",
"order_id": "3fb1a533-f292-4764-a197-3df749f41bd0",
"type": "offramp",
"status": "crypto_pending",
"you_pay": "10.00 USDC",
"you_receive": "117.80 GHS",
"rate": "1 USDC = 11.78 GHS",
"fiat_amount": 117.8,
"fiat_currency": "GHS",
"crypto_amount": 10,
"crypto_currency": "USDC",
"rate_applied": 11.78,
"fee_amount": 1.77,
"fee_bearer": "customer",
"net_fiat_amount": 117.8,
"net_crypto_amount": 10,
"merchant_stellar_address": "GC24GD7HFXFP4JBFNGKWSGDNNHRXLLY32NNNXSWDE24PBZUFZ5T5XNN5",
"expires_at": "2026-08-10T06:40:04Z",
"message": "Send 10.00 USDC to the merchant address below within 5 minutes."
}
}The response includes merchant_stellar_address — this is where the user must send their USDC. The order will expire if crypto is not confirmed before expires_at.
Step 2: User sends USDC
The user sends the exact USDC amount to the merchant_stellar_address returned in the order response. Your app should display this address clearly and instruct the user to send the correct amount.
Step 3: Confirm the crypto payment
Once the user has sent USDC, call the confirm endpoint. The API will search for a matching payment on-chain automatically:
curl -X POST https://api.your-domain.com/api/v1/ramp/orders/RAMP-20260810-6664a946/confirm-crypto \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"source_wallet": "GCGES3SODXJX6RYF2ATRQIA5GLWDXTEXR6QWX3JERVCDY4EIFEMFB55O"}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
source_wallet | string | Yes | The Stellar address the user sent USDC from |
tx_hash | string | No | Specific transaction hash (if known). If omitted, the API searches for a matching payment automatically. |
Response
{
"success": true,
"data": {
"reference": "RAMP-20260810-6664a946",
"status": "crypto_confirmed",
"tx_hash": "a3f59723bc57101d16b322b4cd7b6948b4eefab735eca0654322d48f4c28007a",
"message": "Crypto payment verified. Fiat payout initiated."
}
}What happens after confirmation
- Crypto validated — USDC payment is verified on the Stellar network
- Fiat payout initiated — GHS is sent to the user's Mobile Money
- Completed — Order status moves to
completed
The fiat payout typically arrives within seconds for MTN Mobile Money. Bank transfers may take longer.
Check order status
curl https://api.your-domain.com/api/v1/ramp/orders/RAMP-20260810-6664a946 \
-H "Authorization: Bearer $SECRET_KEY"Completed response:
{
"success": true,
"data": {
"reference": "RAMP-20260810-6664a946",
"type": "offramp",
"status": "completed",
"crypto_tx_hash": "a3f59723bc57101d16b322b4cd7b6948b4eefab735eca0654322d48f4c28007a",
"fiat_amount": 117.8,
"crypto_amount": 10,
"rate_applied": 11.78,
"completed_at": "2026-08-10T06:52:30Z"
}
}Cancel an order
Cancel before crypto is sent:
curl -X POST https://api.your-domain.com/api/v1/ramp/orders/RAMP-20260810-6664a946/cancel \
-H "Authorization: Bearer $SECRET_KEY"Cancellation is only possible when the order is in crypto_pending status. Once crypto is confirmed, the order cannot be cancelled.
Automatic transaction matching
When you call /confirm-crypto without a tx_hash, the API searches the Stellar network for a matching USDC payment:
- From the
source_walletyou provide - To the merchant's Stellar address
- For the exact expected USDC amount (with ±0.01 tolerance)
- Created after the order was placed
If a match is found, the transaction hash is returned and the order proceeds. If no match is found, you'll receive an error with details about what was expected.
Error codes
| Error | Meaning |
|---|---|
KYC_REQUIRED | Phone number not verified |
RAMP_NOT_ACTIVE | Merchant ramp wallets not activated |
RATE_UNAVAILABLE | Exchange rate temporarily unavailable |
INSUFFICIENT_LIQUIDITY | Not enough GHS for the payout |
LIMIT_ERROR | Amount exceeds limits |
RATE_EXPIRED | Quote expired — create a new order |
INVALID_STATUS | Order not in the correct state for this operation |
CRYPTO_VALIDATION_FAILED | No matching USDC payment found on-chain |
DUPLICATE_TX | This transaction hash was already used by another order |
Best practices
- Display the
merchant_stellar_addressand exactcrypto_amountclearly so the user sends the correct amount - Call
/confirm-cryptoonly after you're confident the user has sent the USDC (e.g., after they confirm in your UI or after a delay) - If the user provides the tx_hash from their wallet, include it in the confirm request for faster validation
- Use
idempotency_keyto safely retry order creation - Poll order status to detect when the payout completes
- Handle
expiredstatus by creating a new order if the user missed the window