# Idempotency

## Overview

Network calls can fail in ambiguous ways. A connection can be dropped after your request has been received and processed by WatchEye but before the response reaches you. Without help from the server, retrying the request would risk creating duplicate records, double-billing your account, or sending the same downstream request twice.

To make retries safe, the WatchEye API supports the **`Idempotency-Key`** request header. When you supply this header on a write request, WatchEye guarantees that the same key sent more than once will only do the underlying work once. Any subsequent request with the same key returns the original response, byte-for-byte, with no new database changes and no new billable charges.

Sending the header is **always optional**. If you do not send it, each request is treated as a brand new request.

## Generating a Key

The `Idempotency-Key` value is a string of your choosing, up to 255 characters. It must be unique per logical operation - typically you generate a fresh UUID per request from your application:

```
Idempotency-Key: 8c4d3a18-2f63-4f9a-9f3a-9b1f5a7c2d4f
```

Two different operations should never share the same key. Reusing a key for a different request body is treated as a client error (see below).

## Which Endpoints Honour the Header

The header is honoured on **`POST` requests only**. `GET`, `PATCH` and `DELETE` are conventionally idempotent at the HTTP level already (re-sending the same `PATCH` or `DELETE` produces the same end state), so the header is silently ignored on those methods.

If you send the header on a method where it has no effect, the request is processed normally and no error is returned.

## Behaviour

For a `POST` request that includes the header, one of the following will happen:

1. **First request** - WatchEye records the key, runs the request, charges any applicable fees, returns the response, and remembers the response for **24 hours**.

2. **Retry with the same key and same body** - WatchEye returns the **original response, verbatim** (same status code, same body, same `api_reference`). No new work is done and no new charge is applied. The replay is identifiable by an additional response header:

    ```
    Idempotent-Replay: true
    ```

3. **Retry with the same key but a different body** - WatchEye returns `422 Unprocessable Entity` with the message:

    ```json
    { "message": "Idempotency-Key was reused with a different request body." }
    ```

    This is a programming error in your client - either generate a new key for the new request, or send the same body as the original.

4. **Concurrent retry while the first request is still in flight** - WatchEye returns `409 Conflict` with the message:

    ```json
    { "message": "A request with this Idempotency-Key is already being processed." }
    ```

    Wait for the first request to complete (a few seconds at most for normal calls), then retry.

5. **Original request failed (non-2xx)** - The key is released. You can re-send with the same key, and the request will run fresh. This is intentional: if validation failed the first time and you have fixed your payload, your retry should run as a normal new request.

## Detecting a Replay

Every replayed response includes the header:

```
Idempotent-Replay: true
```

Fresh responses do not include this header (the header is absent rather than `false`). If you ever need to know whether you are looking at the original outcome or a cached replay - for example, when debugging "why didn't this request seem to do anything?" - check for this header in the response.

This is especially useful for asynchronous endpoints such as `POST /v1/monitors/{uuid}/run`, where the response body alone does not tell you whether a new background job was actually queued or whether you are looking at the cached body from an earlier call.

## Storage Window

A successful (2xx) response is remembered for **24 hours**. After that the entry is discarded; a request with the same key after the window will be treated as a brand new request. In practice this is far longer than any reasonable client-side retry window.

## Worked Example

A typical "create entity then retry on network error" flow:

```http
POST /api/v1/programs/9c3e0a8e-2b9f-4f0e-8d1a-1e2b3c4d5e6f/entities HTTP/1.1
Authorization: Bearer wch_xxxx|secret
Idempotency-Key: 8c4d3a18-2f63-4f9a-9f3a-9b1f5a7c2d4f
Content-Type: application/json

{ "entity_type": "individual", "first_name": "Jane", "last_name": "Doe", "risk_level": "low" }
```

If the response never reaches you, replaying the **identical** request returns the originally-created entity, not a duplicate:

```http
HTTP/1.1 201 Created
Idempotent-Replay: true
Content-Type: application/json

{ "data": { "uuid": "...", ... }, "api_reference": "..." }
```

## Best Practices

- **Generate keys per logical operation, not per HTTP request**: if your retry loop wraps the HTTP call, the same key must be used on every attempt of that loop. A new key per attempt defeats the entire mechanism.

- **Use UUIDs**: They are easy to generate, globally unique by construction, and have no information leakage.

- **Treat the key as opaque**: WatchEye stores a hash of the key, not the key itself. Do not depend on parsing the key on either side.

- **Do not reuse a key for a different operation**: Always generate a fresh key when the request body changes.

- **Always retry on network failures with the same key**: This is what the header is for. You cannot accidentally double-charge yourself.

## Testing with API Clients (Postman, Insomnia, etc.)

Many GUI HTTP clients automatically attach a sticky `Idempotency-Key` to saved POST requests. The key is generated once when you first send the request and is then re-used on every subsequent click of **Send** until you regenerate or remove it. Combined with the 24-hour replay window, this means a single "test" request can keep replaying the same cached response for the rest of the day, even though it looks to you like you are submitting a fresh request each time.

The symptoms are easy to mistake for a server-side problem:

- The response body keeps showing the same `pending` / `created` state from your first call.
- The `api_reference` UUID is identical across every response (it should be different on every real request).
- No new database records appear and, for async endpoints, no new background job is queued.
- The `Idempotent-Replay: true` response header is present.

If you see those symptoms while testing:

1. Open the request's **Headers** tab in your client.
2. Either remove the `Idempotency-Key` header entirely (it is optional) or regenerate its value before each Send.
3. Confirm by checking that `api_reference` changes between calls and that `Idempotent-Replay` is no longer in the response.

For production traffic the sticky key is exactly what you want - it makes "retry on network error" automatically safe. It is only the interactive "click Send again" testing workflow where it surprises people.
