Idempotency
Every write requires an Idempotency-Key. It is not optional and a request without one is refused.
POST /api/public/v1/operations/block_dates
Idempotency-Key: 8f14e45f-ceea-467a-9b23-1f0c47a8d2e1
Use a UUID, one per logical operation. Generate it before the first attempt and reuse it for every retry of that same operation.
What it buys you
A network timeout tells you nothing about whether the server acted. Without idempotency the only options are to retry and risk doing it twice, or not retry and risk not doing it at all. Neither is acceptable when the operation blocks a calendar or cancels a stay.
With a key, retrying is safe. Haven records the outcome against the key and replays it.
Semantics
Same key, same payload, operation completed. You get the original response, byte for byte, including its status. The work is not repeated.
Same key, same payload, original still running. You get 409. Wait briefly and retry; the first attempt is in flight.
Same key, different payload or different operation. You get 409 with a mismatch reason. This is a bug in your client: a key identifies one operation with one input, and reusing it for something else would mean Haven either performed the wrong write or returned the wrong answer.
New key. Runs normally.
Keys are scoped to your application and the account you are acting for. Another integration using the same UUID against the same host cannot collide with you.
Records are retained 30 days. A retry beyond that executes as a fresh request.
Getting it right
Generate the key at the point you decide to act, not at the point you send. A key generated inside a retry loop is a new key on every attempt, which is the same as having none.
key = str(uuid.uuid4()) # once, outside the loop
for attempt in range(5):
response = post(url, json=body, headers={"Idempotency-Key": key})
if response.status_code < 500 and response.status_code != 429:
break
sleep(backoff(attempt))
Persist the key alongside whatever queued the work, if the work survives a process restart. A worker that crashes mid-write and regenerates its key on restart will duplicate the operation.
Replays are charged
A replayed response counts against your rate limit. Idempotency is a correctness mechanism, not a free retry channel, and a client hammering one key would otherwise have an unmetered path to the origin.
What is not idempotent
Sending a message to a guest is not, and this is called out because the failure mode is embarrassing rather than merely wrong. If messaging:send returns a timeout, the message may have gone. Retrying with the same key is still the right move, since a completed send replays rather than repeats, but a send that failed after dispatch and before the record was written can produce a duplicate message to a real person. Prefer surfacing the ambiguity to the host over silently retrying.