B2B platform for digital goods

Safe Order Retries in B2B APIs 2026: Status-Check-Before-Retry Pattern

FoxReload has no Idempotency-Key header. The safe retry pattern is: check order status before creating a new one. Client-side dedup and storage strategies explained.

Safe Order Retries in B2B APIs 2026: Status-Check-Before-Retry Pattern

Idempotency keys are the technique that makes any POST request in a B2B API retry-safe. Without them, every network timeout on the client turns into a "did the order create or not?" question — and in 30% of cases the answer is "both". The FoxReload API does not support an Idempotency-Key header. This article explains the correct substitute pattern — status-check-before-retry — along with client-side deduplication strategies.

1. Why the FoxReload API has no idempotency keys

FoxReload's order endpoint at POST /api/orders does not accept an Idempotency-Key header. Each call creates a new order. This means:

  • A single retry after a network blip can create two orders
  • Your balance is debited twice
  • Two sets of codes are delivered

The solution is entirely on the client side.

2. The status-check-before-retry pattern

Before retrying a POST /api/orders that returned a timeout or ambiguous error, check whether the order already exists:

async function safeCreateOrder(
  items: OrderItem[],
  apiKey: string,
): Promise<Order> {
  // Step 1: Record intent in your own DB before calling the API
  const clientRef = randomUUID();
  await db.pendingOrders.insert({ clientRef, items, createdAt: new Date() });

  try {
    const res = await fetch('https://public-api.foxreload.com/api/orders', {
      method: 'POST',
      headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({ items }),
      signal: AbortSignal.timeout(30_000),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const order = await res.json();
    await db.pendingOrders.update(clientRef, { orderId: order.id });
    return order;

  } catch (err) {
    // Step 2: On failure, check if the order was created
    const existing = await db.pendingOrders.findByRef(clientRef);
    if (existing?.orderId) {
      // Order was created; poll its status
      return pollOrder(existing.orderId, apiKey);
    }

    // Step 3: Check recent FoxReload orders for a match
    const recent = await fetchRecentOrders(apiKey); // GET /api/orders?limit=10
    const match = findMatchingOrder(recent, items);
    if (match) {
      await db.pendingOrders.update(clientRef, { orderId: match.id });
      return match;
    }

    // Step 4: Only retry if confirmed not created
    throw err; // caller can retry after delay
  }
}

3. Client-side deduplication storage

The key insight: store the order intent before making the API call. This gives you a paper trail to check against on failure.

Storage Latency TTL Cost / 1M records
Postgres UNIQUE 5–8ms $0.10
Redis SETNX <2ms 24h $0.40
DynamoDB ConditionExpression 8–12ms 24h $1.25
Memcached CAS <1ms 24h $0.30

For most integrations, Postgres with a UNIQUE constraint on client_ref is sufficient and gives a permanent audit trail.

4. Comparing dedup approaches

Approach Prevents duplicates Audit trail Extra DB write?
Idempotency-Key (not available on FoxReload) Yes Server-side No
Status-check-before-retry Yes Your DB Yes
Client-side SETNX gate Yes (within TTL) Redis Yes
Blind retry (never do this) No No No

The status-check approach adds one DB write per order attempt. At any scale below millions of orders per day, this is entirely acceptable.

5. Comparing dedup storage backends

// Postgres approach (recommended for audit)
await db.query(`
  INSERT INTO order_intents (client_ref, items_hash, created_at)
  VALUES ($1, $2, NOW())
  ON CONFLICT (client_ref) DO NOTHING
`, [clientRef, hashItems(items)]);

6. What if you hit a race between two simultaneous requests?

If two workers try to create the same order simultaneously, a database UNIQUE constraint on client_ref will reject one of them at the DB level before either reaches FoxReload. This is the cleanest solution:

CREATE TABLE order_intents (
  client_ref UUID PRIMARY KEY,
  order_id UUID,
  items_hash TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  resolved_at TIMESTAMPTZ
);

Only the first inserter proceeds to call the API. The second reads the existing record and monitors the outcome.

CTA

The FoxReload API does not have idempotency keys — implement status-check-before-retry and a client_ref store in your own system. The full order API reference is available after onboarding — request access.

Frequently asked questions

Does FoxReload support an Idempotency-Key header?
No. FoxReload has no Idempotency-Key header. If you send POST /api/orders twice you will create two separate orders and your balance will be debited twice. The correct pattern is to check order status before retrying.
What should I do if POST /api/orders times out?
Call GET /api/orders?limit=10 and look for an order matching your expected item and timestamp. If found, use that order — do not create a new one. If not found, it is safe to retry the POST.
How do I implement client-side deduplication?
Before calling POST /api/orders, write a pending record to your database with a unique client_ref. On any failure, query your DB for that client_ref. If an order_id is already associated, poll its status; if not, retry the POST.
What is the risk of a double-order?
Each order debits your FoxReload balance and delivers codes to customers. A duplicate order means double spend and double delivery. Without an idempotency key, status-check-before-retry is the only protection.
Get FoxReload API access

Related articles