# Retries and idempotency

> How to retry any call without clicking twice or paying twice.

Networks drop replies. The rule is simple: **send an `Idempotency-Key` on every POST, and resend with the same key when a reply is lost.**

```bash
curl -X POST https://api.trykubi.com/v1/screens/scr_7f2kq9w3x8m4b6tzp1ac/click \
  -H "Authorization: Bearer $KUBI_API_KEY" \
  -H "Idempotency-Key: 2f1d3c4b-8a9e-4f60-b1c2-d3e4f5a6b7c8" \
  -H "Content-Type: application/json" \
  -d '{"x": 450, "y": 280}'
```

## What happens on a resend

| Situation | You get |
|---|---|
| First request finished | The stored reply, with `Idempotent-Replayed: true`. For actions: the stored results and a fresh screenshot. Never a second click. |
| First request still running | `409 IDEMPOTENCY_KEY_IN_USE`; try again shortly |
| Same key, different body | `422 IDEMPOTENCY_KEY_REUSED`; that is a bug in the caller |

Keys are kept 24 hours for screens and power, 10 minutes for actions and commands. The SDK and the MCP server always send one.

## Reading an error before retrying

Every error says whether a retry can help and whether anything happened:

- `retryable: true`: send it again after `retry_after` seconds.
- `outcome: not_applied`: nothing ran. Safe to fix and resend.
- `outcome: unknown`: it may have run (for example the screen restarted mid-action, `504 OUTCOME_UNKNOWN`). Take a screenshot and look before acting again.

`STALE_OBSERVATION` is not retryable either: the screen changed since you looked, so take a new screenshot and decide again.

`CONTROL_HELD` is not retryable as it stands: a person has control of the screen and nothing ran. Wait `retry_after` seconds, then take a new screenshot and decide again.
