# Rate limits

> 600 requests per minute per key, RateLimit-* headers on every response to an authenticated request, a 429 with Retry-After when exceeded, and the unauthenticated health probe.

Source: https://developers.processcart.com/guides/rate-limits

Each API key may make **600 requests per 60-second window**. The window is fixed (it starts on the minute boundary the first request falls in), the count is per key, and every plan has the same limit — create separate keys for separate integrations and each gets its own window.

| Limit | Value |
| --- | --- |
| Requests per key | 600 per 60 seconds (a fixed window) |
| `GET /v1/health` | 60 per 60 seconds per client address (unauthenticated) |
| Headers on every authenticated response | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` (seconds), `RateLimit-Policy` |
| When exceeded | 429 `rate_limited` with `Retry-After` (seconds) |

## Headers

Every response to a request that passed authentication carries the IETF RateLimit header fields — a success, a refusal, a 429:

```http
RateLimit-Limit: 600
RateLimit-Remaining: 412
RateLimit-Reset: 23
RateLimit-Policy: 600;w=60
```

`RateLimit-Reset` is the number of seconds until the window rolls over (never 0).

A response given before a key is accepted carries none of them: a 401 (a missing, unknown or revoked key), and a 405 `method_not_allowed` (a verb the path does not support) or a 404 `not_found` for a path no route matches. Those last two are answered before authentication is attempted, by design: they never count toward the window and are not logged. The health probe is unauthenticated and carries none of them either.

## When the limit is hit

The request is refused with **429 `rate_limited`** and a `Retry-After` header in seconds — the same number as `RateLimit-Reset`:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 23
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 23
Content-Type: application/problem+json
```

```json
{
  "type": "https://developers.processcart.com/errors#rate_limited",
  "title": "Too many requests",
  "status": 429,
  "detail": "This key is limited to 600 requests per 60 seconds.",
  "code": "rate_limited",
  "requestId": "req_…"
}
```

A 429 is never stored for idempotent replay: retry the same request with the same `Idempotency-Key` after `Retry-After` and it executes. Back off on 429 (sleep `Retry-After`, then resume) rather than spreading requests across keys — a refused request still counts toward the window.

## Reducing requests

- `GET /v1/runs` returns full Runs (fields and tasks included) — one request per 100 runs, no per-run follow-ups.
- `updatedSince` fetches only what changed ([Incremental sync](https://developers.processcart.com/guides/incremental-sync)).
- Webhooks deliver the full Run with every run event (a lite header on a workflow-trash cascade) ([Webhooks](https://developers.processcart.com/webhooks)) — no polling at all.

## Health

`GET /v1/health` is unauthenticated and answers `{ "status": "ok" }` with 200 when the database responds, or `{ "status": "degraded" }` with 503. It is limited per client address (60 per minute), cached for ten seconds on each server, and never logged. The public status page at [status.processcart.com](https://status.processcart.com) checks it continuously — see [Status & uptime](https://developers.processcart.com/status).
