# API Rate Limits

Source: https://wraps.dev/docs/reference/rate-limits

Rate limits for the Wraps Platform API by plan. Includes response headers and error handling.

## Limits by Plan

Copy for AI

The Wraps Platform API enforces a per-minute rate limit based on your plan. Limits are applied per organization using DynamoDB-backed sliding windows. There is no daily request cap and no monthly send quota on any plan — sends are unlimited and billed by AWS directly. The per-minute limit exists to protect the API from bursts.

| Plan | Per Minute |
| --- | --- |
| Free | `50` requests |
| Pro | `2,000` requests |
| Business | `5,000` requests |

## Public Endpoints

Copy for AI

Unauthenticated endpoints (health check, public tools) use IP-based rate limiting:

| Scope | Limit |
| --- | --- |
| Per minute per IP | `10` requests |
| Per hour per IP | `100` requests |

## Event Ingestion

Copy for AI

Custom events (sent via `POST /v1/events` and `POST /v1/events/batch`) are the one metered resource — they are Wraps' own storage, not AWS sending, which is never metered. Free ingestion continues through a 25% grace margin (6,250 events) before returning `429` with a `Retry-After` pointing at the 1st of the next month, when the counter resets. Sending is never affected.

| Plan | Custom events / month |
| --- | --- |
| Free | 5,000 |
| Pro | Unlimited |
| Business | Unlimited |

| Header | Reports |
| --- | --- |
| `X-Event-Limit` | Monthly allowance, or -1 when unlimited |
| `X-Event-Current` | Events ingested this month |
| `X-Event-Remaining` | Events left, or -1 when unlimited |
| `X-Event-Percent` | Percent of the allowance used, or 0 when unlimited |

## Response Headers

Copy for AI

Every rate-limited response carries these headers. Read the unprefixed `RateLimit-*` names — the `X-` forms are the originals, kept so existing integrations keep working.

| Header | Description |
| --- | --- |
| `RateLimit-Limit` | Requests permitted in the window closest to exhaustion |
| `RateLimit-Remaining` | Requests still available in that window |
| `RateLimit-Reset` | Seconds until that window resets — not a Unix timestamp |
| `RateLimit-Policy` | Every policy in force, as <limit>;w=<window seconds> |
| `Retry-After` | Seconds to wait before retrying (429 responses only) |
| `X-RateLimit-Limit` | Legacy alias for RateLimit-Limit |
| `X-RateLimit-Remaining` | Legacy alias for RateLimit-Remaining |
| `X-RateLimit-Reset` | Legacy alias for RateLimit-Reset |

### Successful response

response headers

```
HTTP/1.1 200 OKRateLimit-Limit: 500RateLimit-Remaining: 487RateLimit-Reset: 34RateLimit-Policy: 500;w=60X-RateLimit-Limit: 500X-RateLimit-Remaining: 487X-RateLimit-Reset: 34
```

### Rate limited response (429)

429 response

```
HTTP/1.1 429 Too Many RequestsRetry-After: 34RateLimit-Limit: 500RateLimit-Remaining: 0RateLimit-Reset: 34RateLimit-Policy: 500;w=60X-RateLimit-Limit: 500X-RateLimit-Remaining: 0{  "error": "Rate limit exceeded: 500 requests per minute",  "code": "RATE_LIMITED",  "requestId": "01JBZ9K3M4N5P6Q7R8S9T0"}
```

## Handling 429 Errors

Copy for AI

When you receive a 429 status code:

1.  Read the `Retry-After` header for the number of seconds to wait
2.  Wait that duration before retrying
3.  Use exponential backoff if retries continue to fail

retry.ts

```
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {  for (let attempt = 0; attempt < maxRetries; attempt++) {    const response = await fetch(url, options);    if (response.status === 429) {      const retryAfter = Number(response.headers.get('Retry-After') ?? 60);      await new Promise(r => setTimeout(r, retryAfter * 1000));      continue;    }    return response;  }  throw new Error('Rate limit exceeded after retries');}
```

## Next Steps

Error Codes

Complete reference for CLI and SDK error codes.

[View Error Codes](https://wraps.dev/docs/reference/errors)

SDK Reference

Full API reference for the Email SDK.

[View SDK Reference](https://wraps.dev/docs/sdk-reference)
