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 environment | Limit |
|---|---|
| Live | the org’s configured per-minute limit |
| Test | 10% of it — max(1, limit ÷ 10) |
The limit applies to each API key separately: every key gets the full per-minute number for its environment, and one key using up its budget never throttles another. Need a limit raised, lowered, or set? Contact support.
OAuth tokens and MCP calls use the same per-org, per-minute number from their own buckets (ratelimit:oauth:… and ratelimit:mcp:…), so an agent’s exploratory burst cannot exhaust any API key’s budget in the same organization. An MCP tools/call is metered on the organization it names in its organization argument — a connection covering two organizations on different plans gets each one’s own limit.
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: 1747512000RateLimit-Limit/X-RateLimit-Limit— the ceiling that applies to this request, in requests per minute (600here 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://developers.sendops.dev/api-reference/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.
The Send API
The Send API (early access, on ingest.sendops.dev) applies the same per-minute limit as above, from the same budget as the key’s Public API requests, with the same headers and the same 429 rate_limited. Three things differ:
- A monthly fair-use limit. Once your plan’s monthly number of live API sends is used up,
POST /v1/sendanswers429 send_quota_exceededwithlimit,usedandresets_at. ItsRetry-Afteris the time until the billing period resets, which can be days, so don’t retry on it. See Monthly fair-use limit. - A cap while requests are queued. When SendOps is briefly unavailable and the Send API is answering
202 queued, each key may queue about 100 requests per 10 seconds; past that it gets429 rate_limitedwithRetry-After: 10. - Your SES account’s limits surface as
503. A send that hits your SES account’s send rate or daily quota is503 upstream_unavailablewithRetry-After, not a429. Nothing was sent; retry with the sameIdempotency-Key.
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
limityou can. Lists supportlimitup 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.