Webhooks & Status Polling
Track ramp order progress by polling status or handling webhook events for real-time notifications.
Ramp orders are processed asynchronously. After creating an order, you need to track its progress to know when it's completed, failed, or expired.
Polling order status
The simplest integration is polling the order endpoint:
curl https://api.your-domain.com/api/v1/ramp/orders/{reference} \
-H "Authorization: Bearer $SECRET_KEY"Recommended polling strategy
| Order status | Poll interval | Action |
|---|---|---|
fiat_pending | Every 5 seconds | User is approving MoMo prompt |
crypto_pending | Every 10 seconds | Waiting for user to send USDC |
fiat_confirmed / crypto_confirmed | Every 5 seconds | Processing — settlement in progress |
crypto_sending / fiat_sending | Every 5 seconds | Almost done |
completed / failed / expired | Stop polling | Terminal state reached |
Example polling implementation
async function pollOrder(reference, secretKey) {
const baseUrl = 'https://api.your-domain.com/api/v1/ramp/orders';
const terminalStates = ['completed', 'failed', 'expired', 'cancelled'];
while (true) {
const res = await fetch(`${baseUrl}/${reference}`, {
headers: { 'Authorization': `Bearer ${secretKey}` }
});
const { data } = await res.json();
console.log(`Order ${reference}: ${data.status}`);
if (terminalStates.includes(data.status)) {
return data;
}
// Wait before next poll
await new Promise(r => setTimeout(r, 5000));
}
}Merchant webhooks
If your merchant account has a webhook URL configured, payment events will be forwarded to your endpoint when the underlying fiat transaction completes.
Webhook payload
{
"event": "payment.completed",
"data": {
"reference": "PAY-20260810-3e848662-699d-41e8-8d90-fa25cecdb8ac",
"status": "completed",
"amount": 1015,
"currency": "GHS",
"provider_ref": "41e88d90fa25cecdb8ac",
"channel": "mobile_money",
"meta": {
"ramp_order_id": "f16890cc-8fd0-4af7-982e-2100bf3dda14",
"ramp_type": "onramp"
}
}
}The meta.ramp_order_id and meta.ramp_type fields identify which ramp order the payment belongs to. Use this to match webhook events back to your orders.
Webhook events for ramp
| Event | When | Ramp action |
|---|---|---|
payment.completed | Fiat collected (on-ramp) or fiat disbursed (off-ramp) | USDC send initiated (on-ramp) or order completing (off-ramp) |
payment.failed | Fiat collection or payout failed | Order may retry or fail |
Webhooks confirm the fiat leg only. For the complete order lifecycle (including crypto send confirmation), poll the order status endpoint.
Order status transitions
On-ramp lifecycle
quote_locked → fiat_pending → fiat_confirmed → crypto_sending → completed
│ │
├──→ expired └──→ failed
└──→ cancelledOff-ramp lifecycle
crypto_pending → crypto_confirmed → fiat_sending → completed
│ │
├──→ expired └──→ fiat_retry → fiat_sending
└──→ cancelled └──→ failedResponse fields by status
Terminal: completed
{
"status": "completed",
"crypto_tx_hash": "ab517c09c4b67f8c...",
"completed_at": "2026-08-10T06:25:57Z"
}For on-ramp, crypto_tx_hash is the Stellar transaction where USDC was sent to the user.
For off-ramp, crypto_tx_hash is the Stellar transaction where the user sent USDC.
Terminal: failed
{
"status": "failed",
"failure_reason": "Crypto send failed after max retries"
}Always check failure_reason for details on what went wrong.
Terminal: expired
{
"status": "expired",
"expires_at": "2026-08-10T06:22:00Z"
}The rate TTL elapsed before payment was confirmed. Create a new order.
Handling late payments
If a user's MoMo payment confirms after the rate expires:
- The system automatically detects the late payment
- A refund is initiated back to the user's Mobile Money
- The order status moves to
fiat_refunded
For off-ramp, if USDC arrives after expiry:
- The system detects the late crypto payment
- USDC is automatically refunded to the user's source wallet
- The order status moves to
crypto_refunded
Best practices
- Always treat
completedas the only success state — don't fulfil based on intermediate states - Implement exponential backoff if you encounter rate limits during polling
- Log the full order response for debugging — especially
failure_reasonon failed orders - For production, combine polling with webhooks for the fastest detection
- Set a maximum poll duration (e.g., 30 minutes) and handle timeout gracefully
- Verify
crypto_tx_hashon the Stellar network if your application requires proof of settlement