# Error codes

Error body shapes the v1 API can return, and the `issue` → HTTP status lookup table.

## Error body shapes

### V1Error {#schema-v1error}

Shared error body for every auth/registry/signing/origin/pool error (SHIELD_KEY_*, SCOPE_DENIED, ORIGIN_NOT_ALLOWED, POOL_MISMATCH, SHIELD_REGISTRY_*, PAYMENT_ENV_MISMATCH, CAPTURED_PROOF_UNAVAILABLE). `message`/`docs` ALWAYS present. Unlike `LegacyControllerError` below — the per-endpoint PayPal/Stripe error shape — which never carries `message`/`docs`.

### LegacyControllerError {#schema-legacycontrollererror}

Per-endpoint PayPal/Stripe error body — the SAME shape the legacy (non-v1) API has always returned for these routes. Only `issue` is guaranteed; `message`/`docs` NEVER present (unlike `V1Error`). Some responses carry no `data` at all — see each response's own description for the exact body.

### LegacyStringError {#schema-legacystringerror}

Bare `{error:string}` body with no `data` at all — unlike `LegacyGateError` (same shape) and `V1Error`/`LegacyControllerError` (which carry `data`). Used for shared config/internal errors: "Shield misconfigured" (Stripe pk/sk key mode mismatch), "Internal server error" (unknown error), "Shield unavailable" (shield has no secret configured on a PayPal route).

### LegacyGateError {#schema-legacygateerror}

Bare body from the Stripe pk-resolution gate and the PayPal hostname gate — `error` here is a STRING, not `true`, and there is no `data`/`issue` at all. Covers Stripe MISSING_PK/UNKNOWN_PK/PK_POOL_MISMATCH and PayPal's unknown-host/no-secret gate; the issue names in each response's description are documentation only, never present in the actual body.

### StripeErrorPassthrough {#schema-stripeerrorpassthrough}

500 body on `/v1/stripe/create-intent` and `/v1/stripe/complete` when the underlying Stripe API call throws. `issue` is OPEN — it passes through Stripe's raw `error.code` (e.g. `card_declined`, `parameter_invalid_integer`, `amount_too_small`) when Stripe sets one, falling back to the literal string `STRIPE_ERROR` only when it doesn't. NOT a member of the closed `V1Issue` enum — do not validate `issue` against that enum for this response.

## Issue code table {#issue-table}

| issue | HTTP status |
|---|---|
| `SHIELD_KEY_INVALID` | 401 |
| `SHIELD_KEY_ENV_MISMATCH` | 401 |
| `SHIELD_KEY_REVOKED` | 401 |
| `SHIELD_KEY_EXPIRED` | 401 |
| `SCOPE_DENIED` | 403 |
| `ORIGIN_NOT_ALLOWED` | 403 |
| `POOL_MISMATCH` | 403 |
| `SHIELD_REGISTRY_UNAVAILABLE` | 503 |
| `SHIELD_REGISTRY_STALE` | 503 |
| `SHIELD_ENV_MISCONFIGURED` | 503 |
| `PAYMENT_ENV_MISMATCH` | 503 |
| `SIGNING_UNAVAILABLE` | 503 |
| `LEGACY_API_DISABLED` | 410 |
| `CAPTURED_PROOF_UNAVAILABLE` | 502 |

`STRIPE_ERROR` and other endpoint-specific issues (validation/business) are not in the shared table above — see each endpoint's response description on its tag page.
