Quotas & rate limits
The hourly request budget, what each call actually costs, the headers that tell you where you stand, and what happens when the budget runs out.
The API is metered per hour. Three things are worth internalising before you build against it: the budget belongs to the subscription, not the client, calls cost different amounts, and every metered response tells you where you stand.
The budget
| Plan | Requests per hour |
|---|---|
| Free | No API access |
| Pro | No API access |
| Business | 1 000 |
| Enterprise | 5 000 |
The window is a fixed UTC clock hour: it resets on the hour, not an hour after your first call.
The allowance is charged to the subscription owner. Every API client of every organization that owner holds draws from the same bucket — registering a second integration does not buy a second allowance.
Fair share between clients
Once an organization has two or more active clients, no single client may take
more than 50 % of the hourly allowance. One runaway integration therefore cannot
starve the others; the greedy one gets a 429 while the subscription still has
budget left.
With a single active client the ceiling isn't applied at all — it would otherwise make the advertised number unreachable in the most ordinary setup there is.
What a call costs
Most calls cost one unit. Two kinds don't:
| Call | Cost |
|---|---|
/v1/me, /v1/health, /v1/openapi.json, /v1/docs | 0 |
Any 304 Not Modified | 0 — the charge is refunded |
/v1/reports/{id}/data, export.xlsx, export.csv | max(1, days × adAccounts / 1000) |
| Everything else | 1 |
So a 30-day pull across 5 ad accounts costs 1 unit; a 365-day pull across 25 ad
accounts costs 10. days is the length of the requested range and adAccounts is
how many of the report's cabinets the call actually reads — narrowing either with
adAccountIds or a shorter range narrows the cost.
The headers
Every metered response carries:
RateLimit-Limit: 1000
RateLimit-Remaining: 874
RateLimit-Reset: 1757689200
And a refusal adds Retry-After:
HTTP/1.1 429 Too Many Requests
Retry-After: 1420
Content-Type: application/problem+json
Read RateLimit-Remaining rather than counting your own calls — weighted costs make
a local counter wrong.
When something is unavailable
Two dependencies, two deliberately different behaviours:
- The quota store is fail-closed. What cannot be counted cannot be allowed, so if
Level can't meter a request it refuses it:
503 rate-limiter-unavailable. The zero-cost paths keep answering, so/v1/mestill tells you what is going on (quota.state: "unavailable"). - The plan store fails open to stale. If Level briefly can't confirm the plan, the
last known snapshot keeps serving for up to 60 minutes, and
/v1/mereportsplan.state: "stale"while it does. Past that, metered paths answer503 billing-unavailable.
Both carry Retry-After. Neither is your fault and neither needs a code change —
retry after the interval.
The thirty-second deadline
Every /v1 request has a 30-second budget. One that reaches it is cancelled and
answered 503 request-timeout with Retry-After.
It is deliberately not a 500: nothing failed, the request was simply larger than
one call can carry. The fix is on your side — narrow the date range or the ad-account
set rather than repeating the same call.
Building a well-behaved client
Back off on 429 and 503
Honour Retry-After. Do not retry immediately, and do not retry a 400 or a 403
at all — those need a changed request, not a second attempt.
Cache with ETags
Store the ETag per report and range; a 304 costs nothing and tells you nothing
changed.
Pull wide, not often
One 30-day call beats thirty one-day calls: the weighted cost is the same or lower, and it is one round trip instead of thirty.
Watch /v1/me
quota.remaining, plan.state and nearestSecretExpiry are the three numbers worth
an alert. All three are free to read.