Skip to main content

Errors

Every way the Level API can say no — seventeen problem types, what each one means, and which of them are worth retrying.

Errors are returned as RFC 9457 problem documents with the content type application/problem+json:

{
  "type": "https://docs.marketing-bar.com/api/problems/insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This endpoint requires reports:data.",
  "instance": "/v1/reports/8a1b0c2d…/data",
  "traceId": "0HN7…",
  "retryAfterSeconds": null
}

Branch on type, never on title or detail. The catalogue below is closed — there are seventeen types and there will not be an eighteenth without a version bump — while the human wording may be improved at any time.

traceId identifies the request in Level's logs. Quote it when you contact support.

The catalogue

TypeHTTPRetry?
invalid-token401After getting a new token
insufficient-scope403No
insufficient-permission403Not until someone fixes the access
plan-feature-required403No
subscription-locked403No
not-found404No
invalid-parameter400No
conflict409Only after changing the request
idempotency-key-mismatch422With a new key
rate-limit-exceeded429Yes, after Retry-After
rate-limiter-unavailable503Yes
billing-unavailable503Yes
authorization-unavailable503Yes
identity-unavailable503Yes
data-source-unavailable503Later
request-timeout503With a smaller request
internal-error500Yes, once

The caller's side

invalid-token — 401 {#invalid-token}

The token is missing, malformed, expired, belongs to a revoked client, is a signed-in person's token rather than an API client's, or was issued for a different audience.

Level does not say which: telling a caller why a credential was refused is help offered to whoever is guessing.

What to do. Exchange the client secret for a fresh token and retry once. If the new token is refused too, the client or its secret has been revoked — check the API clients screen.

insufficient-scope — 403 {#insufficient-scope}

A valid token whose scopes don't cover this endpoint — asking for report data with only reports:read, for instance.

What to do. Two possibilities: you asked for too few scopes in the token request, or the client was never granted the scope. GET /v1/me shows what the token actually holds. Granting a new scope means editing the client — no code change fixes it.

insufficient-permission — 403 {#insufficient-permission}

A write only. The scope is there, but the person who created this API client no longer holds the permission the write needs in the organization — they left the team, their role changed, or (for a report) they lost Edit or Full access to that report. Level checks this on every write, not at the moment the client was issued.

A token issued before this check existed carries no creator at all and gets the same answer; ask for a new one.

What to do. Restore the permission for that person, or re-create the client under someone who holds it. Reads are unaffected in the meantime.

plan-feature-required — 403 {#plan-feature-required}

The organization owner's plan doesn't include the public API (Business and above). detail names the plan.

What to do. Nothing technical. See Plans & limits.

subscription-locked — 403 {#subscription-locked}

The owner is over their plan limits and everything they own is read-locked. On /v1 this blocks reads too, unlike in the app — but /v1/me, /v1/health, /v1/docs and /v1/openapi.json keep answering, so an integration can always read that it is locked.

What to do. The owner has to delete down to fit the plan or upgrade. See what happens if you go over the limits.

not-found — 404 {#not-found}

No such resource — or one that exists but is outside this client's allow-list, or belongs to another organization. All three answer the same way on purpose: a 403 would confirm that somebody else's row exists.

What to do. Check the id, then check the client's allow-list in the API clients screen.

invalid-parameter — 400 {#invalid-parameter}

A parameter is missing, malformed or out of range: pageSize over 200, a date range wider than 400 days, an unknown metric name, a groupBy this report can't answer. detail says which — and for groupBy, which ones it can.

What to do. Fix the request. Retrying it unchanged will fail identically.

conflict — 409 {#conflict}

The write clashes with what already exists: a duplicate segment name, a target whose period overlaps another for the same ad account and metric, an ad account already linked to another organization — or an Idempotency-Key whose first request is still being processed.

What to do. Read detail, change the request, send it again. For the in-flight idempotency case, wait for the first request to finish and read its answer instead.

idempotency-key-mismatch — 422 {#idempotency-key-mismatch}

This Idempotency-Key was used within the last 24 hours for a different request — the key is fingerprinted on the method, path, query and body, and Level will not let one key stand for two different writes.

What to do. Use a fresh key for a different write. If you meant to retry, send the byte-identical request the key was first used with and you'll get the original response back, marked Idempotent-Replayed: true.

rate-limit-exceeded — 429 {#rate-limit-exceeded}

The hourly budget of the subscription is spent, or this client has reached its 50 % fair share while others still have room. Retry-After carries the seconds.

What to do. Wait out Retry-After, then look at Quotas & rate limits — ETag caching and wider, rarer pulls are usually the whole fix.

Level's side

rate-limiter-unavailable — 503 {#rate-limiter-unavailable}

Level can't meter requests right now, and an unmetered request is not allowed through. Fail-closed, one mode only.

What to do. Retry after Retry-After. /v1/me still answers and reports quota.state: "unavailable".

billing-unavailable — 503 {#billing-unavailable}

The owner's plan couldn't be established — not even from a snapshot, which is served for up to 60 minutes when the billing store is briefly unreachable.

What to do. Retry. /v1/me reports plan.state: "unknown" while this lasts.

authorization-unavailable — 503 {#authorization-unavailable}

A write only. Level could not reach the service that answers may this person do this right now, so it refused rather than guess. Nothing was written.

What to do. Retry with backoff. A write that was never applied is safe to repeat — and safer still with an Idempotency-Key.

identity-unavailable — 503 {#identity-unavailable}

Only on the client-registration path, and therefore only ever seen in the app, not by an integration: the service that holds credentials didn't answer.

What to do. Try creating or rotating the client again in a minute.

data-source-unavailable — 503 {#data-source-unavailable}

An ad platform connection expired or lost a permission, so there is nothing to serve — and it isn't your request's fault.

What to do. Someone has to reconnect the platform in Level. See Managing connections. Retrying earlier than that won't help.

request-timeout — 503 {#request-timeout}

The request hit the thirty-second deadline and was cancelled.

What to do. Ask for less: a shorter date range, fewer ad accounts via adAccountIds, a smaller pageSize. Repeating the same call will time out again.

internal-error — 500 {#internal-error}

Anything else. It carries no detail, ever.

What to do. Retry once. If it persists, contact support with the traceId from the response.