# Rate limits

Rate limits are set at the API-key level. The window starts with the first
counted request.

## Current limits

| Category | Requests per 60 seconds | Endpoints sharing the quota |
| --- | ---: | --- |
| Catalog | 120 | All five `GET /catalog/*` endpoints. |
| Read | 300 | `GET /audit/events`, `GET /balance`, `GET /orders`, and `GET /orders/{id}`. |
| Create | 30 | `POST /orders`. |
| Update | 30 | `POST /orders/{id}/update_photos`. |
| Upload | 20 | `POST /photos`. |
| Webhook | 10 | `POST /webhooks`, `GET /webhooks`, and `DELETE /webhooks/{id}`. |

Need higher limits? Contact [dev@legitgrails.com](mailto:dev@legitgrails.com)
to discuss your expected traffic and a limit configuration suited to your
account.

## Response headers

Counted requests include:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Maximum requests for this API key and category in the current window. |
| `X-RateLimit-Remaining` | Requests remaining in that category's current window. |

A rate-limited response also includes `Retry-After`, in whole seconds. Wait at
least that long before retrying.

## `429` response

When a limit is exceeded, the API returns code `rate-limit-exceeded` with the
active category, limit, window, and wait time:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
```

```json
{
  "statusCode": 429,
  "message": "Rate limit exceeded",
  "timestamp": "<timestamp>",
  "path": "/v1/integrations/photos",
  "code": "rate-limit-exceeded",
  "details": {
    "category": "upload",
    "limit": 20,
    "window_seconds": 60,
    "retry_after_seconds": 17
  },
  "request_id": "<request-id>"
}
```

Use `Retry-After` rather than calculating a delay from local clocks. Add random
jitter when many workers may retry together. For non-idempotent writes, first
reconcile whether the original request succeeded; see [Errors](/errors#rate-limits-and-retries).
