# Rate limits & idempotency

## Limits

| Limit | Applies to |
| --- | --- |
| **60 requests per minute** | Each API key, across all endpoints. |
| **10 report starts per hour** | Each API key, on `POST /v1/reports`, on top of the per-minute limit. |
| Plan limits | Starts on your own provider keys are also bounded by the [Agent plan](/agent-plan/): brands, reports per billing period, reports in any 24 hours, and reports at once. |

Rate-limit windows are fixed: a per-minute budget resets at the start of each minute. Requests without a valid key (public rankings, a missing or mistyped key) are limited per client address instead.

## When you hit one

There are two kinds of `429`. Tell them apart by the `limit` field.

### Rate limits: wait and retry

Over the per-minute or per-hour budget, you get `429` with a JSON [error](/errors/), `retryAfterSeconds` in the body, and these headers:

| Header | Meaning |
| --- | --- |
| `Retry-After` | Seconds to wait before retrying. |
| `RateLimit-Limit` | Requests allowed in the window. |
| `RateLimit-Remaining` | Requests left in the window (`0` on a 429). |
| `RateLimit-Reset` | Seconds until the window resets. |

Wait `Retry-After` seconds, then retry.

### Plan limits: named in `limit`

A start refused by an Agent plan limit is a `429` whose body names the limit in `limit`. It never carries `RateLimit-*` headers. Whether it says when to retry depends on the limit:

| `limit` | `Retry-After` and `retryAfterSeconds` | When it lifts |
| --- | --- | --- |
| `perDay` | Yes | When the oldest start in the last 24 hours turns 24 hours old. |
| `reportsPerMonth`, `projects` | Yes, when the billing period's end is known | At the next billing period. |
| `concurrent` | **Never** | When one of your running reports finishes. Poll its status, then start again. |

Without an active Agent plan, a start is refused with `402` and `limit: "plan"`, also with no retry time. Don't retry plan limits in a loop. Nothing was queued and nothing was charged.

### Polling without burning the budget

A report takes several minutes. Poll `GET /v1/reports/{slug}/status` about **once a minute**; faster polling only spends your budget. Honour `Retry-After` and back off on repeated `429`s.

## Idempotency: retry a start safely

Starting a report spends money, and networks fail. Send an idempotency key with every start so a retry can never spend twice:

```bash
curl -X POST https://api.recomention.com/v1/reports \
  -H "Authorization: Bearer $RECOMENTION_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2e7a4b6d8e0f1a2b3c4d" \
  -d '{"domain":"example.com","category":"meeting scheduling","description":"Booking links for sales teams","plan":"FULL","funding":"credits"}'
```

- The key is 16–128 characters of letters, digits, `-` and `_`. Generate a random one per start (for example `openssl rand -hex 16`).
- Send it as the `Idempotency-Key` header or as `idempotencyKey` in the body. If you send both, they must match.
- **Same key, same body → the first start.** The response is `200` with `reused: true` and the original `slug`. Nothing new is spent.
- **A key identifies one start together with its body.** The same key with a different body is a different start. Reuse a key only to retry the *same* request; a new report needs a new key.
- **`providerKeys` are not part of that fingerprint.** Retrying a BYOK start with the same idempotency key and corrected provider keys returns the first report, which ran (or failed) with the old keys. To run again with different keys, use a new idempotency key.
- **If the first start's credit was returned** — it could not be queued, or it ran, failed and was refunded — a retry with the same key gets `409` instead of a replay. Send a new start with a new key.
- **Keys are scoped to your account.** The same string sent from another account is that account's own, separate request.
- Without a key, every request is a new start.

After a `503`, a timeout or a dropped connection, the outcome of a start is unknown. Retry with the **same** key, or check `GET /v1/reports`. Never retry with a new key, which could start a second report. See [Errors](/errors/#a-503-on-a-start-is-an-unknown-outcome).