Rate limits
Per-credential and per-tenant request limits, how sliding windows work, and how to handle 429 responses.
Rate limits cap how many API requests you can make in a given time period. This page explains how Zoop's limits work, what the API returns when you exceed them, and how to write code that recovers gracefully.
Zoop enforces rate limits on all external API requests. Limits use a sliding window algorithm: instead of resetting a counter at a fixed clock boundary (say, the top of every minute), the counter looks back over a rolling period.
How sliding windows work
With a fixed window, you could send 120 requests at 12:00:59 and another 120 at 12:01:00 — 240 requests in two seconds — without triggering a limit. A sliding window closes that gap by always counting the last 60 seconds, no matter when the clock ticks.
In practice: if you send 120 requests in one burst, you must wait for the oldest requests to fall outside the 60-second window before you can send more.
Primary API limits
These two limits apply to every request you make through the Zoop external API.
| Limiter | Scope | Algorithm | Limit |
|---|---|---|---|
| Per-credential | Your API key or OAuth token | Sliding window | 120 requests per minute |
| Per-tenant | All credentials belonging to your tenant combined | Sliding window | 600 requests per minute |
Both limits must pass. A request that clears the per-credential limit can still be rejected if your tenant has reached 600 requests across all integrations in the same window.
The per-tenant limit means that if you run multiple integrations (for example, a Zapier zap and a custom webhook handler) under the same Zoop account, their requests share the 600 requests-per-minute budget.
Handling 429 responses
When you exceed a limit, Zoop returns HTTP 429 Too Many Requests. The response includes a Retry-After header that tells you how many seconds to wait before retrying. The response body is a JSON object with error code -32003:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{ "jsonrpc": "2.0", "error": { "code": -32003, "message": "Rate limited" }, "id": null }
Always read Retry-After rather than parsing the body — the header is the reliable source of the wait time. The "id": null in the body is expected: a rate-limited request is rejected before it reaches any processing, so there is no request ID to echo back.
The Retry-After value is rounded up to the nearest whole second (minimum 1). It represents how long until the oldest request in your window expires and frees up capacity.
Retry with backoff
Read the Retry-After header and wait at least that long before retrying. Adding a small amount of random jitter prevents multiple parallel clients from hammering the API at the same instant.
async function zoopFetch(url: string, options: RequestInit): Promise<Response> {
const MAX_RETRIES = 3
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const res = await fetch(url, options)
if (res.status !== 429) {
return res
}
if (attempt === MAX_RETRIES - 1) {
// No more retries — surface the error to the caller.
return res
}
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '5', 10)
const jitter = Math.random() * 1000 // up to 1 s of extra wait
await new Promise((resolve) =>
setTimeout(resolve, retryAfter * 1000 + jitter),
)
}
throw new Error('unreachable')
}
import time
import random
import httpx
def zoop_fetch(url: str, **kwargs) -> httpx.Response:
MAX_RETRIES = 3
for attempt in range(MAX_RETRIES):
res = httpx.request(**kwargs, url=url)
if res.status_code != 429:
return res
if attempt == MAX_RETRIES - 1:
return res
retry_after = int(res.headers.get("Retry-After", "5"))
jitter = random.random() # up to 1 s
time.sleep(retry_after + jitter)
raise RuntimeError("unreachable")
Other limiters
A few specific endpoints have their own tighter limits. You are unlikely to hit these during normal integration work, but they do return 429 responses.
| Endpoint / action | Algorithm | Limit |
|---|---|---|
| OAuth token endpoint | Sliding window | 20 requests per minute per IP |
| Dynamic client registration (DCR) — registering a new OAuth app | Fixed window | 10 registrations per hour per IP |
See Authentication — OAuth for background on the OAuth and DCR flows.
Behavior when the rate limiter is unavailable
Zoop's rate limiter runs on Redis (an in-memory data store). On the rare occasion Redis is unreachable, Zoop chooses between two behaviors depending on how sensitive the endpoint is:
Fail-closed — the request is rejected with 429 even though the limiter is down. Zoop uses this for unauthenticated or credential-issuance endpoints (the OAuth token endpoint, DCR registration, public PDF rendering) where allowing unlimited requests could enable abuse. The Retry-After in this case is 60 seconds.
Fail-open — the request is allowed through. Zoop uses this for the primary per-credential and per-tenant limiters. Because your requests are already authenticated, blocking them during a Redis outage would break a working integration for no safety benefit.
In short: if Redis goes down briefly, your authenticated API calls continue uninterrupted. You only see 429 errors on the more sensitive endpoints listed above.
Tips for staying within limits
- Batch where possible. Fetching a list of jobs in one request costs the same 1 request as fetching a single job.
- Cache responses. If you need the same data in multiple places, fetch once and reuse it.
- Watch your tenant budget. If you run several integrations, their request counts add up against the shared 600 requests-per-minute ceiling.
- Implement retry with
Retry-After. Do not use a fixed delay — always read the header.
For questions about authentication and how credentials are scoped, see API keys and OAuth.