Errors

Errors are JSON, like everything else, with an HTTP status that matches and a code you can switch on. The message is a sentence meant for your logs, not for your users; it may change wording, the code never will.

The error envelope

{
  "status": "error",
  "error": {
    "code": "invalid_request",
    "message": "Parameter 'lat' must be a number between -90 and 90.",
    "param": "lat"
  }
}

param is present when a specific parameter caused the problem. X-Request-Id is set on error responses too; include it if you contact support.

Codes

HTTPcodeWhenWhat to do
400https_requiredThe request came over plain HTTP. Only HTTPS is served, so that a key is never sent in the clear.Call the same URL with https://.
400invalid_requestA required parameter is missing, a value is malformed or out of range, more than 100 points or addresses in a batch, more than 10 results requested.Fix the request. Do not retry unchanged.
401missing_keyNo key was sent and keyless access is switched off on this server. By default it is on, and a request without a key is answered.Send a key as X-API-Key, a bearer token or key=.
429quota_exceededThe address has used its 2,500 free requests for the day without a key (for calls from scripts on a web page: the website has).Wait for the reset (Retry-After and X-Quota-Reset say when), or send a key that carries credit or an Unlimited package.
401invalid_keyThe key does not exist.Check for typos or a truncated copy.
401key_revokedThe key was revoked in the dashboard.Use a current key.
402no_creditsThe account's free allowance for the day is used and its credit balance is empty.Add credit in the dashboard, by card or crypto, or wait for the reset.
403key_ip_limitAll of this key's IP slots are currently held by other addresses.Use one of the slot-holding machines, give this machine its own key, or add a package. See IP slots.
403browser_key_originA browser key was used from another website, or without an Origin or Referer header, or before its website was set.Call it only from scripts on its website; use a server key elsewhere. See keys and browsers.
403account_suspendedThe account is suspended.Check email from us or open a ticket.
403email_unverifiedThe account has not confirmed its email address yet.Open the confirmation link in the email we sent.
403package_suspendedThe Unlimited package behind the key is suspended.Open a ticket.
404not_foundThe path does not exist. A query with no matches is 200 with empty results, not 404.Check the path and version prefix.
429rate_limitedOne visitor sent more than 60 requests a minute on a browser key, or too many asynchronous batch jobs on a drop-in host are waiting to be fetched.Wait for Retry-After, or fetch the waiting results first.
429key_daily_capThe key has reached the daily spending limit its owner set.Wait for the reset at 00:00 UTC, or raise the limit on the key page.
500server_errorSomething broke on our side.Retry once after a second. It is logged and alerts us. Not counted.
503unavailableThe service is temporarily unavailable.Retry with backoff. Not counted.
503backend_unavailableThe elevation service is not answering.Retry with backoff (Retry-After is 10 seconds). Not counted.
503maintenanceThe API is in a maintenance window.Retry after Retry-After. Not counted.

Things that are not errors

Errors on the drop-in hosts

The compatibility hosts return errors in the shape the original provider uses, so existing handling works. For example, an empty credit balance is "status": "OVER_QUERY_LIMIT" with HTTP 200 on gapi.mygeocode.com, "statusCode": 429 in the Bing envelope on bing.mygeocode.com, and {"status":"fail","message":"quota"} on ipapi.mygeocode.com; a bad key is REQUEST_DENIED, 401 and invalid key respectively. The X-Quota-* headers are sent on every host regardless, so you can read the real state from the headers if you want to.

A reasonable handling strategy

def call(url, params, tries=3):
    for attempt in range(tries):
        r = session.get(url, params=params, timeout=10)
        if r.status_code == 200:
            return r.json()
        body = r.json()
        code = body["error"]["code"]
        if r.status_code in (500, 503):
            time.sleep(0.2 * (2 ** attempt))
            continue
        # no_credits, key_ip_limit, invalid_request, invalid_key: retrying will not help
        raise ApiError(code, body["error"]["message"], r.headers.get("X-Request-Id"))
    raise ApiError("retries_exhausted", "Gave up after %d attempts" % tries, None)