# Rate Limiting

The API enforces a **per-org sliding-window rate limit**. Every response — successful or not — carries the current state of your budget so you can pace requests proactively instead of reacting to 429s.

## The numbers

| Key environment | Default limit          |
|-----------------|------------------------|
| Live            | 600 requests / minute  |
| Test            | 60 requests / minute   |

Limits are shared across all keys in an org — if one key burns the budget, all keys in the org see 429s until the window slides. Need a higher ceiling for a specific use case? Contact support.

## Headers on every response

```http
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 412
X-RateLimit-Reset: 37
```

- `X-RateLimit-Limit` — the ceiling that applies to this request, in requests per minute.
- `X-RateLimit-Remaining` — how many more requests you can make in the current window.
- `X-RateLimit-Reset` — seconds until the window slides far enough that you'd be at full capacity again.

These are advisory — a healthy client paces itself off `Remaining` and only falls back to retry-on-429 as a safety net.

## What happens at the ceiling

When you've consumed the budget, the next request returns:



`Retry-After` is the canonical signal — sleep at least that many seconds before retrying. The same value also appears in `X-RateLimit-Reset` and the problem body's `retry_after`.

## Handling 429 in code

The safe pattern is **exponential backoff with jitter**, honouring `Retry-After` as the minimum delay.

<CodeBlock lang="javascript" code={`async function callSendOps(path) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(\`https://api.sendops.dev\${path}\`, {
      headers: { Authorization: \`Bearer \${process.env.SENDOPS_API_KEY}\` },
    })
    if (res.status !== 429) return res

    const retryAfter = Number(res.headers.get("Retry-After") ?? 1)
    const jitter = Math.random() * 250
    await new Promise(r => setTimeout(r, retryAfter * 1000 + jitter))
  }
  throw new Error("rate-limited after 4 attempts")
}`} />

A few rules:

- Never retry faster than `Retry-After`. Doing so spends budget without making progress and slows your own recovery.
- Cap retries — runaway loops are the most common reason a service stays rate-limited indefinitely.
- Add jitter so multiple clients don't lock-step into the same retry instant.

## Auth-failure limits (IP-based)

Unauthenticated traffic and traffic from invalid keys is rate-limited **per IP**, with a tighter ceiling, so we don't get DDoS'd by credential-stuffing or scanner probes. You should never see this limit in normal use — it only trips when you're sending many requests with bad keys from the same source.


  If your client retries indefinitely on 401, the originating IP will hit the IP-based auth limit and start receiving 429s on every request — including ones with valid keys. Fix the underlying auth issue before retrying.


## Test vs live quotas

Test keys run at 10% of the live limit. That's intentional — test keys are for CI smoke checks and unit tests, not load tests. If you find yourself wanting to load-test against test keys, use live keys against a non-production SendOps account instead.

## Tips for high-volume readers

Most readers won't come close to the limit, but if you're paginating large message lists or polling reports on a tight loop:

- **Cache responses.** Reports are computed from event data that updates within seconds — caching for 30–60 seconds on your side is usually safe and reduces request count dramatically.
- **Use the largest `limit` you can.** Lists support `limit` up to 200 on most endpoints. Fewer requests, same data.
- **Filter server-side.** Don't pull all messages and filter client-side; use the available query parameters.
- **Subscribe instead of poll** where possible. For event-driven needs, configure a webhook in the dashboard rather than polling the API.