> ## Documentation Index
> Fetch the complete documentation index at: https://docs.footstep.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Error codes and how to handle them

When a request fails, the API returns a consistent JSON error body with an HTTP status code. This page covers every error you might encounter, what causes it, and how to handle it.

## Error shape

Every error response has the same structure:

```json theme={null}
{
  "error": "error_code",
  "message": "Human-readable description"
}
```

The `error` field is a stable, machine-readable code you can match on in your code. The `message` field is a human-readable explanation that may change over time, so don't parse it programmatically.

## Error codes

### Client errors (4xx)

These indicate a problem with the request. Fix the issue before retrying.

| HTTP status | Code                | Description                            | Common cause                                                                                                                                                                                              |
| ----------- | ------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`       | `bad_request`       | Invalid request body or parameters     | Malformed JSON, missing required fields, coordinates outside valid range                                                                                                                                  |
| `401`       | `unauthorized`      | Missing or malformed API key           | No `x-api-key` header, or key doesn't match the `sk_live_*` format                                                                                                                                        |
| `402`       | `payment_required`  | Account balance is empty               | Top up at [console.footstep.ai](https://console.footstep.ai). The key is fine — only the balance is blocking the request                                                                                  |
| `403`       | `forbidden`         | Invalid, suspended, or revoked API key | Key was deleted, suspended by admin, or doesn't exist                                                                                                                                                     |
| `404`       | `not_found`         | Endpoint does not exist                | Typo in the URL path, or using an unsupported HTTP method                                                                                                                                                 |
| `408`       | `request_timeout`   | Request took too long to process       | Very long routes, complex optimisation problems, or temporary server load                                                                                                                                 |
| `429`       | `too_many_requests` | Rate limit exceeded                    | Per-product per-second cap exceeded for your API key. Back off and retry — the response body's `retryAfter` field gives the milliseconds until the next request will succeed. See per-product caps below. |

### Rate limits per product

Each product has its own per-second budget per API key. Hitting one cap does not affect the others — a customer using the routing API at full speed has full geocoding capacity available in parallel.

| Product   | Default cap    | Endpoints                                                                                               |
| --------- | -------------- | ------------------------------------------------------------------------------------------------------- |
| Geocoding | **10 req/sec** | `/v1/geocoding/*`, `/v1/routing/find-and-route`, `/v1/routing/search-along-route`                       |
| Routing   | **20 req/sec** | `/v1/routing/route`, `/matrix`, `/isochrone`, `/snap`, `/locate`, `/elevation`, `/optimize`, `/compare` |
| AI        | **5 req/sec**  | `/v1/ai/*`                                                                                              |
| Predict   | **2 req/sec**  | `/v1/predict`                                                                                           |
| Telemetry | **50 req/sec** | `/v1/telemetry/*`                                                                                       |

Note that `/v1/routing/find-and-route` and `/v1/routing/search-along-route` count against the **geocoding** cap, not the routing cap, because they internally perform a geocode.

When you exceed a cap, the response is `429 too_many_requests` with a `retryAfter` field giving the milliseconds until your next request will succeed.

### Server errors (5xx)

These indicate a problem on our side. They are usually transient and safe to retry.

| HTTP status | Code                  | Description                            | Common cause                                                 |
| ----------- | --------------------- | -------------------------------------- | ------------------------------------------------------------ |
| `502`       | `bad_gateway`         | Upstream service unreachable           | The routing engine is starting up or temporarily unreachable |
| `503`       | `service_unavailable` | Routing engine temporarily unavailable | Maintenance or deployment in progress                        |
| `504`       | `gateway_timeout`     | Upstream service timed out             | The routing engine took too long to respond                  |

## Handling errors in code

Check the HTTP status code first, then read the `error` field for specifics:

<CodeGroup>
  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.footstep.ai/v1/routing/route", {
    method: "POST",
    headers: {
      "x-api-key": process.env.FOOTSTEP_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ locations }),
  });

  if (!response.ok) {
    const { error, message } = await response.json();

    switch (error) {
      case "bad_request":
        // Fix the request, don't retry
        console.error("Invalid request:", message);
        break;
      case "unauthorized":
      case "forbidden":
        // Check your API key
        console.error("Auth failed:", message);
        break;
      case "too_many_requests":
        // Back off and retry
        break;
      default:
        // Server error, retry with backoff
        break;
    }
  }
  ```

  ```python Python theme={null}
  import os
  import requests

  response = requests.post(
      "https://api.footstep.ai/v1/routing/route",
      headers={"x-api-key": os.environ["FOOTSTEP_API_KEY"]},
      json={"locations": locations},
  )

  if not response.ok:
      data = response.json()
      error = data["error"]
      message = data["message"]

      if error == "bad_request":
          # Fix the request - don't retry
          raise ValueError(f"Invalid request: {message}")
      elif error in ("unauthorized", "forbidden"):
          # Check your API key
          raise PermissionError(f"Auth failed: {message}")
      elif error == "too_many_requests":
          # Back off and retry
          pass
      else:
          # Server error - retry with backoff
          pass
  ```
</CodeGroup>

## Retry strategy

Some errors are transient and safe to retry. Use exponential backoff with jitter to avoid thundering herd problems, where all your retries hit the server at the same time:

```javascript theme={null}
async function fetchWithRetry(url, options, maxRetries = 3) {
  const retryable = new Set([408, 429, 502, 503, 504]);

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.ok || !retryable.has(response.status)) {
      return response;
    }

    if (attempt < maxRetries) {
      // Exponential backoff: 1s, 2s, 4s... capped at 30s
      const delay = Math.min(1000 * 2 ** attempt, 30000);
      // Add random jitter (50-100% of delay) to spread out retries
      const jitter = delay * (0.5 + Math.random() * 0.5);
      await new Promise((resolve) => setTimeout(resolve, jitter));
    }
  }
}
```

| Safe to retry                 | Do not retry                  |
| ----------------------------- | ----------------------------- |
| `408` `429` `502` `503` `504` | `400` `401` `402` `403` `404` |

## Common mistakes

<AccordionGroup>
  <Accordion title="Getting 401 but I'm sending a key">
    Make sure you're using the `x-api-key` header (not `Authorization` or `api-key`), and that your key starts with `sk_live_`. Keys passed as query parameters or in the request body are not recognised.
  </Accordion>

  <Accordion title="Getting 400 with valid-looking coordinates">
    Check that latitude is between -90 and 90, and longitude is between -180 and 180. A common mistake is swapping lat/lon. If your latitude is something like `51.5` but your longitude is `151.2`, the coordinates may point to an area with no road network data.
  </Accordion>
</AccordionGroup>
