# Idempotency

> Send an Idempotency-Key on every call that spends credits so a retry returns the first answer instead of charging twice.

Networks fail. If a reveal times out you cannot know whether it was charged. An `Idempotency-Key` makes the retry safe: Funnelfeedr remembers the first successful answer for that key and returns it again instead of running the call twice.

```http
POST /external/v1/contacts/reveal HTTP/1.1
Host: api.funnelfeedr.com
Authorization: Bearer ff_live_...
Content-Type: application/json
Idempotency-Key: 9a3e7c1d-2b4f-4d8e-a6c0-5f1b3e9d7a28
```

## When to send one

| Call | `Idempotency-Key` |
|---|---|
| `POST /contacts/reveal`, dry runs included | **Required.** Without it: `400 idempotency_key_required`. |
| `POST /organizations/match`, `POST /people/match` or `POST /organizations/switchboards/match` that reveals (`reveal` set, `dryRun` false) | **Required.** Without it: `400 idempotency_key_required`. |
| `POST /organizations/match`, `POST /people/match` or `POST /organizations/switchboards/match` without a reveal, or as a dry run | Optional. Honoured when sent. |
| Every other call: the `GET`s | Ignored. |

## The key

- Any 1 to 255 visible ASCII characters; anything else is `422 validation_failed`. Use a new UUID for each **logical** request.
- Generate it once, before the first attempt, and reuse it for every retry of that request.
- Idempotency keys are kept per API key: two integrations cannot collide.

## What happens

| Situation | Result |
|---|---|
| First request with the key | Runs normally. A successful (`2xx`) answer is stored for 24 hours. |
| Same key, same request, after it succeeded | The stored status and body, byte for byte, with the header `Idempotent-Replayed: true`. Nothing runs and nothing is charged again. |
| Same key while the first request is still running | `409 idempotency_key_in_use` (`retryable: true`). Wait a moment and send it again to get the first request's answer. |
| Same key, different request | `422 idempotency_key_reused`. Use a new key for a new request. |
| Same key, after the first attempt **failed** | Runs again. Only successful (`2xx`) answers are stored, and a failed reveal charged nothing, so this is safe — and it is what you want after topping up following a `402`. |
| After 24 hours | The key is forgotten; a repeat runs as a new request. |

"Same request" means the same method, path, query string and **body bytes**, checked per API key. Re-serialising your JSON with a different key order or spacing makes it a different request. Keep the body you sent and resend exactly that.

## A retry loop

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

def reveal(items, info_types, max_credits):
    key = str(uuid.uuid4())  # one key for every attempt of this logical request
    body = {"items": items, "infoTypes": info_types, "maxCredits": max_credits}
    for attempt in range(5):
        response = requests.post(
            "https://api.funnelfeedr.com/external/v1/contacts/reveal",
            headers={
                "Authorization": f"Bearer {os.environ['FUNNELFEEDR_API_KEY']}",
                "Idempotency-Key": key,
            },
            json=body,
            timeout=60,
        )
        if response.ok:
            return response.json()
        problem = response.json()
        if not problem.get("retryable"):
            raise RuntimeError(f"{problem['code']}: {problem['detail']}")
        time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))
    raise RuntimeError("Gave up after 5 attempts")
```

`requests` serialises the same `body` dict identically on every attempt, so the retries match the first request.
