API rate limiting
How /api/v1 throttles traffic, which response headers to read, and how that relates to monthly API quotas.
The public API applies two separate limits:
- Per-minute rate limits — cap short bursts so the service stays responsive for everyone.
- Monthly API quota — caps total successful usage for your team’s plan (when your deployment includes billing).
This guide explains per-minute limits and response headers. Monthly quotas are summarized at the end.
Standards and headers
HTTP 429 Too Many Requests
When you exceed a per-minute limit, the API responds with 429 and a JSON body:
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded, please try again in 42s"
}The 429 status code is defined for this purpose in RFC 6585 and described in RFC 9110 (HTTP Semantics) as the appropriate response when the client has sent too many requests in a given period.
Retry-After
On 429 responses from rate limiting, we send a Retry-After header whose value is a delay in seconds (not an HTTP-date). That matches the delay-in-seconds interpretation documented for Retry-After in RFC 9110, Section 10.2.3.
Retry-After: 42Wait at least that many seconds before retrying. For automated clients, combine this with exponential backoff and jitter.
X-RateLimit-* (informal convention)
Authenticated /api/v1 responses include:
| Header | When present | Meaning |
|---|---|---|
X-RateLimit-Limit | Success and throttle | Maximum requests allowed in the current window |
X-RateLimit-Remaining | Same | Requests left in the window (0 when exhausted) |
X-RateLimit-Reset | Same | Seconds until the window resets, not a Unix timestamp |
Retry-After | Only on 429 | Same value as X-RateLimit-Reset for that response |
These names follow the widespread X-RateLimit-* pattern used by many HTTP APIs. They are not defined by an IETF RFC. The IETF is standardizing similar fields (for example RateLimit-Limit) in draft-ietf-httpapi-ratelimit-headers; our headers use the same ideas (limit, remaining, reset interval) with the familiar X- prefix.
Read header names case-insensitively per RFC 9110. In browsers, exposed header names may appear with different casing.
How limits work
Fixed one-minute window
Per-minute limits use a fixed window (typically 60 seconds from your first request in that window):
- Each request counts toward the limit for that window.
- When you exceed the limit, further requests receive 429 until the window resets.
- After the window ends, the counter starts fresh.
This is not a sliding window. Traffic at the end of one minute and the start of the next can briefly exceed the per-minute number across a two-minute span.
What shares a limit
| Request type | Limit applies to |
|---|---|
/api/v1/team/... with an API key | Each API key has its own counter |
GET /api/v1/openapi.json (no API key) | Client IP address |
Keys on the same team do not share one per-minute counter. Use one key per integration; do not rotate keys to get around limits.
Limits by endpoint type
| Endpoint type | Limit | Window |
|---|---|---|
Authenticated team API (/api/v1/team/...) | 60 requests | 1 minute |
OpenAPI document (GET /api/v1/openapi.json) | 30 requests | 1 minute (per IP) |
Tool result polling (GET /api/v1/tools/.../results) | 300 requests | 1 minute (per API key), when your deployment exposes tool routes |
Starting an async tool job (POST /api/v1/tools/...) counts toward the same 60/min limit as other authenticated team API calls. Polling results uses the higher limit so you can poll every few seconds without exhausting the general cap.
Example: read headers with curl
curl -sS -D - -o /dev/null \
-H "Authorization: Bearer YOUR_KEY" \
"https://YOUR_DOMAIN/api/v1/team"A successful response:
HTTP/2 200
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 58After you are throttled:
HTTP/2 429
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 37
Retry-After: 37
Content-Type: application/json
{"code":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded, please try again in 37s"}Recommended client behavior
On 429 with code RATE_LIMIT_EXCEEDED:
wait at least Retry-After seconds (or use X-RateLimit-Reset)
retry with backoff and jitter; cap retry attempts
On 429 with code API_QUOTA_EXCEEDED:
stop tight-looping; check plan usage or wait for the billing period to resetTrack X-RateLimit-Remaining on success to slow down before you hit 429. For tool jobs, poll results on an interval of a few seconds rather than hammering the API.
If you use the TypeScript SDK (emailguard-sdk), honor Retry-After in your retry logic when you handle PublicApiError from throttled responses.
Monthly API quota
On plans with API usage metering, each successful /api/v1 call may count toward your team’s monthly allowance. Limits depend on your subscription tier.
| Per-minute rate limit | Monthly quota | |
|---|---|---|
| Scope | Per API key (or per IP for unauthenticated routes) | Per team |
| HTTP status | 429 | 429 |
| Error code | RATE_LIMIT_EXCEEDED | API_QUOTA_EXCEEDED |
| Response headers | X-RateLimit-*, Retry-After | No dedicated quota headers |
| What to do | Wait for the minute window to reset | Upgrade your plan or wait for the billing period to reset |
When the monthly quota is exceeded, the API returns API_QUOTA_EXCEEDED and does not include X-RateLimit-* headers on that response.
Per-minute throttles (RATE_LIMIT_EXCEEDED) do not count against your monthly quota.
View current usage with GET /api/v1/team/billing/usage when your API key includes the billing:read scope.
Related docs
- SDK clients — published clients and authentication
- API key scopes — scopes for tools, billing, and deliverability
- MCP integration — same limits apply to MCP-forwarded REST calls