API Rate Limits
Rate limits for the Wraps Platform API by plan, with response headers and error handling.
Rate limits for the Wraps Platform API by plan. Includes response headers and error handling.
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 |
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 |
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 |
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 |
HTTP/1.1 200 OKRateLimit-Limit: 500RateLimit-Remaining: 487RateLimit-Reset: 34RateLimit-Policy: 500;w=60X-RateLimit-Limit: 500X-RateLimit-Remaining: 487X-RateLimit-Reset: 34HTTP/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"}When you receive a 429 status code:
Retry-After header for the number of seconds to waitasync 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');}Complete reference for CLI and SDK error codes.
View Error CodesFull API reference for the Email SDK.
View SDK Reference