# 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`](https://docs.rephut.com/developers/reference/sku-mappings#map-your-sku-to-one-of-your-products) | Optional |
| [`POST /v1/orders`](https://docs.rephut.com/developers/reference/orders#create-an-order) | Required |
| [`PATCH /v1/orders/{id}`](https://docs.rephut.com/developers/reference/orders#change-an-order-that-needs-attention) | Optional |
| [`POST /v1/orders/{id}/cancel`](https://docs.rephut.com/developers/reference/orders#cancel-an-order-that-needs-attention) | Optional |
| [`POST /v1/shipments`](https://docs.rephut.com/developers/reference/shipments#request-a-shipment-from-your-stock) | Required |
| [`POST /v1/shipments/{id}/cancel`](https://docs.rephut.com/developers/reference/shipments#cancel-a-shipment-you-requested) | Optional |
| [`POST /v1/shipments/{id}/confirm`](https://docs.rephut.com/developers/reference/shipments#confirm-a-shipment-s-freight-quote) | Optional |
| [`POST /v1/requests/sourcing`](https://docs.rephut.com/developers/reference/requests#ask-us-to-source-new-products) | Required |
| [`POST /v1/requests/reorder`](https://docs.rephut.com/developers/reference/requests#ask-us-to-restock-products-at-your-agreed-prices) | 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 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](https://docs.rephut.com/developers/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

```js
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.`);
```
