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

# Idempotency

> Safe retries on POST + PATCH + DELETE via the Idempotency-Key header.

Network blips happen. Your code retries the same `POST /action-items` request twice — without idempotency that creates two duplicate items. SiteVisit mirrors Stripe's pattern: pass an `Idempotency-Key` header on any write, and we'll guarantee the request runs **at most once**, returning the original response for any retries within the next 24 hours.

## How to use it

Pass a header that's unique to the *intent* of the request:

```bash theme={null}
curl -X POST https://sitevisit.app/api/v1/action-items \
  -H "Authorization: Bearer sv_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-42-action-1" \
  -d '{"site_visit_id":"cly5...","title":"Replace bulb"}'
```

Generate keys however you like — UUIDs, hashes of inputs, monotonic counters — as long as the same logical write always uses the same key. 255 character max.

## What the server does

```
First call with key X → run the handler, store the response under (your_user_id, X)
Same key X, same body → 200/201 from the stored response. No re-run. Header: Idempotent-Replay: true
Same key X, DIFFERENT body → 422 idempotency_key_in_use (client bug — use a fresh key)
Same key X, request still mid-flight (concurrent retries) → 409 idempotency_in_progress (retry in ~1s)
After 24 hours → key is purged, treated as fresh
```

The store is **scoped to your account**, so two customers can pick the same key without colliding.

## When to use it

* **Always on write retries.** If your code has any retry / circuit-breaker logic around POST/PATCH/DELETE calls, generate a key once per logical attempt and pass it on every retry. Otherwise you'll create dupes on flaky networks.
* **On webhooks fanning into the API.** When your webhook handler kicks off a SiteVisit write, use the webhook's `delivery_id` (or a deterministic hash of the payload) as the key — receivers retry, but you'll only write once.
* **On batch jobs.** When a nightly script reconciles state, key each write by `(job_run_id, record_id)` so a partial run can resume safely.

## When you can skip it

* `GET` and other read-only requests — naturally idempotent.
* `DELETE` — naturally idempotent (deleting the same id twice returns the same observable result either way). The header is accepted but doesn't do anything special.
* One-off scripts you'll never retry. Even then, it's free insurance.

## Detecting a replay

Successful replays come back with the `Idempotent-Replay: true` response header. Useful in logs to distinguish a network retry from a genuinely repeated user action.

```
HTTP/2 201
content-type: application/json
idempotent-replay: true
x-request-id: req_b9b0122805a1e7f351a8cca6
```

## Edge cases

**Same key, different body → 422.** Means your client cached a key longer than it should have and is now sending a *different* request under it. The server can't honor "return the original response" because the original response doesn't match the new request — that would be lying. Use a fresh key.

**Concurrent retries → 409.** If two requests with the same key arrive at the same instant, one wins the reservation race and runs the handler; the other returns `409 idempotency_in_progress`. A short backoff + retry will succeed (it'll then get the stored response from the winner).

**Past TTL → fresh request.** After 24 hours we purge the record. If you retry with the same key 25 hours later, it'll run as a fresh request (which may create a duplicate). Either set your retry window shorter than 24h, or use longer-lived keys + a job-id scoping scheme.
