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:
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.' };}// Usageconst status = await pollTopUpStatus('order-123456');console.log('Final status:', status.status);
import requestsimport timeimport osdef 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.' }# Usagestatus = poll_topup_status('order-123456')print(f'Final status: {status["status"]}')
<?phpfunction 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/bashAPI_KEY="YOUR_API_KEY"REF="$1"MAX_ATTEMPTS=60INTERVAL=5check_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')
<?phpfunction 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:
from celery import Celeryapp = 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 pollingdef 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 midnightcron.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 scheduleimport timefrom datetime import datetime, timedeltadef 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 midnightschedule.every().day.at("00:00").do(recheck_uncertain_orders)# Keep runningwhile 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.shecho "🔄 Checking uncertain orders..."# Query database for uncertain orders (adjust based on your DB)# This is a pseudo-exampleuncertain_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" fidone <<< "$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
Wait for status to resolve to FULFILLED or REFUNDED within 24 hours before processing refunds.
Use Background Jobs
Poll status asynchronously to avoid blocking user requests and improve performance.
Implement Timeout Protection
Set maximum polling attempts (typically 60 = 5 minutes) and reschedule checks if needed.
Cache Status in Database
Store status in your database to minimize API calls. Only query API when status is not final.
Display Arabic Messages
Always show the refund_message to users as-is. It’s in Arabic and explains the issue clearly.
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