# Idempotency and retries

Quattrino is deterministic: the same request, with the same authority state, returns the same decision. Retries are
safe when you follow three rules.

## 1. Binding authorizations require an `Idempotency-Key`

`POST /api/v1/authorize` with `mode: binding` **must** carry `Idempotency-Key: <string, 8+ chars>`.
Without it: `422 idempotency_key_required` - "An Idempotency-Key header is required for binding authorizations."
Dry runs may send one (then the same replay rules apply) but do not need to.

Use a key that identifies **your** unit of work (order id, task id, `"order-1001"`), not a random value per attempt -
otherwise a retry after a timeout binds twice. The SDKs generate `qtrn-<uuid>` when you pass none; pass your own for
anything you might retry.

Real behaviour (captured):

| retry | result |
|---|---|
| same key, **same body** | `200` with the **original** response, plus `"idempotent_replay": true`; same `authorization_id`; nothing new is bound or reserved |
| same key, **different body** | `409 idempotency_key_reuse` - "This Idempotency-Key was already used with a different request body" (`details.idempotency_key`) |
| same key while the first call is still running | `409 idempotent_request_in_progress` (`retryable: true`) - wait and retry with the same key |
| first call failed with an error (4xx/5xx) | the key is released; retry with the same key |

The fingerprint covers `mode, action_type, amount_minor, currency, merchant, service_id, purpose, task, recurring,
disclosures, execution, delegated_authority_id, task_id, envelope_id, subactions, commitment, attributes, action_class,
fulfillment, proof_audience` and the agent. Keys are scoped to your organization and are **not** expired.

## 2. Outcomes are single-consumption - no key needed

`POST /authorizations/{id}/outcome` consumes the authorization exactly once. A retry returns `409 outcome_already_recorded`;
read the stored result with `GET /authorizations/{id}`. Never treat the 409 as a failure of the first attempt.

## 3. Execution POSTs through Quattrino adapters require a key

`POST /agent/transactions`, `/agent/services/{id}/purchase`, `/agent/merchant-payments/purchase` (sandbox adapters) require
`Idempotency-Key` on every call (`422 idempotency_key_required` - "required for every economic request"). The SDK
`execute()` generates `qtrn-exec-<uuid>` if you pass none.

## What never to retry

| response | retry? |
|---|---|
| `decision: DENIED` | **no.** Same input -> same denial. Altered wording / split amounts are recorded. |
| `decision: APPROVAL_REQUIRED` | **no.** Poll or subscribe ([APPROVAL_REQUIRED.md](APPROVAL_REQUIRED.md)). |
| `decision: NEEDS_INFORMATION` | only **with** the missing term / after the owner answered. |
| `429 rate_limited` | after `details.retry_after_seconds` (agent credentials: 120 requests / minute by default). |
| `409 idempotent_request_in_progress` | yes, same key, after a short wait. |
| `5xx` / network timeout on a binding call | yes, **same key** - you get the original result or a fresh decision, never a duplicate binding. |

## Recipe

```python
key = "order-" + order.id                       # stable per unit of work
for attempt in range(3):
    try:
        auth = q.authorize_binding(amount_minor=1999, merchant="Amazon", purpose="USB-C cable", commitment="ONE_TIME",
                                   task={"max_amount_minor": 2000}, execution="external", idempotency_key=key)
        break
    except QuattrinoError as e:
        if e.code in ("idempotent_request_in_progress", "rate_limited") or e.status >= 500:
            time.sleep(2 ** attempt); continue
        raise                                      # idempotency_key_reuse etc. are programming errors
```

## Timeouts and expiry

A binding authorization lives 15 minutes by default (`expires_at`). If your executor cannot finish in time, request a
longer `ttl_seconds` (up to 24 h, still bounded by task / envelope / delegation windows) **before** binding; an expired
authorization cannot be revived - authorize again with a new key.
