Rate Limiting

Search Documentation

Search across all developer documentation

Rate Limiting

The API supports a per-org sliding-window rate limit, but it is off by default — requests are unthrottled until a per-org per-minute limit is configured on your plan. When a limit is in effect, 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

There is no fixed default limit. The ceiling is a per-org, per-minute number; while it’s unset (the out-of-the-box state) nothing is throttled and the rate-limit headers are omitted entirely. Once a limit is configured:

Key environmentLimit
Livethe org’s configured per-minute limit
Test10% of it — max(1, limit ÷ 10)

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 limit raised, lowered, or set? Contact support.

Headers on every response

Once a limit is configured, two header families are written on every response — the modern IETF-draft RateLimit-* names and the legacy X-RateLimit-* names (identical values), so clients pinned to either draft keep working:

RateLimit-Limit: 600
RateLimit-Remaining: 412
RateLimit-Reset: 1747512000
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 412
X-RateLimit-Reset: 1747512000
  • RateLimit-Limit / X-RateLimit-Limit — the ceiling that applies to this request, in requests per minute (600 here is only an example of a configured limit).
  • RateLimit-Remaining / X-RateLimit-Remaining — how many more requests you can make in the current window.
  • RateLimit-Reset / X-RateLimit-Reset — when the window resets, as a Unix timestamp (seconds).

These are advisory — a healthy client paces itself off Remaining and only falls back to retry-on-429 as a safety net. When no per-org limit is configured, these headers are absent.

What happens at the ceiling

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

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 12
RateLimit-Limit: 600
RateLimit-Remaining: 0
RateLimit-Reset: 1747512000
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747512000

{
"type": "https://docs.sendops.dev/api/errors/rate_limited",
"title": "Rate limit exceeded",
"status": 429,
"code": "rate_limited",
"detail": "You have exceeded the per-minute API key request limit; retry after the time indicated in Retry-After.",
"retry_after": 12
}

Retry-After is the canonical signal — sleep at least that many seconds before retrying. It is the number of seconds to wait; the Reset headers give the absolute Unix timestamp of the window reset, and the problem body’s retry_after mirrors Retry-After.

Handling 429 in code

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

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.

Repeated 401s will trip an IP block

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.