Overview
After sending a top-up request, poll the status endpoint until the transaction reaches a final state. The API typically processes requests in 5-30 seconds, going through PENDING → HANDLING → FULFILLED/REFUNDED states.Critical: Properly handle all status values, especially
UNKNOWN_ERROR. Never refund immediately on unknown status!Check by Reference API
GET /v3/mobile/check-ref/:ref - Track using your reference
Check by ID API
GET /v3/mobile/check-id/:id - Track using topup ID
Status Values
Understanding each status is critical for proper handling:See Check by ID API Reference for detailed status descriptions and use cases.
| Status | Meaning | Duration | Action |
|---|---|---|---|
| PENDING | Queued for processing | 2-15 seconds | Continue polling |
| HANDLING | Being processed | 3-8 seconds | Continue polling |
| FULFILLED | Successfully completed ✅ | Final | Mark complete, notify user |
| REFUNDED | Failed and refunded ❌ | Final | Refund user, show error |
| UNKNOWN_ERROR | Status uncertain ⚠️ | Resolves in 1-24h | Wait, don’t refund yet |
Basic Polling Implementation
async function checkTopUpStatus(ref) {
const response = await fetch(
`https://api.oneclickdz.com/v3/mobile/check-ref/${ref}`,
{
headers: {
'X-Access-Token': process.env.ONECLICKDZ_API_KEY
}
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.success) {
throw new Error('Failed to check status');
}
return data.data;
}
async function pollTopUpStatus(ref, maxAttempts = 60, intervalMs = 5000) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const status = await checkTopUpStatus(ref);
console.log(`[${attempt}/${maxAttempts}] Status: ${status.status}`);
// Check if reached final state
const finalStates = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR'];
if (finalStates.includes(status.status)) {
return status;
}
// Still processing, wait before next check
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error.message);
// Continue polling even if one check fails
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
}
// Timeout
return {
status: 'TIMEOUT',
message: 'Status check timeout. Check again later.'
};
}
// Usage
const status = await pollTopUpStatus('order-123456');
console.log('Final status:', status.status);
import requests
import time
import os
def check_topup_status(ref):
"""Check top-up status by reference"""
response = requests.get(
f'https://api.oneclickdz.com/v3/mobile/check-ref/{ref}',
headers={'X-Access-Token': os.getenv('ONECLICKDZ_API_KEY')}
)
response.raise_for_status()
data = response.json()
if not data['success']:
raise Exception('Failed to check status')
return data['data']
def poll_topup_status(ref, max_attempts=60, interval_seconds=5):
"""Poll top-up status until completion"""
for attempt in range(1, max_attempts + 1):
try:
status = check_topup_status(ref)
print(f'[{attempt}/{max_attempts}] Status: {status["status"]}')
# Check if reached final state
final_states = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR']
if status['status'] in final_states:
return status
# Still processing, wait before next check
if attempt < max_attempts:
time.sleep(interval_seconds)
except Exception as error:
print(f'Attempt {attempt} failed: {error}')
# Continue polling even if one check fails
if attempt < max_attempts:
time.sleep(interval_seconds)
# Timeout
return {
'status': 'TIMEOUT',
'message': 'Status check timeout. Check again later.'
}
# Usage
status = poll_topup_status('order-123456')
print(f'Final status: {status["status"]}')
<?php
function checkTopUpStatus($ref) {
$ch = curl_init("https://api.oneclickdz.com/v3/mobile/check-ref/{$ref}");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Access-Token: ' . getenv('ONECLICKDZ_API_KEY')
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("HTTP error! status: $httpCode");
}
$data = json_decode($response, true);
if (!$data['success']) {
throw new Exception('Failed to check status');
}
return $data['data'];
}
function pollTopUpStatus($ref, $maxAttempts = 60, $intervalSeconds = 5) {
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
$status = checkTopUpStatus($ref);
echo "[{$attempt}/{$maxAttempts}] Status: {$status['status']}\n";
// Check if reached final state
$finalStates = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR'];
if (in_array($status['status'], $finalStates)) {
return $status;
}
// Still processing, wait before next check
if ($attempt < $maxAttempts) {
sleep($intervalSeconds);
}
} catch (Exception $error) {
echo "Attempt {$attempt} failed: {$error->getMessage()}\n";
// Continue polling even if one check fails
if ($attempt < $maxAttempts) {
sleep($intervalSeconds);
}
}
}
// Timeout
return [
'status' => 'TIMEOUT',
'message' => 'Status check timeout. Check again later.'
];
}
// Usage
$status = pollTopUpStatus('order-123456');
echo "Final status: {$status['status']}\n";
#!/bin/bash
API_KEY="YOUR_API_KEY"
REF="$1"
MAX_ATTEMPTS=60
INTERVAL=5
check_status() {
curl -s "https://api.oneclickdz.com/v3/mobile/check-ref/$1" \
-H "X-Access-Token: $API_KEY" | jq -r '.data.status'
}
poll_status() {
for attempt in $(seq 1 $MAX_ATTEMPTS); do
status=$(check_status "$1")
echo "[$attempt/$MAX_ATTEMPTS] Status: $status"
# Check if final state
if [[ "$status" == "FULFILLED" ]] || \
[[ "$status" == "REFUNDED" ]] || \
[[ "$status" == "UNKNOWN_ERROR" ]]; then
echo "Final status: $status"
return 0
fi
# Wait before next check
if [ $attempt -lt $MAX_ATTEMPTS ]; then
sleep $INTERVAL
fi
done
echo "Timeout after $MAX_ATTEMPTS attempts"
return 1
}
poll_status "$REF"
Handling Different Status States
async function handleTopUpStatus(orderId, ref) {
const status = await pollTopUpStatus(ref);
switch (status.status) {
case 'FULFILLED':
// Success! Mark order as complete
await db.orders.update({
status: 'COMPLETED',
completedAt: new Date()
}, {
where: { id: orderId }
});
// Notify user
await notifyUser(orderId, 'success', 'Top-up completed successfully');
console.log('✅ Top-up completed successfully');
break;
case 'REFUNDED':
// Failed, refund user
await db.orders.update({
status: 'REFUNDED',
refundMessage: status.refund_message,
refundedAt: new Date()
}, {
where: { id: orderId }
});
// Refund user balance
const order = await db.orders.findOne({ where: { id: orderId } });
await refundUserBalance(order.userId, order.cost);
// Notify user with error message (in Arabic)
await notifyUser(orderId, 'failed', status.refund_message);
// Show suggested offers if available
if (status.suggested_offers && status.suggested_offers.length > 0) {
console.log('Suggested alternatives:', status.suggested_offers);
await showSuggestedOffers(orderId, status.suggested_offers);
}
console.log('❌ Top-up refunded:', status.refund_message);
break;
case 'UNKNOWN_ERROR':
// Status uncertain - DO NOT REFUND YET!
await db.orders.update({
status: 'PENDING_VERIFICATION',
verificationMessage: status.refund_message
}, {
where: { id: orderId }
});
// Show message to user (don't say it failed!)
await notifyUser(orderId, 'pending', status.refund_message);
// Schedule recheck in 1 hour
await scheduleStatusRecheck(orderId, ref, Date.now() + 3600000);
console.log('⚠️ Status unknown, will verify within 24h');
break;
case 'TIMEOUT':
// Polling timeout - schedule recheck
await db.orders.update({
status: 'CHECKING'
}, {
where: { id: orderId }
});
// Schedule recheck in 5 minutes
await scheduleStatusRecheck(orderId, ref, Date.now() + 300000);
console.log('⏱️ Polling timeout, will check again later');
break;
}
}
async def handle_topup_status(order_id, ref, db):
"""Handle different top-up status outcomes"""
status = poll_topup_status(ref)
if status['status'] == 'FULFILLED':
# Success! Mark order as complete
db.orders.update_one(
{'_id': order_id},
{
'$set': {
'status': 'COMPLETED',
'completed_at': datetime.now()
}
}
)
# Notify user
notify_user(order_id, 'success', 'Top-up completed successfully')
print('✅ Top-up completed successfully')
elif status['status'] == 'REFUNDED':
# Failed, refund user
db.orders.update_one(
{'_id': order_id},
{
'$set': {
'status': 'REFUNDED',
'refund_message': status.get('refund_message'),
'refunded_at': datetime.now()
}
}
)
# Refund user balance
order = db.orders.find_one({'_id': order_id})
refund_user_balance(order['user_id'], order['cost'])
# Notify user
notify_user(order_id, 'failed', status.get('refund_message'))
# Show suggested offers
if status.get('suggested_offers'):
print('Suggested alternatives:', status['suggested_offers'])
show_suggested_offers(order_id, status['suggested_offers'])
print(f"❌ Top-up refunded: {status.get('refund_message')}")
elif status['status'] == 'UNKNOWN_ERROR':
# Status uncertain - DO NOT REFUND YET!
db.orders.update_one(
{'_id': order_id},
{
'$set': {
'status': 'PENDING_VERIFICATION',
'verification_message': status.get('refund_message')
}
}
)
# Notify user (don't say it failed!)
notify_user(order_id, 'pending', status.get('refund_message'))
# Schedule recheck in 1 hour
schedule_status_recheck(order_id, ref, time.time() + 3600)
print('⚠️ Status unknown, will verify within 24h')
elif status['status'] == 'TIMEOUT':
# Polling timeout
db.orders.update_one(
{'_id': order_id},
{'$set': {'status': 'CHECKING'}}
)
# Schedule recheck in 5 minutes
schedule_status_recheck(order_id, ref, time.time() + 300)
print('⏱️ Polling timeout, will check again later')
<?php
function handleTopUpStatus($orderId, $ref, $db) {
$status = pollTopUpStatus($ref);
switch ($status['status']) {
case 'FULFILLED':
// Success! Mark order as complete
$db->query(
"UPDATE orders SET status = 'COMPLETED', completed_at = NOW() WHERE id = ?",
[$orderId]
);
// Notify user
notifyUser($orderId, 'success', 'Top-up completed successfully');
echo "✅ Top-up completed successfully\n";
break;
case 'REFUNDED':
// Failed, refund user
$db->query(
"UPDATE orders
SET status = 'REFUNDED', refund_message = ?, refunded_at = NOW()
WHERE id = ?",
[$status['refund_message'] ?? null, $orderId]
);
// Refund user balance
$order = $db->query("SELECT * FROM orders WHERE id = ?", [$orderId])
->fetch(PDO::FETCH_ASSOC);
refundUserBalance($order['user_id'], $order['cost'], $db);
// Notify user
notifyUser($orderId, 'failed', $status['refund_message'] ?? 'Top-up failed');
// Show suggested offers
if (!empty($status['suggested_offers'])) {
echo "Suggested alternatives: " . json_encode($status['suggested_offers']) . "\n";
showSuggestedOffers($orderId, $status['suggested_offers']);
}
echo "❌ Top-up refunded: {$status['refund_message']}\n";
break;
case 'UNKNOWN_ERROR':
// Status uncertain - DO NOT REFUND YET!
$db->query(
"UPDATE orders
SET status = 'PENDING_VERIFICATION', verification_message = ?
WHERE id = ?",
[$status['refund_message'] ?? null, $orderId]
);
// Notify user (don't say it failed!)
notifyUser($orderId, 'pending', $status['refund_message'] ?? 'Verifying status...');
// Schedule recheck in 1 hour
scheduleStatusRecheck($orderId, $ref, time() + 3600);
echo "⚠️ Status unknown, will verify within 24h\n";
break;
case 'TIMEOUT':
// Polling timeout
$db->query("UPDATE orders SET status = 'CHECKING' WHERE id = ?", [$orderId]);
// Schedule recheck in 5 minutes
scheduleStatusRecheck($orderId, $ref, time() + 300);
echo "⏱️ Polling timeout, will check again later\n";
break;
}
}
Background Status Polling
For better performance, poll status in background jobs:const Queue = require('bull');
const pollQueue = new Queue('status-polling');
// Start polling job
async function startStatusPolling(orderId, ref) {
await pollQueue.add({
orderId,
ref,
attempt: 1
}, {
delay: 5000, // Start after 5 seconds
attempts: 60,
backoff: {
type: 'fixed',
delay: 5000
}
});
}
// Process polling jobs
pollQueue.process(async (job) => {
const { orderId, ref, attempt } = job.data;
console.log(`Checking status (attempt ${attempt})...`);
const status = await checkTopUpStatus(ref);
const finalStates = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR'];
if (finalStates.includes(status.status)) {
// Reached final state, handle it
await handleTopUpStatus(orderId, ref);
return { done: true, status: status.status };
}
// Still processing, continue polling
if (attempt < 60) {
await pollQueue.add({
orderId,
ref,
attempt: attempt + 1
}, {
delay: 5000
});
} else {
// Timeout
await handleTopUpStatus(orderId, ref);
}
return { done: false, status: status.status };
});
from celery import Celery
app = Celery('polling', broker='redis://localhost:6379/0')
@app.task(bind=True, max_retries=60)
def poll_status_task(self, order_id, ref):
"""Poll status asynchronously"""
try:
status = check_topup_status(ref)
print(f'Checking status (attempt {self.request.retries + 1})...')
final_states = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR']
if status['status'] in final_states:
# Reached final state
handle_topup_status(order_id, ref, db)
return {'done': True, 'status': status['status']}
# Still processing, retry in 5 seconds
raise self.retry(countdown=5)
except Exception as error:
if self.request.retries >= self.max_retries:
# Timeout
handle_topup_status(order_id, ref, db)
return {'done': True, 'status': 'TIMEOUT'}
# Retry
raise self.retry(countdown=5, exc=error)
# Start polling
def start_status_polling(order_id, ref):
poll_status_task.apply_async(args=[order_id, ref], countdown=5)
Scheduled Recheck for UNKNOWN_ERROR
Set up daily cronjob to recheck uncertain orders:const cron = require('node-cron');
// Run daily at midnight
cron.schedule('0 0 * * *', async () => {
console.log('🔄 Checking uncertain orders...');
// Find all orders with UNKNOWN_ERROR or PENDING_VERIFICATION
// that are older than 1 hour
const uncertainOrders = await db.orders.findAll({
where: {
status: {
[db.Op.in]: ['UNKNOWN_ERROR', 'PENDING_VERIFICATION']
},
createdAt: {
[db.Op.lt]: new Date(Date.now() - 3600000) // 1 hour ago
}
}
});
console.log(`Found ${uncertainOrders.length} orders to recheck`);
for (const order of uncertainOrders) {
try {
const status = await checkTopUpStatus(order.ref);
console.log(`Order ${order.id}: ${order.status} → ${status.status}`);
// Update based on new status
if (status.status === 'FULFILLED') {
await db.orders.update({
status: 'COMPLETED',
completedAt: new Date()
}, {
where: { id: order.id }
});
await notifyUser(order.id, 'success', 'Top-up completed successfully');
} else if (status.status === 'REFUNDED') {
await db.orders.update({
status: 'REFUNDED',
refundMessage: status.refund_message,
refundedAt: new Date()
}, {
where: { id: order.id }
});
// NOW we can refund
await refundUserBalance(order.userId, order.cost);
await notifyUser(order.id, 'failed', status.refund_message);
}
} catch (error) {
console.error(`Failed to recheck order ${order.id}:`, error);
}
}
console.log('✅ Recheck completed');
});
import schedule
import time
from datetime import datetime, timedelta
def recheck_uncertain_orders():
"""Daily cronjob to recheck uncertain orders"""
print('🔄 Checking uncertain orders...')
# Find uncertain orders older than 1 hour
one_hour_ago = datetime.now() - timedelta(hours=1)
uncertain_orders = db.orders.find({
'status': {'$in': ['UNKNOWN_ERROR', 'PENDING_VERIFICATION']},
'created_at': {'$lt': one_hour_ago}
})
uncertain_orders = list(uncertain_orders)
print(f'Found {len(uncertain_orders)} orders to recheck')
for order in uncertain_orders:
try:
status = check_topup_status(order['ref'])
print(f"Order {order['_id']}: {order['status']} → {status['status']}")
if status['status'] == 'FULFILLED':
db.orders.update_one(
{'_id': order['_id']},
{
'$set': {
'status': 'COMPLETED',
'completed_at': datetime.now()
}
}
)
notify_user(order['_id'], 'success', 'Top-up completed successfully')
elif status['status'] == 'REFUNDED':
db.orders.update_one(
{'_id': order['_id']},
{
'$set': {
'status': 'REFUNDED',
'refund_message': status.get('refund_message'),
'refunded_at': datetime.now()
}
}
)
# NOW we can refund
refund_user_balance(order['user_id'], order['cost'])
notify_user(order['_id'], 'failed', status.get('refund_message'))
except Exception as error:
print(f"Failed to recheck order {order['_id']}: {error}")
print('✅ Recheck completed')
# Schedule daily at midnight
schedule.every().day.at("00:00").do(recheck_uncertain_orders)
# Keep running
while True:
schedule.run_pending()
time.sleep(60)
#!/bin/bash
# Save as: /usr/local/bin/recheck-topups.sh
# Add to crontab: 0 0 * * * /usr/local/bin/recheck-topups.sh
echo "🔄 Checking uncertain orders..."
# Query database for uncertain orders (adjust based on your DB)
# This is a pseudo-example
uncertain_orders=$(mysql -u user -p'pass' -D db -se \
"SELECT id, ref FROM orders
WHERE status IN ('UNKNOWN_ERROR', 'PENDING_VERIFICATION')
AND created_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)")
while IFS=$'\t' read -r order_id ref; do
echo "Checking order $order_id ($ref)..."
status=$(curl -s "https://api.oneclickdz.com/v3/mobile/check-ref/$ref" \
-H "X-Access-Token: $API_KEY" | jq -r '.data.status')
echo "Status: $status"
if [ "$status" = "FULFILLED" ]; then
mysql -u user -p'pass' -D db -e \
"UPDATE orders SET status='COMPLETED', completed_at=NOW() WHERE id=$order_id"
echo "✅ Order $order_id completed"
elif [ "$status" = "REFUNDED" ]; then
mysql -u user -p'pass' -D db -e \
"UPDATE orders SET status='REFUNDED', refunded_at=NOW() WHERE id=$order_id"
# Trigger refund process
echo "❌ Order $order_id refunded"
fi
done <<< "$uncertain_orders"
echo "✅ Recheck completed"
Optimized Polling Strategy
Use intelligent polling intervals:async function adaptivePollTopUpStatus(ref) {
const intervals = [
{ attempts: 3, delay: 3000 }, // First 3 checks: every 3s
{ attempts: 5, delay: 5000 }, // Next 5 checks: every 5s
{ attempts: 12, delay: 10000 }, // Next 12 checks: every 10s
{ attempts: 40, delay: 30000 } // Final checks: every 30s
];
let totalAttempts = 0;
for (const { attempts, delay } of intervals) {
for (let i = 0; i < attempts; i++) {
totalAttempts++;
const status = await checkTopUpStatus(ref);
console.log(`[${totalAttempts}] Status: ${status.status} (delay: ${delay}ms)`);
const finalStates = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR'];
if (finalStates.includes(status.status)) {
return status;
}
// Wait before next check
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return { status: 'TIMEOUT' };
}
def adaptive_poll_topup_status(ref):
"""Poll with adaptive intervals"""
intervals = [
{'attempts': 3, 'delay': 3}, # First 3 checks: every 3s
{'attempts': 5, 'delay': 5}, # Next 5 checks: every 5s
{'attempts': 12, 'delay': 10}, # Next 12 checks: every 10s
{'attempts': 40, 'delay': 30} # Final checks: every 30s
]
total_attempts = 0
for interval in intervals:
for i in range(interval['attempts']):
total_attempts += 1
status = check_topup_status(ref)
print(f"[{total_attempts}] Status: {status['status']} (delay: {interval['delay']}s)")
final_states = ['FULFILLED', 'REFUNDED', 'UNKNOWN_ERROR']
if status['status'] in final_states:
return status
# Wait before next check
time.sleep(interval['delay'])
return {'status': 'TIMEOUT'}
Best Practices
Never Refund UNKNOWN_ERROR Immediately
Never Refund UNKNOWN_ERROR Immediately
Wait for status to resolve to FULFILLED or REFUNDED within 24 hours before processing refunds.
Use Background Jobs
Use Background Jobs
Poll status asynchronously to avoid blocking user requests and improve performance.
Implement Timeout Protection
Implement Timeout Protection
Set maximum polling attempts (typically 60 = 5 minutes) and reschedule checks if needed.
Cache Status in Database
Cache Status in Database
Store status in your database to minimize API calls. Only query API when status is not final.
Display Arabic Messages
Display Arabic Messages
Always show the
refund_message to users as-is. It’s in Arabic and explains the issue clearly.Handle Suggested Offers
Handle Suggested Offers
When
suggested_offers is present, show these alternatives to improve conversion.Next Steps
Polling Strategies
Learn advanced polling optimization techniques
Webhooks
Set up real-time status notifications instead of polling
List Top-Ups API
View all your top-up transactions
Error Handling
Complete error handling reference

