# Idempotency

> How retry-safe mutations work in the Hatched API — the Idempotency-Key contract, what the platform caches, and how the SDK uses it automatically.

Source: https://docs.hatched.live/docs/concepts/idempotency

Hatched accepts an `Idempotency-Key` header on every mutating endpoint
(`POST`, `PUT`, `PATCH`, `DELETE`). When you supply one, the platform
guarantees that retries of the *same* request return the *same* response
— body, status, and side effects — for 24 hours.

This is the same shape Stripe, Slack, and AWS use. Apply it whenever a
network blip, timeout, or client crash could cause you to repeat a
request that should run exactly once.

## How it works

1. You send `Idempotency-Key: <unique-string>` alongside the request.
2. The interceptor hashes the request method, path, and body to produce
   a fingerprint.
3. **Cache miss** → handler runs normally. The response (body + status)
   is cached under `idem:{customer_id}:{key}` for 24 hours.
4. **Cache hit, matching fingerprint** → the cached response replays.
   `Idempotency-Replayed: true` is set on the response so you can tell
   the difference in a log line.
5. **Cache hit, *different* fingerprint** → `409 idempotency_key_conflict`.
   You reused the key for a different request. Use a fresh key.

Failed responses (`4xx` / `5xx`) are **not** cached — retries should
produce a fresh attempt. Cache only sticks on `2xx` success.

## Picking a key

A good key is:

- **Unique per logical action.** Use the business id (`order_<id>`,
  `lesson_<lessonId>_user_<userId>`), not a clock value.
- **Stable across retries.** The whole point: a retry must reuse the
  original key.
- **Opaque to the user.** Never expose it in a URL.

UUID v4 works fine when you don't have a natural business id. The SDK
auto-generates one for you (next section).

## SDK behaviour

`@hatched/sdk-js` injects `Idempotency-Key` automatically on mutating
resource methods. Most callers never have to think about it:

```ts
// Auto-generated, retried safely on transient network errors.
await hatched.eggs.create({ userId: 'user_42', ensure: true });
```

To pass an explicit key from your own business logic — recommended for
operations you may retry from a different process — set it via the
options object:

```ts
await hatched.events.send(
  {
    eventId: 'lesson_42_user_99',
    userId: 'user_99',
    type: 'lesson_completed',
    properties: { … },
  },
  {
    headers: { 'Idempotency-Key': `lesson:42:user:99` },
  },
);
```

## What this is *not*

- **Not event-level deduplication.** Use `eventId` on
  [send events](/docs/guides/send-events#idempotency) for that — it lives
  in the rule-engine, not the HTTP layer.
- **Not webhook idempotency.** That contract is on the consumer side —
  see [Webhook delivery](/docs/concepts/webhook-delivery#idempotency-in-detail).
- **Not a queue.** Replays return the cached response immediately; they
  don't re-enqueue work.

## When to skip it

Read-only requests (`GET`, `HEAD`) ignore the header — they're naturally
idempotent. One-shot operations that intentionally produce a side effect
each time (charging a credit grant, issuing a unique token) should reuse
a *different* key per attempt rather than letting the platform replay.

If you need a route that explicitly opts out of idempotency caching,
flag it on your side — Hatched will keep the header behaviour but you
can set the key to a unique UUID per attempt to force a fresh run.
