# Idempotency

> Send an Idempotency-Key with every verification so network retries return the original result and are never billed twice.

Networks fail. A request can time out after Speedmap has already run the check, and a naive retry would run (and bill) it again. An idempotency key makes retries safe: Speedmap remembers the first result for that key and returns it again.

Add the header to every `POST` to a product endpoint:

```http
Idempotency-Key: 4f9d3c1e-8a2b-4c7d-9e0f-1a2b3c4d5e6f
```

_cURL_

```bash
curl -X POST http://localhost:4000/v1/verify/account \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "account_number": "0123456789",
  "bank_code": "058",
  "match": {
    "name": "Clarke Griffin"
  }
}'
```

_Node.js_

```javascript
const res = await fetch("http://localhost:4000/v1/verify/account", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    "account_number": "0123456789",
    "bank_code": "058",
    "match": {
      "name": "Clarke Griffin"
    }
  }),
});

const verification = await res.json();
if (!res.ok) throw new Error(verification.error.message);
console.log(verification.status, verification.match);
```

_Python_

```python
import os, uuid, requests

res = requests.post(
    "http://localhost:4000/v1/verify/account",
    headers={
        "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "account_number": "0123456789",
        "bank_code": "058",
        "match": {
            "name": "Clarke Griffin"
        }
    },
    timeout=30,
)
res.raise_for_status()
verification = res.json()
print(verification["status"], verification["match"])
```

## Rules

- **Format.** Any string of 8 to 128 characters. A UUID v4 is ideal. Anything else returns `400 invalid_idempotency_key`.
- **Scope.** Keys are scoped to your workspace and mode: the same key in sandbox and live refers to two different requests.
- **Same key, same body.** You get the original verification back, with the same `reference_id`, and you are not charged again. If the original is still pending you get `202` and the pending verification.
- **Same key, different body.** Rejected with `409 idempotency_key_reused`. Generate a new key for every new check.
- **Failed attempts release the key.** If the first attempt failed (for example `503 verification_unavailable` during a provider outage), the key is released and the retry runs the check again. A failed attempt is never billed.
- **Where it applies.** Product endpoints (`POST /v1/verify/*` and `POST /v1/signals`). Bulk jobs assign a key to every row for you.

**409 Conflict**

```json
{
  "error": {
    "code": "idempotency_key_reused",
    "message": "Idempotency-Key was already used with a different request"
  }
}
```

## A retry loop

Generate the key once per logical check, **outside** the retry loop, and reuse it on every attempt:

_Node.js_

```javascript
async function verifyWithRetry(path, body, attempts = 4) {
  const key = crypto.randomUUID(); // one key for every attempt
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(`${process.env.SPEEDMAP_API_URL}${path}`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SPEEDMAP_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    }).catch(() => null); // network error: retry

    if (res && res.status < 500 && res.status !== 429) return res.json();
    const wait = Number(res?.headers.get("Retry-After")) || 2 ** i;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error("Speedmap unavailable after retries");
}
```

_Python_

```python
import os, time, uuid, requests

def verify_with_retry(path, body, attempts=4):
    key = str(uuid.uuid4())  # one key for every attempt
    for i in range(attempts):
        try:
            res = requests.post(
                os.environ["SPEEDMAP_API_URL"] + path,
                headers={
                    "Authorization": f"Bearer {os.environ['SPEEDMAP_API_KEY']}",
                    "Idempotency-Key": key,
                },
                json=body,
                timeout=30,
            )
            if res.status_code < 500 and res.status_code != 429:
                return res.json()
            wait = int(res.headers.get("Retry-After", 2 ** i))
        except requests.RequestException:
            wait = 2 ** i
        time.sleep(wait)
    raise RuntimeError("Speedmap unavailable after retries")
```

---

Source: https://speedmap-delta.vercel.app/docs/idempotency
