FulfillbotDocs
Menu
MarkdownDownload

Idempotency

Networks fail. When a write times out you can't tell whether it happened. Send an Idempotency-Key header and you can safely send the same request again: we run it once and return the saved result to every repeat.

How it works

  • Use a new, unique key for each new request, for example a UUID. A key is 1 to 255 visible ASCII characters (no spaces); anything else returns 400 invalid_idempotency_key. Where the key is required, a request without it returns 400 idempotency_key_required.
  • Keys belong to your account (all your API keys share them) and are kept for 24 hours. After that, the same key counts as new.
  • Once the request has run, we save its response, whatever the status: success or a business error. A repeat with the same key and the same request returns that saved response with the same status and the header Idempotency-Replayed: true. It doesn't run again and doesn't change anything.
  • "The same request" means the same method, path, query string (in any order) and exactly the same body bytes. The same key with anything different returns 422 idempotency_key_reused.
  • Checks that fail before the request runs (the key, the rate limit, the permission, account on hold, validation) don't use up the idempotency key: fix the request and send it again with the same key.
  • While the first request is still running, a repeat returns 409 request_in_progress.
  • Replays still count against the rate limits, including the daily write limit: the limits are checked before the key, so a repeat uses up a request like any other (see Rate limits). On a network error or timeout, retry with the same key rather than a new one.

When the result is unknown

We never run a request again on our own when its result is unknown. If a write returned a 5xx, or 409 request_in_progress doesn't go away, the write may have happened. For example, an order can be saved even though its response failed.

  1. Read the current state with GET. For an order, list your orders and look for your storeOrderNo.
  2. If the write didn't happen, send it again with a new key.
  3. If it did happen, carry on with what you found.

A network error or timeout is different: the request may still be running, so retry with the same key. Only after a saved 5xx, or a request_in_progress that lasts through a few tries, is a new key the way to try again, and only if the write didn't happen. A saved 5xx keeps coming back for the same key.

Example: creating an order safely

import { randomUUID } from "node:crypto";

const order = { storeOrderNo: "1042", recipientName: "Alex Morgan", address1: "12 Example Street", city: "Springfield", postalCode: "62701", countryCode: "US", lines: [{ customerSku: "TEE-BLK-M", qty: 2 }] };
// Create the key once per order and store it with the order, so a retry after a crash reuses it.
const idempotencyKey = randomUUID();
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let lastProblem = "no attempt was made";
for (let attempt = 1; attempt <= 5; attempt += 1) {
  let response;
  try {
    response = await fetch("https://api.rephut.com/v1/orders", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.FULFILLBOT_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
      body: JSON.stringify(order),
    });
  } catch (error) {
    lastProblem = `network error: ${error.message}`;
    await sleep(1000 * attempt);
    continue; // The first request may still be running. Retry with the same key, never a new one.
  }
  if (response.status === 429) {
    lastProblem = "rate limited";
    await sleep(Number(response.headers.get("retry-after") ?? "1") * 1000);
    continue; // Rate limits are checked before the key is used.
  }
  const result = await response.json();
  if (result.code === "request_in_progress") {
    lastProblem = "the first request is still running";
    await sleep(1000 * attempt);
    continue; // Wait a moment and ask again with the same key.
  }
  if (response.ok) {
    console.log("Order", result.data.number, response.headers.get("idempotency-replayed") ? "(replayed)" : "(created)");
    process.exit(0);
  }
  if (response.status >= 500) {
    throw new Error(`Result unknown (HTTP ${response.status}). Look for the order with GET /v1/orders before using a new key.`);
  }
  throw new Error(`Rejected: ${result.code} ${result.error}`);
}
throw new Error(`Gave up after 5 attempts (${lastProblem}). The order may exist: look for it with GET /v1/orders before using a new key.`);