Rate limits
Rate limits protect your workspace and the service from runaway scripts and bursts. This page lists the limits, the headers that report them and how to respond to a 429.
Limits
Every operation belongs to one rate-limit class, shown in the API reference (as x-rate-limit-class). Each class is limited per key and per workspace, per minute, and the two modes are counted separately: test traffic never uses up your live budget.
| Class | Operations | Per key | Per workspace |
|---|---|---|---|
read | Every GET | 600 per minute | 1,200 per minute |
write | Creates, updates, deletes and other actions | 120 per minute | 240 per minute |
dial | Placing a call (POST /v1/calls), starting and resuming a campaign | none | 30 per minute |
The per-workspace limit is shared by all keys of the workspace in that mode, so several integrations with their own keys can still reach it together. Your workspace's current limits are in GET/workspace under limits.rate_limits; read them rather than hard-coding the numbers.
Requests that are refused with 429 do not count against the limit.
Requests that never reach an operation
Requests that are refused before they reach an operation (an invalid or missing key, an unknown path or method, a refused workspace, a body that is not JSON or is too large) do not count against your key's or workspace's limits. They are limited per source IP address instead: 300 per minute. Once an address has used that up, every request from it, valid or not, answers rate_limited (429) with Retry-After until the minute is over. Their responses carry RateLimit-Limit: 300, and the rate_limited error's details.limit_class is read. A misconfigured client that keeps sending a bad key can therefore block other clients that share its outbound address: fix authentication errors instead of retrying them.
Daily call limit
In live mode, a workspace can place a limited number of calls through the API per UTC day, as a guard against a loop or a retry storm calling people again and again. The limit that applies is limits.daily_call_limit in GET /v1/workspace (500 by default; null in test mode, which has no daily limit). It resets at 00:00 UTC. The count is kept durably (a restart does not reset it) and is checked as the request is accepted; if it cannot be checked, the request is refused with service_unavailable rather than let through.
| Request | Counts |
|---|---|
POST /v1/calls | One call. A replay with the same Idempotency-Key, if you send one, does not count again. |
| Starting a campaign | One per recipient, counted before any dialling. If they do not all fit in what is left today, the start is refused whole and nothing is dialled. |
| Resuming a campaign | Nothing: its start already counted. A resume is never refused for the daily limit. |
A campaign with more recipients than the daily limit (more than 500 on the default limit) therefore cannot be started through the API in one day: split it into smaller campaigns, or start it from the console.
When a request does not fit, it answers daily_call_limit_reached (429). details say the limit, how many calls the request needed, how many are left and when the limit resets, and Retry-After is the number of seconds until then:
JSON
Nothing was dialled. Schedule the work for after resets_at rather than sleeping, or (for a campaign) start a smaller one that fits in remaining. If your expected volume is higher, write to support@relationsai.com to discuss it.
Calling capacity
Separately from your rate limits, call starts are paced across the phone network. When no capacity is free right now, POST /v1/calls answers calling_capacity_reached (429) with Retry-After. The request was valid and nothing was dialled: wait and send it again. For large volumes, use a campaign, which paces calls for you.
Headers
Every response, successful or not, includes:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the current one-minute window by the tightest limit that applied to this request: the key's or the workspace's limit for the operation's class, or the per-address limit (300) for a request that never reached an operation. |
RateLimit-Remaining | Requests left in the window. |
RateLimit-Reset | Seconds until the window resets. |
429 and 503 responses also include Retry-After, in seconds.
Text
Use them to slow down before you hit the limit: when RateLimit-Remaining reaches 0, wait RateLimit-Reset seconds before the next request.
Handling 429
rate_limited means nothing was done. For example, a contact sync that sends too many writes in one minute:
Text
details.limit_class says which class (read, write or dial) was exceeded. Wait 30 seconds (the Retry-After value) plus a little random jitter, then send the same request again (with the same Idempotency-Key, if you sent one); nothing was done the first time. Then:
- Spread the remaining requests out: when
RateLimit-Remainingreaches0, waitRateLimit-Resetseconds before the next one. - For errors with
retry: "yes"on reads, and for requests that got no response at all, use exponential backoff with full jitter: wait a random time between 0 andmin(cap, base × 2^attempt), for example a base of 1 second, doubling, with a cap of 30 seconds. For a500internal_erroron a write, check whether the change was applied before retrying: a500is not stored under the key. - Cap the number of attempts (for example five), and hand very long waits, such as the daily call limit, back to your scheduler.
- If you hit
429regularly, use fewer and larger pages (limit=100), prefer webhooks to polling, and use campaigns for bulk calling.
When the API cannot check rate limits because of a temporary problem on its side, it refuses the request with service_unavailable (503) and Retry-After, rather than letting it through. Handle it the same way.
The complete runnable example is one request helper that waits for Retry-After, backs off with jitter, slows down when RateLimit-Remaining reaches 0, and retries a write (sent without an Idempotency-Key) only after a 429 or 503, which mean nothing was done; after any other uncertain answer it stops so you can check the state first.
Complete runnable example
One request helper that respects rate limits and retries only what is safe to retry.
The same steps as a complete script, with error handling, tested against test mode on every docs build. Each script reads your test key from an environment variable named in its first lines.
- cURL script (bash)
rate-limit-backoff.sh - JavaScript (Node.js 20+)
rate-limit-backoff.mjs - Python 3.9+ (requests)
rate-limit-backoff.py
Polling budget
If you poll calls instead of using webhooks, poll no more than every few seconds per call, and stop when settled is true. With the read limit of 600 per minute per key, polling every 3 seconds supports about 30 calls at a time per key; beyond that, use webhooks.