> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usehasp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> How the Idempotency-Key header makes retries safe, its scope, and what it does and does not cover.

Network failures happen mid-request — you sent a `POST /v1/webhooks` and the connection dropped before the response arrived. Did the webhook get created? Retrying blind risks creating it twice. The `Idempotency-Key` header solves this: retry safely, and HASP guarantees the mutation only happens once.

## Using it

Send an `Idempotency-Key` header on any JSON `POST`, `PATCH`, or `PUT` request:

```bash theme={null}
curl -X POST https://api.usehasp.com/v1/webhooks \
  -H "Authorization: Bearer hasp_api_live_<key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{"url": "https://example.com/hooks/hasp", "events": ["record.created"]}'
```

Generate the key client-side — a UUID or any sufficiently random string works. Reuse the **same** key when retrying the **same** logical operation; use a fresh key for a new one.

## What happens on replay

| Scenario                                                    | Result                                                                                                                     |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Same key, same request body, within 24 hours                | The original response is returned unchanged, with an `Idempotent-Replayed: true` header. The mutation does not re-execute. |
| Same key, **different** request body                        | `409 IDEMPOTENCY_CONFLICT` — HASP refuses to guess which body you meant.                                                   |
| Same key, original request still in flight (a genuine race) | `409 IDEMPOTENCY_CONFLICT` with `error.retryable: true` — retry shortly once the original completes.                       |
| Key omitted                                                 | No idempotency protection. Each request executes independently.                                                            |
| Key present, more than 24 hours since first use             | Treated as a new request — the window has expired and the key is free to reuse.                                            |

The header is entirely optional. Requests without it behave exactly as they did before this primitive existed.

## Scope and boundaries

**Applies to:** JSON request bodies on mutating `POST`/`PATCH`/`PUT` routes across `/v1` and the Data API. The key is scoped per organization + route + key value, so two different orgs — or two different routes — can reuse the same key value without colliding.

**Does not apply to:**

* **Multipart/form-encoded requests** (e.g. `POST /v1/files`). Deduping would require buffering the entire upload into memory to hash it; HASP does not pay that cost, so these requests always execute, key or no key.
* **The AI inference endpoints** — `/v1/messages`, `/v1/messages/count_tokens`, `/v1/chat/completions`, `/v1/ai/chat`. An `Idempotency-Key` sent to these is silently ignored. Their response content may carry re-identified PHI, and persisting that into the idempotency store would put it outside the governed, retention-and-crypto-shredding-aware storage path the rest of HASP's PHI handling relies on.
* **`GET`/`DELETE` requests.** `GET` is already safe to retry; `DELETE` idempotency is handled by the resource's own soft-delete semantics (a second `DELETE` on an already-deleted resource is a no-op, not an error).

**Secrets returned once are never replayed as plaintext a second time.** If the original request's response included a one-time secret (an API key's `plaintext`, a webhook's `secret`, a credential's `token`), a replay returns that field redacted (`null`) rather than handing the secret out again — consistent with those endpoints' "shown once, never persisted" guarantee. The mutation itself still dedupes correctly; only the secret re-disclosure is suppressed.

## Choosing a key

A UUID v4 is the simplest choice and what most HTTP clients generate by default. Whatever you use, derive it from something that uniquely identifies the *logical* operation in your system — e.g. an internal job ID or order ID — rather than generating a fresh key on every attempt, or retries stop being idempotent by definition.

```javascript theme={null}
import { randomUUID } from 'crypto';

async function createWebhookWithRetry(payload, maxRetries = 3) {
  const idempotencyKey = randomUUID();

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch('https://api.usehasp.com/v1/webhooks', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer hasp_api_live_...',
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey, // same key across every attempt
      },
      body: JSON.stringify(payload),
    });

    if (response.ok || response.status < 500) return response.json();

    await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
  }
}
```

See the [Error Reference](/ai-api/reference/errors#idempotency) for the `IDEMPOTENCY_CONFLICT` error shape.
