Rate limits and quota
How fast and how much you can call the Dashly API, the headers that tell you where you stand, and how to back off.
Two kinds of limits apply to every call:
- Rate limits bound how fast you call. They are measured in seconds to minutes, and waiting a few seconds lifts them.
- The monthly quota bounds how much work your agency asks for in a calendar month. Waiting until it resets lifts it.
Both answer 429 rate_limited with a Retry-After header (in seconds) and details.reason naming the limit you met.
Rate limits
| Limit | Ceiling | Counted per | details.reason |
|---|---|---|---|
| Requests | 120 per minute | key | burst_limit |
| Writes | 60 per minute | key | write_burst_limit |
| Rows read | 20,000 per rolling hour | key | volume_budget |
| Reads from one address | 600 per minute | calling IP address | ip_limit |
| Writes from one address | 240 per minute | calling IP address | ip_limit |
| Unknown or revoked keys from one address | 20 per 10 minutes | calling IP address | auth_failures |
- Requests count every call made with the key, reads and writes together, in fixed one-minute windows.
- Writes (
POST,PATCH,DELETE, and actions that change data) also count against their own ceiling. - Rows read is the number of records your reads returned over the last hour. A read is admitted while the key's hour has room, and is charged afterwards with the rows it actually returned. A full sync of a real agency fits; dumping every collection in one go does not.
- Per address limits apply before the key is even checked, so several integrations behind the same server share them. Unknown keys counts calls with a missing, unknown, revoked or expired key from one address; past it, every call from that address is refused for a while, even with a valid key.
A refused request still counts against the window, so retrying immediately in a loop keeps you locked out.
Rate limit headers
Every successful response tells you where you stand in the per-key request window:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key (120). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Seconds until the window resets. |
Key suspension
A key that keeps reading after volume_budget refused it, ignoring Retry-After (20 refusals within 15 minutes), is treated as a possible leak and suspended. Every call then gets 403 forbidden with details.reason: "key_suspended", and the agency's admins are notified.
An admin can reactivate the key from Connectors → API & webhooks. Reactivating does not reset the hourly budget: an integration still looping is throttled again. See Authentication.
Monthly quota
Each agency has a monthly allowance of tokens, shared by all its keys. It comes with the agency's plan and resets on the first day of each month at 00:00 UTC. The agency can also set a monthly cap per member: a key's usage counts against the cap of its owner.
What a call costs
A token is a unit of work, not a request:
| Call | Cost in tokens |
|---|---|
Read (GET) | 1 + 1 per record returned |
Write (POST, PATCH, DELETE) | 6 |
| Action that changes data | 11 |
| Read-only action | Priced as a read |
A single record (GET /v1/clients/{id}, GET /v1/me) counts as one record, so it costs 2. A list of 200 records costs 201: fetching 200 records in one page costs the same per record as fetching them in pages of 25, plus one token per page.
A call that fails after passing the limits (a 422, for example) is charged the minimum for its kind: 1 for a read, 6 for a write, 11 for an action. Calls refused by a limit or by the quota cost nothing.
Quota headers
Successful responses carry all five headers; a call refused by the quota carries them too:
| Header | Meaning |
|---|---|
X-Quota-Cost | Tokens this call cost. 0 on a 429. Also sent on a failed call that was charged. |
X-Quota-Used | Tokens used by the agency this month. |
X-Quota-Limit | The monthly allowance, or unlimited. |
X-Quota-Remaining | Tokens left, or unlimited. |
X-Quota-Reset | The date the quota resets, YYYY-MM-DD (UTC). |
GET /v1/me returns the same figures in its body. See Requests and responses.
Reads are admitted on their minimum cost, because their real cost is only known once the records are counted. The last calls of a month can therefore overshoot the allowance slightly.
When the quota is spent
{
"error": {
"code": "rate_limited",
"message": "The API token quota is reached for this cycle.",
"details": {
"reason": "agency_quota_exhausted",
"used": 50012,
"limit": 50000,
"resetsAt": "2026-11-01"
},
"request_id": "req_…"
}
}details.reason is agency_quota_exhausted when the agency's allowance is spent, or user_quota_exhausted when the key's owner has reached the personal cap set for them. Retry-After gives the seconds until the reset.
All 429 reasons
details.reason | What happened | Wait |
|---|---|---|
burst_limit | More than 120 requests in a minute with this key | Seconds |
write_burst_limit | More than 60 writes in a minute with this key | Seconds |
volume_budget | The key read 20,000 records in the last hour | Up to an hour |
ip_limit | Too many calls from your address | Seconds |
auth_failures | Too many unknown or revoked keys from your address | Up to 10 minutes |
agency_quota_exhausted | The agency's monthly tokens are spent | Until the reset |
user_quota_exhausted | The key owner's monthly cap is reached | Until the reset |
The per-key and per-address refusals also carry details.limit and details.retryAfter.
How to back off
- Honour
Retry-After. On any429, wait at least that many seconds before retrying the same call. Never retry in a tight loop: refused calls still count, and ignoringvolume_budgetgets the key suspended. - Watch the headers. Slow down when
X-RateLimit-Remaininggets low, and schedule large syncs according toX-Quota-RemainingandX-Quota-Reset. - Read in big pages. Use
per_page=200and filters rather than many small pages; it is cheaper in requests and in tokens. - Spread large syncs over time. Keep a full read of the agency under 20,000 records per hour per key.
- Make writes safe to retry. Send an
Idempotency-KeyonPOSTso a retry after a timeout never creates a duplicate. See Idempotency. - Retry server errors with exponential backoff and jitter. A
500can be retried; a4xxother than429will fail again until you change the request.
async function call(url: string, init: RequestInit, attempt = 0): Promise<Response> {
const res = await fetch(url, init);
if (res.status === 429 && attempt < 5) {
const wait = Number(res.headers.get("Retry-After") ?? "1");
await new Promise((r) => setTimeout(r, wait * 1000));
return call(url, init, attempt + 1);
}
return res;
}An agency_quota_exhausted or user_quota_exhausted refusal can ask you to wait days. Stop the job and resume after the reset rather than sleeping inside a request.