Skip to main content

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!

Status Values

Understanding each status is critical for proper handling:
See Check by ID API Reference for detailed status descriptions and use cases.
StatusMeaningDurationAction
PENDINGQueued for processing2-15 secondsContinue polling
HANDLINGBeing processed3-8 secondsContinue polling
FULFILLEDSuccessfully completed ✅FinalMark complete, notify user
REFUNDEDFailed and refunded ❌FinalRefund user, show error
UNKNOWN_ERRORStatus uncertain ⚠️Resolves in 1-24hWait, 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);

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;
  }
}

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 };
});

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');
});

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' };
}

Best Practices

Wait for status to resolve to FULFILLED or REFUNDED within 24 hours before processing refunds.
Poll status asynchronously to avoid blocking user requests and improve performance.
Set maximum polling attempts (typically 60 = 5 minutes) and reschedule checks if needed.
Store status in your database to minimize API calls. Only query API when status is not final.
Always show the refund_message to users as-is. It’s in Arabic and explains the issue clearly.
When suggested_offers is present, show these alternatives to improve conversion.

Next Steps