B2B platform for digital goods

Getting Order Results from FoxReload — Polling and Building Your Own Notification Layer

FoxReload delivers order results via polling, not webhooks. This guide covers the polling pattern and how to build a webhook-like notification layer in your own backend.

Getting Order Results from FoxReload — Polling and Building Your Own Notification Layer

FoxReload does not send webhooks. There is no X-FoxReload-Signature header, no HMAC callback endpoint, and no order.* event stream. To get order results — including the delivered codes — you poll GET /api/orders/{order_id}. This guide explains the polling pattern in detail and shows how to build a webhook-like push notification layer in your own backend so downstream consumers (your storefront, your bot, your fulfilment queue) can react to order completion without polling themselves.

1. The polling contract

When status == "completed", each items[].externalData is an array of delivered codes or activation data. An items[].error field may hold a per-item error string.

Status Meaning Your action
active Awaiting payment Wait or pay via /api/orders/{id}/pay
paid Payment confirmed Wait
processing Supplier fulfilment in flight Keep polling
completed Codes in externalData Read and deliver codes
cancelled See cancelReason Handle gracefully
failed Fulfilment failed Alert ops

2. Polling implementation

async function waitForCompletion(
  orderId: string,
  apiKey: string,
  maxWaitMs = 600_000,
): Promise<Order> {
  const start = Date.now();
  let delay = 5_000; // start at 5 s

  while (Date.now() - start < maxWaitMs) {
    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 (['completed', 'cancelled', 'failed'].includes(order.status)) {
      return order;
    }
    await sleep(delay);
    delay = Math.min(delay * 1.5, 30_000); // back off up to 30 s
  }
  throw new Error('Order did not complete within timeout');
}

Practical schedule: poll every 5 seconds for the first 60 seconds, then every 15–30 seconds up to 10 minutes. If the order is still not terminal after that, alert your operations team.

3. Avoid duplicate orders on retry

FoxReload has no idempotency keys. If you receive a timeout or network error after calling POST /api/orders, do not immediately create a new order. First call GET /api/orders?limit=10 to check whether the order was actually created. If it exists, poll it; if it does not, it is safe to create a new one.

4. Building your own async notification layer

Your internal consumers (Telegram bot delivery, email sender, fulfilment queue) should not each poll FoxReload independently. Instead, run a single polling worker per order that publishes results to a message queue or emits events that your internal services subscribe to. This is a clean, reliable pattern:

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

This gives you a webhook-like push model for your internal systems while correctly implementing the FoxReload polling contract.

5. Persisting and delivering codes

When status is completed, read items[].externalData and store each code in your database with the order ID, timestamp, and customer reference before delivering. Never deliver codes without first persisting them — a delivery failure after persistence is recoverable; a delivery before persistence is not.

for (const item of order.items) {
  for (const data of item.externalData ?? []) {
    await db.codes.insert({
      orderId: order.id,
      productId: item.product.id,
      code: data.code ?? JSON.stringify(data),
      deliveredAt: null,
    });
  }
}
await deliverCodesToCustomer(order.id);
await db.orders.update(order.id, { status: 'delivered', deliveredAt: new Date() });

6. Alerting on stalled orders

Add a scheduled job that checks for orders stuck in processing for more than 10 minutes and alerts your on-call team. This surfaces supplier-side delays before they become customer support issues.

// Run every 5 minutes
const stalled = await db.orders.findAll({
  status: ['paid', 'processing'],
  createdAt: { lt: new Date(Date.now() - 10 * 60_000) },
});
for (const order of stalled) {
  await alertOps(`Order ${order.id} has been processing for >10 min`);
}

FoxReload provides the order result via polling; everything above that — real-time delivery to your customers, internal event buses, stall alerts — is your infrastructure to build. The pattern is straightforward and robust once it is in place. Request access to get started.

Get FoxReload API access

Related articles