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

# Errors

> Canonical error envelope, the 10-value type enum, the full code reference, and an agent-actionable recovery matrix.

# Errors

Every non-2xx response uses the **same envelope**. Agents branch on `error.type` (a stable 10-value enum); each error carries `nextActions[]` — concrete steps to surface to the user.

## Envelope shape

```json theme={null}
{
  "error": {
    "type": "rate_limited",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded (rpm_exceeded). Retry after 23s.",
    "doc": "https://docs.mareaalcalina.com/concepts/rate-limits",
    "param": null,
    "requestId": "req_30a9358b-70bd-44f3-aa5d-8983b558ad84",
    "requestLogUrl": "https://...",
    "recoverable": true,
    "retryAfterMs": 23000,
    "nextActions": [
      { "label": "Wait 23s and retry the same request.", "method": null, "url": null }
    ],
    "upgrade": null
  }
}
```

## Field guarantees

| Field            | Type                 | Notes                                                                                              |
| ---------------- | -------------------- | -------------------------------------------------------------------------------------------------- |
| `type`           | enum (10 values)     | Stable. Agents branch on this. See the table below.                                                |
| `code`           | string               | Stable, machine-readable. Finer-grained than `type`.                                               |
| `message`        | string               | Human-readable. **Localized** via `Accept-Language`. Do not branch on it.                          |
| `doc`            | string               | Link to the relevant docs page for this code.                                                      |
| `param`          | string \| `null`     | Name of the offending field (`null` if not field-specific). Never missing.                         |
| `requestId`      | string               | `req_<uuid>` — surface to the user when reporting bugs.                                            |
| `requestLogUrl`  | string               | Internal log URL (operator-only).                                                                  |
| `recoverable`    | boolean              | `true` means a retry is meaningful (with appropriate changes).                                     |
| `retryAfterMs`   | integer \| `null`    | When set, also returned as the `Retry-After` header (in seconds).                                  |
| `nextActions`    | array                | **Always an array** (possibly empty). `null` is not allowed. Each entry: `{ label, method, url }`. |
| `upgrade`        | object \| `null`     | `{ currentPlan, requiredPlan, upgradeUrl, previewUrl? }`. Set only on `plan_limit` errors.         |
| `requiredScopes` | string\[] (optional) | Present only when the failure is a scope mismatch.                                                 |
| `heldScopes`     | string\[] (optional) | Present only when the failure is a scope mismatch.                                                 |

## The 10 error types

| `type`                 | Typical HTTP    | When it happens                                                             | Agent action                                                                                                                          |
| ---------------------- | --------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `rate_limited`         | 429             | rpm or rpd quota exceeded                                                   | Read `retryAfterMs`, sleep, retry. `recoverable: true`. See [Rate limits](/concepts/rate-limits).                                     |
| `invalid_request`      | 400 / 410 / 422 | Malformed body, invalid `Idempotency-Key`, oversize snapshot, empty publish | Fix the body and reissue. Do not retry the exact same request.                                                                        |
| `auth`                 | 401 / 403       | Missing / invalid / revoked key, missing scope                              | 401 → fix credential. 403 with `requiredScopes` → request an upscoped key.                                                            |
| `not_found`            | 404             | Resource missing OR cross-tenant access (silent denial)                     | Confirm the id is correct AND the key owns it.                                                                                        |
| `plan_limit`           | 402             | Plan ceiling hit (publish blocked, product cap exceeded)                    | Surface `upgrade.upgradeUrl` to the user as a CTA. See [Plan limits](/concepts/plan-limits).                                          |
| `internal`             | 500             | Server bug                                                                  | Surface `requestId`. Retry once with backoff, then escalate.                                                                          |
| `conflict`             | 409             | `Idempotency-Key` in flight (same key still processing)                     | Wait `retryAfterMs` and retry with the same key.                                                                                      |
| `idempotency_conflict` | 409             | Same `Idempotency-Key` reused with a **different** body                     | Generate a NEW `Idempotency-Key`. See [Safe mutations](/concepts/safe-mutations).                                                     |
| `service_unavailable`  | 503             | Dependency outage or feature flag off                                       | Retry with exponential backoff.                                                                                                       |
| `tos_not_accepted`     | 451             | Calling user has not accepted the Terms of Service                          | Surface `nextActions[0]` (dashboard link). User must accept; agent cannot bypass. See [ToS jurisdiction](/concepts/tos-jurisdiction). |

## Recovery matrix

| If `error.type` is                 | Surface to user?         | Retry?                   | What to do                                          |
| ---------------------------------- | ------------------------ | ------------------------ | --------------------------------------------------- |
| `rate_limited`                     | No (transparent)         | Yes after `retryAfterMs` | Sleep + retry same request + same `Idempotency-Key` |
| `invalid_request`                  | No (fix client-side)     | No (without fixing)      | Fix the body. Do not retry blindly.                 |
| `auth` (401)                       | Maybe (credential issue) | No                       | Re-prompt for a valid key                           |
| `auth` (403, scope)                | Maybe                    | No                       | Request a key with `requiredScopes`                 |
| `not_found`                        | Maybe ("not found")      | No                       | Confirm id; confirm key ownership                   |
| `plan_limit`                       | **Yes** (paywall CTA)    | No                       | Surface `upgrade.upgradeUrl` verbatim               |
| `internal`                         | Yes (with `requestId`)   | Once with backoff        | Then escalate                                       |
| `conflict` (in-flight idempotency) | No                       | Yes after 1s             | Retry with same `Idempotency-Key`                   |
| `idempotency_conflict`             | No                       | Yes with NEW key         | Generate a fresh `Idempotency-Key`                  |
| `service_unavailable`              | Yes (after retries)      | Yes with backoff         | Exponential backoff                                 |
| `tos_not_accepted`                 | **Yes** (ToS link)       | After user accepts       | Surface `nextActions[0].url`                        |

## Stable code reference

Every code below is emitted by production code today. Agents may branch on `code` for finer-grained handling than `type`.

### Auth (401 / 403)

| `code`                         | HTTP | Source                                                                 |
| ------------------------------ | ---- | ---------------------------------------------------------------------- |
| `missing_authorization`        | 401  | `src/api/middleware/apiKey.ts`                                         |
| `invalid_authorization_format` | 401  | `src/api/middleware/apiKey.ts`                                         |
| `key_not_found`                | 401  | `src/api/middleware/apiKey.ts`                                         |
| `key_revoked`                  | 401  | `src/api/middleware/apiKey.ts`                                         |
| `insufficient_scope`           | 403  | `src/api/middleware/scope.ts` — includes `requiredScopes`/`heldScopes` |
| `developer_context_unresolved` | 401  | Bootstrap / issue-key handlers when developer ctx is missing           |
| `tenant_unresolved`            | 401  | Catalog / verify handlers when owner ctx is missing                    |
| `developer_not_found`          | 404  | `src/api/v1/me.get.ts` — dev account missing from Firebase Auth        |

### Validation (400 / 410 / 413 / 422)

| `code`                             | HTTP | Source                                                     |
| ---------------------------------- | ---- | ---------------------------------------------------------- |
| `invalid_request`                  | 400  | Zod validation failure (per-field)                         |
| `invalid_json`                     | 400  | Body parsing failure                                       |
| `invalid_idempotency_key`          | 400  | `src/api/middleware/idempotency.ts`                        |
| `invalid_storefront_id`            | 400  | `src/api/services/translation/storefront-id.ts`            |
| `invalid_product_id`               | 400  | `src/api/services/translation/storefront-id.ts`            |
| `invalid_email_syntax`             | 400  | Bootstrap email validation                                 |
| `invalid_email_mx`                 | 400  | Bootstrap MX-record check                                  |
| `code_invalid`                     | 400  | `src/api/v1/users.verify.ts`                               |
| `code_expired`                     | 410  | `src/api/v1/users.verify.ts`                               |
| `idempotency_snapshot_unavailable` | 410  | `src/api/middleware/idempotency.ts` (oversize replay)      |
| `payload_too_large`                | 413  | Body parser                                                |
| `no_products`                      | 422  | `src/api/v1/storefronts.publish.ts` (empty storefront)     |
| `user_not_verified`                | 422  | `src/api/v1/users.issueKey.ts` (pre-verify additional key) |
| `blocks_explicit_not_supported`    | 422  | Storefront manifest (legacy block fields)                  |
| `theme_preset_not_supported`       | 422  | Storefront manifest (theme presets not on plan)            |

### Not found (404)

| `code`                 | HTTP | Source                                                         |
| ---------------------- | ---- | -------------------------------------------------------------- |
| `storefront_not_found` | 404  | `src/api/v1/storefronts.*` — also fires on cross-tenant access |
| `user_not_found`       | 404  | Verify, resend, issue-key handlers — also cross-tenant         |
| `code_not_found`       | 404  | `src/api/v1/users.verify.ts` — no active verification code     |

### Conflicts (409)

| `code`                  | HTTP | Source                                                         |
| ----------------------- | ---- | -------------------------------------------------------------- |
| `idempotency_in_flight` | 409  | `src/api/middleware/idempotency.ts` — emits `Retry-After: 1`   |
| `idempotency_conflict`  | 409  | `src/api/middleware/idempotency.ts` — different body, same key |
| `email_exists`          | 409  | Bootstrap — email already registered                           |

### Plan / paywall (402)

| `code`                         | HTTP    | Source                                                                                             |
| ------------------------------ | ------- | -------------------------------------------------------------------------------------------------- |
| `plan_blocks_publish`          | 402     | `src/api/v1/storefronts.publish.ts` (`NO_ACTIVO` plan)                                             |
| `plan_max_products_reached`    | 402     | `src/services/catalog/productCreateCore.ts` — single-product POST would exceed the plan cap        |
| `plan_max_storefronts_reached` | 402     | `src/services/catalog/createMenuCore.ts` — would exceed the per-plan storefront cap                |
| `products_over_limit`          | 207/402 | `src/api/v1/storefronts.create.ts` (manifest with too many products) — 207 partial OR 402 over cap |

### Rate limit (429)

| `code`                      | HTTP | Source                                                              |
| --------------------------- | ---- | ------------------------------------------------------------------- |
| `rate_limit_exceeded`       | 429  | `src/api/middleware/rateLimit.ts` — `rpm_exceeded` / `rpd_exceeded` |
| `too_many_attempts`         | 429  | `src/api/v1/users.verify.ts` — 3 wrong codes                        |
| `bootstrap_ip_rate_limited` | 429  | `src/api/services/bootstrap.service.ts`                             |
| `bootstrap_quota_exhausted` | 429  | `src/api/services/bootstrap.service.ts`                             |
| `resend_hour_limit`         | 429  | `src/api/v1/users.resendVerification.ts`                            |
| `resend_day_limit`          | 429  | `src/api/v1/users.resendVerification.ts`                            |

### ToS (451)

| `code`         | HTTP | Source                                                                                 |
| -------------- | ---- | -------------------------------------------------------------------------------------- |
| `tos_required` | 451  | Reserved on `storefronts.publish` — see [ToS jurisdiction](/concepts/tos-jurisdiction) |

### Service / internal (500 / 503)

| `code`                    | HTTP | Source                                                       |
| ------------------------- | ---- | ------------------------------------------------------------ |
| `api_disabled`            | 503  | `src/api/middleware/featureFlag.ts` — kill switch            |
| `verify_unexpected_state` | 500  | `src/api/v1/users.verify.ts` — defense-in-depth fall-through |
| `internal_error`          | 500  | Unhandled exception path                                     |

## What NOT to do

* **Do not branch on `error.message`** — it's localized via `Accept-Language` and may change. Branch on `error.type` (enum) and `error.code` (string).
* **Do not swallow `nextActions[]`.** The most common agent-failure mode is silently retrying past a `402` (paywall) or `451` (ToS) without ever showing the user the action.
* **Do not auto-accept the ToS** on the user's behalf. The 451 → modal flow requires user input by design.
* **Do not retry `invalid_request`** with the same body. Fix the body first.
* **Do not branch on HTTP status alone.** Several types share a status (409 for two conflict shapes; 404 for both real-missing and cross-tenant). The `type` + `code` pair disambiguates.

## Verification in code

* `src/api/contracts/error.zod.ts` — the locked envelope schema (10 type values).
* `src/api/api-error.ts` — the `ApiError` class that wires status + type + code + recovery hints.
* `src/api/middleware/errors.ts` — the global error handler that emits the envelope.
