# Idempotency and retries

> Retry Hodoflow operations safely by separating JSON:API response replay, webhook delivery deduplication, read retries, and permanent failures.

Hodoflow has two distinct duplicate-suppression contracts and ordinary retry
guidance. Do not reuse one surface's behavior as a promise for another.

## JSON:API unsafe writes

Send a unique `Idempotency-Key` on `POST`, `PUT`, `PATCH`, or `DELETE` requests
under `/api/json`. The key is scoped by tenant and service-account principal.
The request fingerprint includes method, path, and raw body.

```http
Idempotency-Key: dashboard-create-20260731-001
```

- First request: reserves the key and runs normally.
- Same key and same request: replays the stored response with
  `idempotency-replayed: true`.
- Same key and different request: returns `409 idempotency_key_reused`.
- Concurrent duplicate while the first is running: returns
  `409 idempotency_key_in_progress`.
- A `5xx` response is not retained, so a transient failure remains retryable.

Completed responses are retained for 24 hours by default; deployments can
configure the window. Generate keys from a stable business operation ID, not a
random value on every retry. Never put a credential or tenant secret in a key.

## Webhook delivery deduplication

Webhook ingestion uses a stable sender delivery identifier from
`Idempotency-Key` or a supported provider header. It retains accepted delivery
identity for seven days. A duplicate receives `202` with
`{"status":"duplicate"}` and no new run ID.

When no stable header exists, Hodoflow falls back to a time-bucketed hash of the
trigger token and raw body. That protects short retries but is not a substitute
for a provider delivery ID. See [Retries and troubleshooting](/webhook-ingestion/retries-troubleshooting).

## Retry matrix

| Result | Retry? | Action |
|---|---|---|
| Network timeout before any response | Yes | Retry with the same idempotency/delivery key |
| `429` | Yes | Wait the `Retry-After` duration, then retry |
| `500` or `503` | Usually | Exponential backoff with jitter and a bounded attempt/time budget |
| `401` | Once after exchange | Discard cached token, exchange, retry once; then alert |
| `403` | No | Correct scopes or run-as permissions |
| `404` | No unchanged | Correct path/ID or object access |
| `409` idempotency conflict | No | Reconcile key reuse; do not generate a new key blindly |
| `413` or `422` | No unchanged | Correct body size or validation failure |

Reads are naturally repeatable, but still need bounded backoff. GraphQL
`factQuery` is read-only and does not use JSON:API response replay. A GraphQL
validation or complexity error is permanent until the document or variables
change.

## Operational rules

- Cap attempts and total elapsed time.
- Add jitter so many workers do not retry together.
- Respect the credential-wide rate budget.
- Preserve method, path, body bytes, and idempotency key across retries.
- Record `x-request-id`, status, attempt, and non-secret operation identity.
- Never log bearer tokens, client secrets, trigger URLs, or sensitive bodies.

Practice the contract in [Perform an idempotent write](/guides/idempotent-write).
