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

# Idempotency

> Replay a logical mutation safely within Journeybee's 24-hour Redis authority window

# Idempotency

Every `POST` request to the Journeybee API must include a unique
`Idempotency-Key` header. The SDK also sends one for `PUT`, `PATCH`, and
`DELETE`; raw non-POST clients remain compatible without a header unless they
explicitly opt in.

Journeybee uses Redis as a 24-hour replay authority. Within that window, a
retry of the same logical request receives the recorded terminal response. It
reduces ambiguity after a lost response, but it is not a database transaction
and cannot promise no duplicate outside the replay window or across a crash
boundary.

## Quick start

Generate a UUID per logical operation and send it with your request:

```bash theme={null}
curl -X POST https://api.journeybee.io/v1/leads \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 7f3a9b2c-4e8d-4a5b-9c1d-8e5f2a3b4c5d" \
  -H "Content-Type: application/json" \
  -d '{ "first_name": "Jane", "email": "jane@acme.test", "partnership_uuid": "<partnership-uuid>" }'
```

If the network times out, retry with the **same key and exact request**. Within
the replay window, Journeybee returns the recorded response instead of running
the handler again.

## Rules

| Condition                                                                 | Response                                                    |
| ------------------------------------------------------------------------- | ----------------------------------------------------------- |
| First request with a new key                                              | Runs the handler and records its terminal response for 24h  |
| Retry with same key + same canonical request                              | Replays the cached response (same status + body)            |
| Retry with same key + a different request                                 | `422` `idempotency_conflict`                                |
| Retry while the first request is still running                            | `409` — tells you to back off and retry                     |
| Handler-produced terminal 4xx, 429, or 5xx response                       | Cached and replayed; use a new logical ID for a new attempt |
| Schema validation or authorization failure before idempotency acquisition | Not cached; the key is not consumed                         |
| Missing header on any POST                                                | `400` — header is required                                  |
| Redis/store failure                                                       | `503` `idempotency_store_error`; the request fails closed   |

Fastify validates request schemas before idempotency acquisition, and live
authorization guards run before a cached response can be replayed. A schema
validation or authorization failure is therefore not cached and does not
consume the key: after correcting the request or restoring access, retry with
the same key. Only responses produced after idempotency acquisition, including
terminal handler `4xx`, `429`, and `5xx` responses, are recorded.

## Key format

* 1–255 characters
* Alphanumeric, underscore, hyphen (`[A-Za-z0-9_-]`)
* UUIDs recommended — they're collision-free and easy to generate
* A key is scoped by company, stable actor fingerprint, canonical operation,
  and logical ID, so the same logical ID can be used for different operations

The canonical request includes the HTTP method, OpenAPI operation identity,
path parameters, normalized query parameters, and canonical JSON body. Object
key order does not change the request identity.

## Retention

Each terminal response is retained in Redis for **24 hours**. After that, the
same logical ID is no longer protected by this replay authority and may run
again.

## What to use as a key

Pick a value that uniquely identifies the **logical** operation the client
intends, not the physical HTTP request:

* ✅ `crm-lead-7841-sync` — one key per lead you're syncing.
* ✅ A UUID generated when the user clicked "Save".
* ❌ `Date.now()` — changes on every retry, defeats deduplication.
* ❌ A constant string — every request collides.

## Client examples

### Node.js

```javascript theme={null}
import { randomUUID } from "node:crypto";

async function createLead(payload) {
  const key = randomUUID();

  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch("https://api.journeybee.io/v1/leads", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.JOURNEYBEE_API_KEY}`,
        "Idempotency-Key": key,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    });

    if (res.status === 409) {
      // In flight — back off and retry with the SAME key.
      await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
      continue;
    }
    return res.json();
  }
  throw new Error("createLead: exhausted retries");
}
```

### Python

```python theme={null}
import uuid
import time
import requests

def create_lead(payload, api_key):
    key = str(uuid.uuid4())
    for attempt in range(3):
        response = requests.post(
            "https://api.journeybee.io/v1/leads",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Idempotency-Key": key,
            },
            json=payload,
        )
        if response.status_code == 409:
            time.sleep(0.5 * 2 ** attempt)
            continue
        return response.json()
    raise Exception("exhausted retries")
```

## Mutation methods

`POST` always requires `Idempotency-Key`. Headerless `PUT`, `PATCH`, and
`DELETE` requests retain their legacy behaviour. Supplying a valid header on
those methods enables the same replay protection, and the Journeybee SDK does
this automatically for all mutation methods without overwriting an explicit
key.

## Reliability boundary

Idempotency is a Redis replay protocol, not an atomic transaction with the
business database. It cannot guarantee that a mutation was never applied if
the 24-hour window expires or a failure occurs at the database/Redis crash
boundary. Preserve the logical ID for a lost-response retry; use a new logical
ID only when intentionally starting a new operation.

## Related

* [Authentication](/guides/authentication) — API key setup
* [Errors](/guides/errors) — error response shapes
