Skip to main content
Networks fail. Clients time out. Users double-click checkout buttons. The Idempotency-Key header is how you guarantee that no matter how many times you retry a request, Yonne creates the order exactly once.

How it works

Send a unique string in the Idempotency-Key header on every POST /api/v1/external/create-order call:
POST /api/v1/external/create-order
X-API-Key: yonne_live_xxxxxxxxx
Idempotency-Key: order-100245-attempt-1
Content-Type: application/json
If Yonne receives a second request with the same key within 24 hours, it returns the original success response instead of creating a second order. The response may include:
{
  "idempotent_replay": true
}

Generating a good idempotency key

A good key encodes your internal order ID plus a retry attempt counter so you can distinguish retries from genuinely new orders:
order-{your_internal_order_id}-attempt-{n}
Examples:
  • order-WEB-100245-attempt-1 — first attempt
  • order-WEB-100245-attempt-2 — retry after timeout
Never generate a new random key on each retry. If you do, Yonne will create a new order every time, and you’ll have duplicates.

The retry decision tree

Request failed or timed out?

├─ Network error / timeout / 5xx
│   └─ Retry with the SAME Idempotency-Key ✓

├─ 401 Unauthorized
│   └─ Fix your API key — do not retry ✗

├─ 400 Validation error (missing coordinates, etc.)
│   └─ Fix the payload — do not retry ✗

├─ 402 Insufficient Funds
│   └─ Top up wallet, then retry ✗ (do not retry blindly)

└─ 422 ERR_NO_CAPACITY_AVAILABLE
    └─ Wait for capacity, then retry ✗ (do not retry blindly)

Safe vs. unsafe retries

RequestSafe to retry?Condition
GET /validateYesAlways safe
GET /quoteYesAlways safe
POST /create-orderYes, with conditionsOnly with the same Idempotency-Key
POST /create-orderNoDo not retry on 402 or 422 without resolving the underlying issue
GET /track/:idYesAlways safe

Handling a timed-out create-order

If your client times out before receiving a response, you don’t know whether Yonne created the order. Here’s what to do:
async function createOrderWithRetry(payload, internalOrderId, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const response = await fetch("https://api.yonne.app/api/v1/external/create-order", {
        method: "POST",
        headers: {
          "X-API-Key": process.env.YONNE_API_KEY,
          "Content-Type": "application/json",
          "Idempotency-Key": `order-${internalOrderId}-attempt-${attempt}`
        },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(10000) // 10s timeout
      });

      const data = await response.json();
      if (data.success) return data;

      // Business errors — do not retry
      if (response.status === 402 || response.status === 422) throw data;

    } catch (err) {
      if (attempt === maxAttempts) throw err;
      await new Promise(r => setTimeout(r, 1000 * attempt)); // backoff
    }
  }
}

What to log on every order attempt

Always log these fields so you can reconcile duplicate concerns in support:
  • merchant_reference_id — your internal order ID
  • Idempotency-Key — the exact key you sent
  • order_id — from the Yonne response
  • tracking_id — from the Yonne response
  • HTTP status code
  • Full error payload on failure