B2B platform for digital goods

Order Result Polling and Recovery Patterns 2026: B2B Digital-Goods APIs

Battle-tested patterns for reliable order result retrieval: polling with backoff, stall detection, DLQ-style recovery, and building your own push layer on top of FoxReload polling.

Order Result Polling and Recovery Patterns 2026: B2B Digital-Goods APIs

FoxReload delivers order results through polling, not webhooks. There is no X-FoxReload-Signature header, no HMAC callback endpoint, and no order.* event stream. When an order's status becomes completed, the delivered codes appear in items[].externalData. This article covers the production patterns for reliable polling, recovery from stalled orders, and building a webhook-like notification layer in your own backend.

1. Polling with exponential backoff — the math

A naive 5-second fixed interval creates unnecessary API load. The correct formula backs off as time passes:

function nextPollDelay(attempt: number): number {
  const base = 5_000; // 5s
  const cap = 30_000; // 30s
  const exp = Math.min(base * Math.pow(1.5, attempt), cap);
  const jitter = Math.random() * exp * 0.3; // ±30%
  return exp + jitter;
}
// attempt 0: 5–6.5s
// attempt 3: ~17–22s
// attempt 6+: 27–39s (capped at 30s base + jitter)

2. Polling implementation with stall detection

// Express + BullMQ pattern
async function pollOrder(orderId: string, apiKey: string): Promise<Order> {
  const maxAttempts = 40; // ~15 minutes total
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const res = await fetch(
      `https://public-api.foxreload.com/api/orders/${orderId}`,
      { headers: { 'X-API-Key': apiKey } },
    );
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const order = await res.json();

    if (order.status === 'completed') {
      // Extract and deliver codes
      const codes = order.items.flatMap((i: any) => i.externalData ?? []);
      await db.insert('delivery_log', { orderId, codes, deliveredAt: new Date() });
      await deliverToCustomer(orderId, codes);
      return order;
    }
    if (['cancelled', 'failed'].includes(order.status)) {
      await handleTerminalFailure(order);
      return order;
    }
    await sleep(nextPollDelay(attempt));
  }
  // Stalled — alert ops
  await alertOps(`Order ${orderId} stalled after ${maxAttempts} poll attempts`);
  throw new Error(`Order ${orderId} did not complete in time`);
}

3. Stalled-order recovery queue

After your initial polling exhausts, do not discard stalled orders. Move them to a recovery queue for periodic re-check:

// Scheduled job — runs every 5 minutes
async function recoverStalledOrders(apiKey: string) {
  const stalled = await db.orders.findAll({
    status: ['active', 'paid', 'processing'],
    createdAt: { lt: new Date(Date.now() - 10 * 60_000) },
    inRecovery: false,
  });

  for (const order of stalled) {
    await db.orders.update(order.id, { inRecovery: true });
    await queue.add('recover-order', { orderId: order.foxreloadId, apiKey }, {
      attempts: 8,
      backoff: { type: 'exponential', delay: 30_000 },
      removeOnFail: false, // keep in DLQ for manual inspection
    });
  }
}

DLQ entries are reviewed by on-call: either re-queued once the issue is resolved, or manually closed if the order is confirmed cancelled.

4. Deduplication — prevent double delivery

Your polling worker may run concurrently or be retried. Guard against delivering the same codes twice using a database UNIQUE constraint:

async function deliverOnce(orderId: string, codes: any[]): Promise<boolean> {
  try {
    await db.query(
      'INSERT INTO delivery_log (order_id, codes, delivered_at) VALUES ($1, $2, NOW())',
      [orderId, JSON.stringify(codes)],
    );
    await sendCodesToCustomer(orderId, codes); // email, bot message, etc.
    return true;
  } catch (err: any) {
    if (err.code === '23505') return false; // already delivered (unique violation)
    throw err;
  }
}
Storage Latency TTL Cost / 1M records
Postgres UNIQUE 5–8ms Forever $0.10
Redis SETNX <2ms 24h $0.40
DynamoDB ConditionExpression 8–12ms 24h $1.25

For most partners, Postgres UNIQUE is optimal: cheap, permanent, and provides a full audit trail.

5. Building your own push notification layer

Your downstream consumers (Telegram bot, email sender, fulfilment queue) should not each poll FoxReload independently. Run one polling worker per order and publish completion events internally:

POST /api/orders ──▶ Your backend creates order
                       │
                       ▼
            Polling worker (one process per order)
              polls GET /api/orders/{id} with backoff
                       │
                       ▼ (on completed)
            Internal queue (Redis Streams / SQS / BullMQ)
            publish { orderId, status, codes }
                       │
                 ┌─────┴─────────────┐
                 ▼                   ▼
          Telegram delivery      Email sender

6. Alerting on delivery degradation

The metric to monitor is a rolling 10-minute order completion rate. If it drops below 99%, that is an incident. Prometheus rule:

- alert: OrderDeliveryDegraded
  expr: |
    (sum(rate(orders_completed_total[10m]))
     / (sum(rate(orders_completed_total[10m])) + sum(rate(orders_stalled_total[10m])))
    ) < 0.99
  for: 2m
  labels: { severity: page }

The alert routes to PagerDuty/Opsgenie. In 80% of cases the root cause is a supplier-side delay — the stalled-order recovery queue resolves it automatically within the retry window.

CTA

Full FoxReload order API documentation, status codes, and externalData schema are available after onboarding — request API access.

Frequently asked questions

Does FoxReload send webhooks when an order is completed?
No. FoxReload does not send webhooks. There is no X-FoxReload-Signature header, no HMAC callback, and no order.* event stream. The correct way to get order results is to poll GET /api/orders/{order_id} until the status is 'completed', 'cancelled', or 'failed'.
How do I deduplicate order results?
Check your own database for the order ID before processing. When your polling worker detects status == 'completed', insert a row to your delivery_log table with a UNIQUE constraint on order_id. If the insert fails, the order was already processed — skip it.
What should a polling loop look like?
Start at 5-second intervals for the first 60 seconds, then back off to 15–30 seconds. Set a maximum total wait of 10–15 minutes before alerting ops. Most FoxReload orders complete in under 60 seconds.
How do I monitor for orders that never complete?
Run a scheduled job every 5 minutes that finds all orders with status in [active, paid, processing] and createdAt older than 10 minutes. Alert your on-call team for each stalled order so they can investigate.
Get FoxReload API access

Related articles