# Rate limits (/docs/api/rate-limits)

Every `/api/v1` request counts against one ceiling, per client IP, in a fixed one-minute window:

| Scope                                             | Ceiling                                                                        |
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
| All `/api/v1` requests, per IP                    | 600 / minute                                                                   |
| `POST /ats/connections/{id}/sync`, per connection | 1 accepted / 5 minutes (`429` inside the window; `409` while a run is running) |

Past the ceiling the edge answers `429` before the request reaches the API. The window is fixed, so
the count resets at the top of the next minute rather than sliding.

The agent endpoint runs a model on every call. Treat the ceiling as a budget, not a target.

## Backing off [#backing-off]

Retry on `429` and `503`, with exponential backoff and jitter. Do not retry `4xx` responses other
than `429` — a `403` will not start working because you asked again.

<Callout type="warn">
  If the agent endpoint returns `429`, retrying in a tight loop is the worst response: you will stay at the ceiling and
  never make progress. Wait for the reset.
</Callout>

## Retrying the agent safely [#retrying-the-agent-safely]

`POST /agent/invoke` accepts an `Idempotency-Key` header:

```bash
curl -X POST https://app.superposition.ai/api/v1/agent/invoke \
  -H "Authorization: Bearer $SUPERPOSITION_API_KEY" \
  -H "Idempotency-Key: 8f31c0a4-..." \
  -H "Content-Type: application/json" \
  -d '{"prompt": "How many candidates are awaiting review?"}'
```

A repeat of the same key returns the **original** result instead of running the agent again. Use a
fresh key per logical request and reuse it across retries of that request. Without one, a network
retry runs the agent twice and you pay twice.

The response tells you which happened:

```json
{ "sessionId": "...", "reply": "...", "status": "completed", "idempotentReplay": true }
```

`status` has three values, and each one tells you what to do next:

| `status`      | What happened                                                        | What to do                                                              |
| ------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `completed`   | The run finished. `reply` holds the answer.                          | Nothing.                                                                |
| `in_progress` | A retry arrived while the first run is still going. `reply` is null. | Poll `GET /api/v1/agent/sessions/{sessionId}`. Do **not** invoke again. |
| `failed`      | The original run for this key died. `reply` is null.                 | Retry with a **new** key, or investigate first.                         |

`failed` is terminal. Reusing that key will keep returning `failed` — it will never turn into
`completed`, so retrying it forever is pointless:

```json
{
  "sessionId": "...",
  "reply": null,
  "status": "failed",
  "idempotentReplay": true,
  "error": "The agent run did not complete.",
  "failedCorrelationId": "3f7c1e02-95a4-4a6d-9c5a-1b0e2f8d4a71"
}
```

Quote `failedCorrelationId` in a support request — it points at the run that actually died, not at
your replay.

<Callout type="warn">
  **Do not send `Idempotency-Key` together with `sessionId`.** The key applies to a session's first invocation, so the
  combination is refused with `400 invalid_request` rather than accepted under a guarantee it cannot keep. Resume a
  conversation without the header, or start a new session with it.
</Callout>

<Callout type="warn">
  Retrying with a **new** key runs the agent again from the start. A failed run may already have had side effects, so
  treat a new key as a deliberate decision rather than an automatic retry.
</Callout>
