Reference

Errors and limits

Error shape

{
  "statusCode": 400,
  "message": "at must be an ISO timestamp",
  "error": "Bad Request"
}

message is a string, or an array when several validation errors apply. Log it — the messages name what was wrong.

StatusMeaningRetry?
400Malformed request — bad timestamp, bad cursorNo, fix the call
400invalid_purpose — see belowNo, fix the call
400webhook_url_not_registered — a rule named a URL that is not one of your endpointsNo, fix the call
401Token missing, revoked, or expiredNo — the user must reconnect
403missing_scope — the user never granted this scopeNo
403scope_expired — the sensitive tier lapsedNo — ask the user to renew
404Doesn't exist, or isn't visible to youNo
409A cap was reached (rules active at once, places)No
422Value out of range, e.g. radius_m below 100No, fix the call
429Rate or budget limitYes, with backoff
503A dependency is downYes, with backoff

Only 429 and 503 are worth retrying. Everything else means the request has to change.

Two kinds of 403

Both mean "not allowed", and neither is worth retrying, but they call for different words to the user. Branch on error, not on message.

missing_scope — the connection never held the scope, or the user or an admin narrowed it. Turn the feature off, or ask for the scope at consent.

{
  "statusCode": 403,
  "error": "missing_scope",
  "message": "grant is missing scope location.latest.read"
}

scope_expired — the connection held a sensitive scope and its tier lapsed on the date the user chose. Everything else on the connection still works. Until renewal_grace_ends_at renewing is one confirmation; after it the error becomes missing_scope, because the scope has been removed. See Two expiries.

{
  "statusCode": 403,
  "error": "scope_expired",
  "message": "access to location.latest.read expired; ask the user to renew it",
  "sensitive_expired_at": "2026-10-13T09:00:00.000Z",
  "renewal_grace_ends_at": "2026-10-27T09:00:00.000Z"
}

Invalid purpose

Every call to /v1/locations/latest, /lookup, /timerange and /days names a registered purpose in the purpose query parameter. A 400 with error: "invalid_purpose" means it was missing, is not a purpose of this app, is not approved for a production app, or does not cover the scope the endpoint needs. detail says which.

{
  "statusCode": 400,
  "error": "invalid_purpose",
  "message": "purpose trip_map is not registered for scope location.history.read",
  "detail": "purpose trip_map is not registered for scope location.history.read"
}

Check the key against your registered purposes in code, before the request: an invalid_purpose is a mismatch between your code and your portal settings, never a user's choice.

The limits

Two levels. Per connection bounds what one user is exposed to; per app bounds how much damage a leaked client secret or a runaway loop can do across every user at once.

LimitPer connectionPer appApplies to
Requests600 / hour2,000 + 300 × connections per hour, max 1,000,000Every endpoint
Questions60 / hour, 300 / day1,000 + 200 × connections per day, max 500,000verify-zone, current-place, presence
Different areas12 / day—verify-zone
Rules active at once10—/v1/rules/* — see windows
Calls per purpose2 × your expected daily volume, flagged—/v1/locations/* — see purposes
Sensitive tier90 days at most, then 14 days to renew—/v1/locations/* — see two expiries

Sandbox apps are flat instead of scaled — 5,000 requests an hour and 2,000 questions a day — because only their own developer can connect them.

The purpose volume is a flag, not a refusal. Each purpose you register states how many calls a day one connection should need. A connection that makes more than twice that for one purpose in a day is flagged for review; the calls themselves still answer. See Purposes.

Per-connection caps are the ones you'll meet. 60 questions an hour is about one a minute, sustained, and the question budget is shared across the three answer endpoints, so alternating between them doesn't help.

The per-app ceilings grow with you. They're a floor plus an allowance for every live connection, so an app that doubles its users doesn't have to ask us for a raise. We email the developer at 80% of the daily question ceiling, so a 429 at that level should never be the first you hear of it. If the scaling is simply wrong for what your app does, reply to that email — a ceiling can be set for one app without waiting on a release.

Different-areas budget — 12 a day, verify-zone only. Zone centers are rounded to roughly 1.1 km cells, and asking about more than twelve different cells in a day is refused.

A 429 names which ceiling you hit, and the two are worth handling differently: a per-connection limit means one integration is asking too often, while an app-wide one usually means all of them are.

Why the different-areas budget exists

How often you ask says little. An app polling one delivery zone every minute is behaving normally. An app asking about a dozen scattered areas is narrowing down where someone is by elimination.

So the limit is on variety, not frequency. Asking about the same zone again is free. Only new territory counts.

Tripping it does more than return 429. It also:

  • flags the connection,
  • writes a line the user reads: "blocked: asked about more than 12 separate areas in 24h",
  • marks the connection as abuse-flagged wherever it is shown,
  • and emails them, at most once a week per app.

If your product genuinely needs many zones — a courier app watching twenty depots is a reasonable thing to build — talk to us rather than spreading queries across connections. Working around it looks the same as abuse from where we're standing.

Handling 429

There's no Retry-After header yet. Back off exponentially starting around a minute, and cap your retries.

Two things not to do:

  • Don't retry a different-areas 429 with a different zone. That advances the budget further and looks exactly like the sweep the limit exists to stop.
  • Don't fan out across users. A limit hit on one connection says something about your query pattern, which probably applies to the rest.

If you're hitting the question budget, the fix is usually fewer requests, not more: register a rule and be told instead of asking.

What happens on our side

Both budgets fail open. If our rate-limit store has a problem, requests are allowed through rather than refused — a limiter should never become the outage.

The privacy guarantees don't depend on it. The margin around every zone is enforced in the answer itself.

So you can't infer anything from a limit not firing.

Previous
Webhook events