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

# Error Codes

> Reference for errors returned by the Lumina API, the SDK, and the on-chain contracts. Includes retry strategy + webhook retry semantics.

When something goes wrong, the API returns a JSON envelope:

```json theme={null}
{ "error": "machine_code", "message": "human-readable explanation", "code": "machine_code" }
```

The SDK maps non-2xx HTTP responses into `LuminaError` with `.status`,
`.code`, `.message`. Use the `code` field for branching; the `message`
is for logs / UI surfacing.

## HTTP envelope

| Code | Meaning                                                      | Typical action                                            |
| ---- | ------------------------------------------------------------ | --------------------------------------------------------- |
| 400  | Invalid request shape                                        | Check the request body against the OpenAPI schema         |
| 401  | Missing / invalid API key                                    | Verify `x-api-key` header is present and valid (`lk_...`) |
| 403  | Forbidden — rate limit, sandbox cap, or 2FA required         | Reduce request rate; check `x-ratelimit-*` headers        |
| 404  | Resource not found                                           | Verify the `productId` / `policyId` / `epochId` exists    |
| 409  | Idempotency conflict                                         | Use a fresh `Idempotency-Key` for new requests            |
| 422  | Validation error                                             | Check `coverageAmount` is within product limits           |
| 429  | Rate limit                                                   | Honour the `Retry-After` header                           |
| 500  | Server error                                                 | Retry with exponential backoff                            |
| 503  | Service unavailable (RPC down, oracle stale, sequencer down) | Retry after 30s                                           |

## Application-level codes

The `code` field on the JSON envelope is one of:

| code                          | HTTP | Cause                                             | Resolution                                                          |
| ----------------------------- | ---- | ------------------------------------------------- | ------------------------------------------------------------------- |
| `invalid_product`             | 404  | `productId` not in catalogue                      | Use `GET /products` to list valid IDs                               |
| `product_inactive`            | 422  | `productActive[pid] === false` on PolicyManagerV2 | Wait for re-activation or choose another product                    |
| `coverage_below_minimum`      | 422  | `coverageAmount < 100e6` (`$100`)                 | Minimum is `100000000` base units USDC                              |
| `coverage_above_maximum`      | 422  | Coverage exceeds BondVault available capacity     | Reduce coverage or wait for capacity                                |
| `sandbox_disabled`            | 503  | `SANDBOX_WALLET` not configured on the API server | Use full onboarding path                                            |
| `sandbox_cap_exceeded`        | 403  | \$100 sandbox cap exhausted for the caller IP     | Wait for the hourly window to reset (10/h/IP) or onboard            |
| `rate_limit`                  | 429  | Caller exceeded the per-key RPM allowance         | Slow down; honour `Retry-After`                                     |
| `idempotency_conflict`        | 409  | Same `Idempotency-Key` with a different body      | Generate a fresh UUIDv4 for genuinely new requests                  |
| `health_contracts_incomplete` | 500  | `/health.contracts` missing a required key        | Server-side; retry or report                                        |
| `chain_not_found`             | 503  | RPC provider returned no result                   | Transient — retry with backoff                                      |
| `not_authorized_relayer`      | 403  | Caller is not in the relayer allowlist            | Direct callers should use `purchasePolicy`, not `purchasePolicyFor` |

## On-chain reverts (surfaced verbatim from the contracts)

These come back inside the `message` field when a transaction reverts.
The SDK preserves the raw revert reason; below is the catalogue of
strings you may see and what they mean.

### CoverRouterV2

| Reason                            | Cause                                                                | Resolution                                          |
| --------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------- |
| `ContractPaused`                  | `CoverRouterV2.paused == true` OR `GlobalPauseRegistry.isPaused()`   | Wait for unpause                                    |
| `SEQUENCER_DOWN`                  | Base L2 sequencer uptime feed reports down or within 1h grace period | Wait for sequencer recovery + grace                 |
| `ProductNotConfigured(productId)` | Product not registered in CoverRouter                                | Use a `productId` from `GET /products`              |
| `ProductInactive(productId)`      | `products[pid].active == false`                                      | Choose a different product                          |
| `InvalidCoverage(amount)`         | `coverageAmount < 100_000_000` (less than \$100 in 6-dec USDC)       | Min cover is \$100 base units                       |
| `NotAuthorizedRelayer(addr)`      | Caller is not in the relayer allowlist                               | Direct path `purchasePolicy` doesn't need allowlist |

### BaseFlashShield / FlashBTC/ETHShield

| Reason              | Cause                                                              | Resolution                                                     |
| ------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- |
| `NOT_ROUTER`        | `msg.sender` is not the FlashShieldAdapter wired into this shield  | Always go through the adapter (PolicyManagerV2 routes for you) |
| `SEQUENCER_DOWN`    | Base L2 sequencer down OR in grace period                          | Wait + retry                                                   |
| `ORACLE_INVALID`    | Chainlink `answer <= 0`                                            | Transient oracle outage; retry                                 |
| `ORACLE_STALE`      | Chainlink `block.timestamp - updatedAt > MAX_PRICE_STALENESS` (1h) | Wait for next oracle update                                    |
| `POLICY_EXISTS`     | Adapter tried to create a policy with an already-used id           | Bug — open an issue                                            |
| `INVALID_WINDOW`    | `expiresAt <= startTimestamp`                                      | Bug — open an issue                                            |
| `WINDOW_MISMATCH`   | Adapter passed `duration != shield._window()`                      | Bug — open an issue                                            |
| `POLICY_NOT_FOUND`  | `verifyAndCalculate` called for an id that doesn't exist           | Bug or stale `policyId`                                        |
| `ALREADY_FINALIZED` | Trigger was already evaluated for this policy                      | A policy can only fire (or not) once                           |
| `WINDOW_EXPIRED`    | `block.timestamp > policy.expiresAt` on trigger attempt            | Policy expired without firing — premium stays burned           |

### BondVault

| Reason                 | Cause                                             | Resolution                                        |
| ---------------------- | ------------------------------------------------- | ------------------------------------------------- |
| `Exceeds capacity`     | `totalCommittedUSD + payoutUSD > vault max`       | Capacity full; wait or scale down coverage        |
| `Only PolicyManager`   | Caller is not PolicyManagerV2                     | Always go through PolicyManager                   |
| `Zero payout`          | Trigger tried to issue a `$0` bond                | Bug — check coverage rounding                     |
| `Not matured`          | `redeemBond()` called before maturity (730 days)  | Wait until `block.timestamp >= maturityTimestamp` |
| `Insufficient bonds`   | Caller doesn't hold enough ERC-1155 of that epoch | Check `claimBond.balanceOf(caller, epochId)`      |
| `Price too low`        | LUMINA price \< `MIN_REDEEM_PRICE` (\$0.001)      | Wait for price recovery — circuit breaker         |
| `Insufficient reserve` | Vault LUMINA balance \< computed payout           | Wait — should be transient                        |

When a redemption would exceed the **1.08% per-epoch throttle**, the
bond is BURNED IMMEDIATELY and added to the FIFO queue for the next
epoch (no revert). The user gets a `BondQueued` event back; LUMINA
pays out when `processQueue()` is called against the target epoch.
See [BondVault throttle](/concepts/bondvault-throttle) for the full
queue lifecycle.

### LuminaBondMarketplace

| Reason                     | Cause                                                   | Resolution                              |
| -------------------------- | ------------------------------------------------------- | --------------------------------------- |
| `InsufficientListingValue` | Listing total below the anti-spam floor (audit fix M-3) | Increase `totalPriceUsdc` or `amount`   |
| `NotMatured`               | Listing duration outside allowed range                  | Adjust the listing window               |
| `NotBondOwner`             | Caller doesn't own the bond being listed                | Verify ownership via ClaimBond ERC-1155 |
| `ListingNotFound`          | Buying a listingId that's already filled or cancelled   | Refresh the listing book                |

### LuminaTokenV2

| Reason                       | Cause                              | Resolution                          |
| ---------------------------- | ---------------------------------- | ----------------------------------- |
| `ERC20InsufficientBalance`   | Buyer USDC balance \< premium      | Top up                              |
| `ERC20InsufficientAllowance` | Approval to CoverRouter \< premium | Approve at least the quoted premium |

## Retry strategy

For **transient** errors (HTTP 5xx, `ORACLE_STALE`, `SEQUENCER_DOWN`,
`chain_not_found`):

* Initial retry after 1s
* Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s
* Max 6 retries (\~63s total wall clock)
* Honour `Retry-After` headers when present

For **permanent** errors (HTTP 4xx except 429, on-chain reverts other
than ORACLE\_STALE / SEQUENCER\_DOWN):

* Do not retry
* Surface the `code` + `message` to your user with a clear next step

## Webhook delivery

When the API can't deliver a webhook (3xx / 4xx / 5xx / timeout):

* Retry schedule: **1m, 5m, 30m, 2h, 12h, 24h** (6 attempts)
* After the 6 attempts, the webhook is marked `undeliverable`
* Re-enable via `POST /api/v1/webhooks/{id}/retry`
* Each delivery carries an HMAC signature in `X-Lumina-Signature` —
  reject any payload that fails verification
