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
| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | invalid_request | A required parameter is missing, a value is malformed or out of range, more than 100 elevation points, more than 10 results requested, a plain HTTP request. | Fix the request. Do not retry unchanged. |
| 401 | invalid_key | The key does not exist or has been revoked. | Check the key. A request with a bad key does not fall back to the free tier. |
| 402 | no_credits | The account's free allowance for the day is used and its credit balance is zero. | Add credits in the dashboard, by card or crypto, or wait for the reset. |
| 403 | key_ip_limit | The key has already been used from two other IP addresses in the last 24 hours. | Use the key from a recorded address, whitelist this server, or create a key for it. See the two IP rule. |
| 403 | account_ip_limit | Unlimited plan: the account has already been used from two other IP addresses in the last 24 hours. | Use one of the recorded addresses, or switch to credits, which have no account-wide address limit. |
| 403 | forbidden | The account is suspended for a terms breach. | Check email from us. |
| 404 | not_found | The path does not exist. Note that a query with no matches is 200 with empty results, not 404. | Check the path and version prefix. |
| 405 | method_not_allowed | Anything other than GET (or POST where an endpoint documents it). | Use GET. |
| 429 | quota_exceeded | The anonymous daily allowance for this IP or /40 is used up. | Wait for X-Quota-Reset or attach an account. Do not retry in a loop. |
| 429 | rate_limited | More than the burst limit in one second. | Back off and retry. Retry-After: 1. |
| 500 | server_error | Something broke on our side. | Retry once after a second. It is logged and alerts us. Not counted. |
| 503 | unavailable | The endpoint or its data is temporarily unavailable, typically during a data release. | Retry with backoff. Retry-After is set. Not counted. |
Things that are not errors
- No results. A forward geocode of gibberish returns
200with"results": []. A reverse geocode in the ocean returns200with"result": null. Both count as a request. - Low precision. A result with
"precision": "admin"when you wanted a house is a successful request that found less than you hoped. Check the field. - Private IP addresses. Looking up
10.0.0.1returns200with"is_private": trueand no location.
Errors on the drop-in hosts
The compatibility hosts return errors in the shape the original provider uses, so existing handling works. For example, a used-up quota 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. 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 code == "rate_limited" or r.status_code in (500, 503):
time.sleep(0.2 * (2 ** attempt))
continue
# quota_exceeded, invalid_request, invalid_key, key_ip_limit: 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)