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

# Errors

> Every error code AcreLens can return, the HTTP status it ships with, and how to handle each.

## Error envelope

Every error response uses the same shape:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed.",
    "request_id": "req_01HY2Z...",
    "retriable": false,
    "docs_url": "https://docs.acrelens.com/errors/validation_failed",
    "fields": [
      { "field": "address", "message": "Required" },
      { "field": "mode", "message": "Invalid mode" }
    ]
  }
}
```

| Field | Always present? | Notes |
| - | - | - |
| `code` | ✅ | Stable string identifier — switch on this in your client |
| `message` | ✅ | Human-readable summary |
| `request_id` | ✅ | Quote this when contacting support — it's also returned in the `x-request-id` response header |
| `retriable` | ✅ | `true` only for 5xx codes. Many `false` codes are still retriable in your logic (e.g. `429` after waiting) |
| `docs_url` | ✅ | Direct link to that code's section on this page |
| `fields` | only on `422 validation_failed` | Per-field messages for invalid input |

## Error code catalog

| Code | HTTP | When | Retriable in client? |
| - | - | - | - |
| [`unauthorized`](#unauthorized) | 401 | Missing, malformed, or revoked API key | No — fix the key |
| [`forbidden`](#forbidden) | 403 | Account is `CANCELED` or `PAST_DUE` | No — update billing |
| [`api_access_required`](#api_access_required) | 403 | Plan doesn't include REST API access (Free / Starter subscription) | No — upgrade to Pro |
| [`insufficient_balance`](#insufficient_balance) | 402 | Balance too low and no free reports left | No — top up |
| [`DAILY_CAP_EXCEEDED`](#daily_cap_exceeded) | 402 | Daily spend cap reached | After 00:00 UTC |
| [`MONTHLY_CAP_EXCEEDED`](#monthly_cap_exceeded) | 402 | Monthly spend cap reached | After month rollover |
| [`not_found`](#not_found) | 404 | Resource doesn't exist or belongs to another customer | No |
| [`idempotency_conflict`](#idempotency_conflict) | 409 | `Idempotency-Key` reused with a different body | No — use a fresh key |
| [`validation_failed`](#validation_failed) | 422 | Request body / query failed validation | No — fix the request |
| [`rate_limit_exceeded`](#rate_limit_exceeded) | 429 | Too many requests per second | Yes — wait `Retry-After` |
| [`quota_exceeded`](#quota_exceeded) | 429 | Monthly report/quota limit reached | After period reset |
| [`upstream_error`](#upstream_error) | 502 | A dependency (Stripe, geocoder) is down | Yes — exponential backoff |
| [`internal_error`](#internal_error) | 500 / 503 | Unexpected server error | Yes — retry once, contact support if persistent |

***

## `unauthorized`

**HTTP:** 401

Your `Authorization` header is missing, malformed, doesn't match the `al_(live|test)_[a-zA-Z0-9]{32}` format, or the key has been revoked or never existed.

```json theme={null}
{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key.",
    "request_id": "req_...",
    "retriable": false,
    "docs_url": "https://docs.acrelens.com/errors/unauthorized"
  }
}
```

**Fix:** Check the header is `Authorization: Bearer al_live_...`. If the key was revoked, [create a new one](/authentication#rotating-keys).

***

## `forbidden`

**HTTP:** 403

Your account isn't in an `ACTIVE` or `TRIAL` state (e.g. `CANCELED` or `PAST_DUE`). The key is valid, but billing is blocking new requests.

**Fix:** Update your card in the [dashboard](https://acrelens.com/dashboard) or contact [support@acrelens.com](mailto:support@acrelens.com).

***

## `api_access_required`

**HTTP:** 403

Your plan does not include REST API access. The API is available on Pro and above — upgrade at [acrelens.com/pricing](https://acrelens.com/pricing). This applies to Free and Starter subscription accounts; legacy pay-as-you-go accounts keep API access.

**Fix:** Upgrade to Pro (or above) in the [dashboard](https://acrelens.com/dashboard), or see [pricing](/reference/pricing).

***

## `insufficient_balance`

**HTTP:** 402

You're a PAYG customer, your balance is below `$3.99`, and you have no free trial reports remaining.

**Fix:** Top up in the dashboard, or enable [auto-recharge](/concepts/quota-and-billing#balance--top-ups) so this doesn't happen again.

***

## `DAILY_CAP_EXCEEDED`

**HTTP:** 402

You set a daily spend cap and this request would push you over it.

**Fix:** Wait until 00:00 UTC for the cap to reset, or raise/remove the cap in [Manage → Limits](https://acrelens.com/dashboard/manage/limits). See [spend caps](/concepts/quota-and-billing#spend-caps).

***

## `MONTHLY_CAP_EXCEEDED`

**HTTP:** 402

Same as above, monthly. Resets on the first of the calendar month.

***

## `not_found`

**HTTP:** 404

The resource (report, batch, state profile) doesn't exist, or it belongs to a different customer. AcreLens deliberately returns the same code in both cases — we don't leak existence of other customers' resources.

**Fix:** Verify the ID. If you think a report has gone missing on your end, check the dashboard's recent reports view.

***

## `idempotency_conflict`

**HTTP:** 409

You sent the same `Idempotency-Key` as a recent request but the body differs. Idempotency requires the body to match (canonical JSON, key order doesn't matter).

**Fix:** Use a fresh `Idempotency-Key` for distinct requests. See the [idempotency guide](/guides/idempotency).

***

## `validation_failed`

**HTTP:** 422

Your request body or query parameters failed validation. The error envelope includes a `fields` array with per-field messages:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "Request validation failed.",
    "request_id": "req_...",
    "retriable": false,
    "docs_url": "https://docs.acrelens.com/errors/validation_failed",
    "fields": [
      { "field": "address", "message": "Required" },
      { "field": "state", "message": "Must be a 2-letter US state code" },
      { "field": "mode", "message": "Must be one of off_grid, rural_residential, recreational, investment" }
    ]
  }
}
```

**Fix:** Surface the per-field messages to your form, or log them and fix the request.

***

## `rate_limit_exceeded`

**HTTP:** 429

You're sending requests faster than your tier allows. The response includes a `Retry-After` header (seconds).

**Fix:** Wait `Retry-After`, jitter, and back off if you keep hitting it. See [rate limits](/reference/rate-limits) for the correct retry pattern.

***

## `quota_exceeded`

**HTTP:** 429

You've reached your monthly report/quota limit. Subscription accounts on the Free plan hit this after their monthly report allotment (3 reports) is used; legacy tier-based plans (DEVELOPER) also trigger it at their monthly quota. Legacy PAYG accounts don't have a report quota.

**Fix:** Wait for the period to reset, or [contact us](mailto:support@acrelens.com) about a higher limit.

***

## `upstream_error`

**HTTP:** 502

A downstream dependency we rely on (Stripe, geocoder, NREL) is failing. AcreLens marks this `retriable: true`.

**Fix:** Exponential backoff — `1s → 2s → 4s → 8s`. If it persists more than a few minutes, check [status.acrelens.com](https://status.acrelens.com) or email support.

***

## `internal_error`

**HTTP:** 500 or 503

An unexpected error on our side. AcreLens marks this `retriable: true`.

**Fix:** Retry once after a short delay. If it persists, contact support with the `request_id` — we can correlate it to the trace and root-cause faster than you can.

***

## Reading errors in code

Switch on `error.code`, not on HTTP status — multiple codes can share a status (`429` is both `rate_limit_exceeded` and `quota_exceeded`):

```javascript theme={null}
const res = await fetch(url, options);
const body = await res.json();

if (!res.ok) {
  switch (body.error.code) {
    case "rate_limit_exceeded":
      return retryAfter(res.headers.get("Retry-After"));
    case "insufficient_balance":
      return notifyOpsToTopUp();
    case "validation_failed":
      return surfaceFieldErrors(body.error.fields);
    case "internal_error":
    case "upstream_error":
      return retryWithBackoff(body.error.request_id);
    default:
      throw new AcreLensError(body.error);
  }
}
```

## Support

When something doesn't match this doc, send us the `request_id` from the error and a one-line description: [support@acrelens.com](mailto:support@acrelens.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.