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.
Which endpoints use it
| Endpoint | Idempotency-Key |
|---|---|
POST /v1/sku-mappings |
Optional |
POST /v1/orders |
Required |
PATCH /v1/orders/{id} |
Optional |
POST /v1/orders/{id}/cancel |
Optional |
POST /v1/shipments |
Required |
POST /v1/shipments/{id}/cancel |
Optional |
POST /v1/shipments/{id}/confirm |
Optional |
POST /v1/requests/sourcing |
Required |
POST /v1/requests/reorder |
Required |
Other endpoints ignore the header.
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 returns400 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.
- Read the current state with
GET. For an order, list your orders and look for yourstoreOrderNo. - If the write didn't happen, send it again with a new key.
- 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.`);