Skip to main content
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:
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.