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:
{
"error": {
"code": "INVALID_REQUEST",
"message": "The latitude value is invalid.",
"details": {
"field": "lat"
}
}
}| Field | Type | Description |
|---|---|---|
error.code | string | Stable, machine-readable code. Branch on this. |
error.message | string | Human-readable explanation. The wording may change, so do not parse it. |
error.details | object | Optional context, for example field (the invalid parameter), reason or limit. Only present when there is something useful to add. |
Error codes
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | INVALID_REQUEST | A 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. |
| 401 | INVALID_API_KEY | The 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. |
| 403 | API_KEY_REVOKED | The key was revoked in the dashboard. | Use another key or create a new one. |
| 403 | ENDPOINT_NOT_ALLOWED | The 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. |
| 404 | NOT_FOUND | Nothing 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. |
| 408 | REQUEST_TIMEOUT | The data service behind the API did not answer in time. | Retry after a short delay with exponential backoff. |
| 429 | RATE_LIMIT_EXCEEDED | The key used up its requests for the current one-minute window. | Wait for the number of seconds in the Retry-After header, then retry. |
| 500 | INTERNAL_ERROR | An unexpected error in the API. | Retry later. If it persists, report the X-Request-ID. |
| 502 | UPSTREAM_ERROR | The data service behind the API failed or returned an invalid answer. | Retry with backoff. |
| 503 | SERVICE_UNAVAILABLE | The 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
{
"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
{
"error": {
"code": "INVALID_API_KEY",
"message": "The API key is missing or invalid."
}
}403 API_KEY_REVOKED
{
"error": {
"code": "API_KEY_REVOKED",
"message": "This API key has been revoked."
}
}403 ENDPOINT_NOT_ALLOWED
{
"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
{
"error": {
"code": "NOT_FOUND",
"message": "No address or place was found near this location."
}
}408 REQUEST_TIMEOUT
{
"error": {
"code": "REQUEST_TIMEOUT",
"message": "The data service did not respond in time."
}
}429 RATE_LIMIT_EXCEEDED
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests.",
"details": {
"limit": 100
}
}
}500 INTERNAL_ERROR
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Internal server error."
}
}502 UPSTREAM_ERROR
{
"error": {
"code": "UPSTREAM_ERROR",
"message": "The upstream data service returned an error."
}
}503 SERVICE_UNAVAILABLE
{
"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 forRetry-After),REQUEST_TIMEOUT,UPSTREAM_ERROR,SERVICE_UNAVAILABLEandINTERNAL_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.