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.
