Skip to content
Errors

Guides

Errors

Every error response has the same shape, with a stable code you can branch on.

Error format

Any response with a status of 400 or above has this JSON body, whatever the endpoint and whatever went wrong:

400 Bad Request
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "The latitude value is invalid.",
    "details": {
      "field": "lat"
    }
  }
}
Error fields
FieldTypeDescription
error.codestringStable, machine-readable code. Branch on this.
error.messagestringHuman-readable explanation. The wording may change, so do not parse it.
error.detailsobjectOptional context, for example field (the invalid parameter), reason or limit. Only present when there is something useful to add.

Error codes

Errors
StatusCodeMeaningWhat to do
400INVALID_REQUESTA parameter is missing or invalid. details.field names it. Also used with status 405 for a wrong HTTP method and 413 for an oversized body.Fix the request; retrying it unchanged will fail again.
401INVALID_API_KEYThe Authorization header is missing or malformed, the key does not exist, or it has expired (details.reason is expired).Send a valid key as Authorization: Bearer YOUR_API_KEY.
403API_KEY_REVOKEDThe key was revoked in the dashboard.Use another key or create a new one.
403ENDPOINT_NOT_ALLOWEDThe key is not allowed to call this endpoint. Each key is limited to the endpoints chosen when it was created; details.allowed_endpoints lists them.Use a key that includes this endpoint, or create one in the dashboard.
404NOT_FOUNDNothing was found: no address near the point (reverse geocoding), no route or no road near a waypoint (routing, details.reason explains), or an unknown path.Check the coordinates. For routing, move the point closer to a road or try another mode.
408REQUEST_TIMEOUTThe data service behind the API did not answer in time.Retry after a short delay with exponential backoff.
429RATE_LIMIT_EXCEEDEDThe key used up its requests for the current one-minute window.Wait for the number of seconds in the Retry-After header, then retry.
500INTERNAL_ERRORAn unexpected error in the API.Retry later. If it persists, report the X-Request-ID.
502UPSTREAM_ERRORThe data service behind the API failed or returned an invalid answer.Retry with backoff.
503SERVICE_UNAVAILABLEThe service is temporarily unavailable, for example while a data source is being configured or loaded.Retry with backoff. details.retryable is true when a retry can succeed.

400 INVALID_REQUEST

400 response
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Invalid value for 'lat': input should be less than or equal to 90",
    "details": {
      "field": "lat"
    }
  }
}

401 INVALID_API_KEY

401 response
{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The API key is missing or invalid."
  }
}

403 API_KEY_REVOKED

403 response
{
  "error": {
    "code": "API_KEY_REVOKED",
    "message": "This API key has been revoked."
  }
}

403 ENDPOINT_NOT_ALLOWED

403 response
{
  "error": {
    "code": "ENDPOINT_NOT_ALLOWED",
    "message": "This API key is not allowed to call this endpoint. Create a key that includes it.",
    "details": {
      "allowed_endpoints": [
        "geocode",
        "reverse-geocode"
      ]
    }
  }
}

404 NOT_FOUND

404 response
{
  "error": {
    "code": "NOT_FOUND",
    "message": "No address or place was found near this location."
  }
}

408 REQUEST_TIMEOUT

408 response
{
  "error": {
    "code": "REQUEST_TIMEOUT",
    "message": "The data service did not respond in time."
  }
}

429 RATE_LIMIT_EXCEEDED

429 response
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests.",
    "details": {
      "limit": 100
    }
  }
}

500 INTERNAL_ERROR

500 response
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error."
  }
}

502 UPSTREAM_ERROR

502 response
{
  "error": {
    "code": "UPSTREAM_ERROR",
    "message": "The upstream data service returned an error."
  }
}

503 SERVICE_UNAVAILABLE

503 response
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "This service is temporarily unavailable.",
    "details": {
      "retryable": true
    }
  }
}

Handling errors

  • Fix and resend: INVALID_REQUEST, NOT_FOUND. Retrying the same request gives the same answer.
  • Fix the credentials: INVALID_API_KEY, API_KEY_REVOKED. Do not retry in a loop.
  • Retry with backoff: RATE_LIMIT_EXCEEDED (wait for Retry-After), REQUEST_TIMEOUT, UPSTREAM_ERROR, SERVICE_UNAVAILABLE and INTERNAL_ERROR. Use exponential backoff with a cap, and give up after a few attempts.
const RETRYABLE = new Set(["RATE_LIMIT_EXCEEDED", "REQUEST_TIMEOUT", "UPSTREAM_ERROR", "SERVICE_UNAVAILABLE"]);

async function getWithRetry(url: string, attempts = 3): Promise<unknown> {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.GEO_API_KEY}` },
      signal: AbortSignal.timeout(10_000),
    });
    if (response.ok) return response.json();
    const { error } = (await response.json()) as { error: { code: string; message: string } };
    if (!RETRYABLE.has(error.code) || attempt === attempts) {
      throw new Error(`${error.code}: ${error.message}`);
    }
    // Honour Retry-After on 429, otherwise back off exponentially.
    const retryAfter = Number(response.headers.get("Retry-After"));
    const waitMs = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }
}

Request IDs

Every response, successful or not, carries an X-Request-ID header. Log it with errors and include it when you ask for help: it identifies the exact request. You can also send your own X-Request-ID (8–64 letters, digits, ., _ or -) to correlate requests with your logs; otherwise one is generated. Rate-limit headers are described in Rate limits.