Skip to main content

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

PlanRequests per hour
FreeNo API access
ProNo API access
Business1 000
Enterprise5 000

The window is a fixed UTC clock hour: it resets on the hour, not an hour after your first call.

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:

CallCost
/v1/me, /v1/health, /v1/openapi.json, /v1/docs0
Any 304 Not Modified0 — the charge is refunded
/v1/reports/{id}/data, export.xlsx, export.csvmax(1, days × adAccounts / 1000)
Everything else1

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/me still 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/me reports plan.state: "stale" while it does. Past that, metered paths answer 503 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.