# Error Codes

Every response from `/api/*` is JSON, errors included. The body always has a
`message`; validation errors add a field-level `errors` map.

| Status | Meaning                                                  | Body shape                                             |
| ------ | -------------------------------------------------------- | ------------------------------------------------------ |
| `200`  | OK — report **deduplicated** into an existing card       | `{ "data": { … }, "deduplicated": true }`              |
| `200`  | **Dropped** — accepted and discarded, no card created    | `{ "dropped": true, "reason": "…" }`                   |
| `201`  | Created — a new card                                     | `{ "data": { … }, "deduplicated": false }`             |
| `401`  | Missing/invalid key, bad signature, or expired timestamp | `{ "message": "Invalid or missing project API key." }` |
| `403`  | Publishable key used from a disallowed origin            | `{ "message": "Origin not allowed." }`                 |
| `422`  | Validation failed (or the project has no board column)   | `{ "message": "…", "errors": { "field": ["…"] } }`     |
| `429`  | Per-minute burst limit exceeded                          | `{ "message": "…" }` + `Retry-After`                   |
| `5xx`  | Server error                                             | `{ "message": "…" }`                                   |

> **A `200` is not always a stored card.** A report can be **accepted and
> dropped** — the server answers `200` with `{ "dropped": true, "reason": "…" }`
> and **no `data`**. This is deliberate: an SDK watching your app must never
> surface our billing or project state as a failure _inside_ your app. A drop is
> **never retryable**.
>
> `quota_exceeded: true` rides along on every drop as a **legacy alias** of
> `dropped`, so SDK versions released before `reason` existed still know not to
> retry. Prefer `dropped` and `reason` in new code.
>
> **The card is hidden by default.** The SDKs send `X-Bb-Hide-Response`, which drops `data` from
> the `200`/`201` body — you get a bare `{ "deduplicated": true }` or `{ "deduplicated": false }`.
> The status codes and the outcome flags (`deduplicated`, `dropped`, `reason`) are unchanged; only
> the card itself is withheld, so a report you encrypted isn't handed back in plaintext. See the
> **[API Reference](/docs/api-reference)**.

## Why a report was dropped

`reason` tells you which of three very different things happened — and each has a
different fix.

| `reason`   | What it means                                                                                 | What to do                                                                                                                                             |
| ---------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `quota`    | Your **account's** event allowance for the day is spent. It is shared across every project you own. | Not retryable. It resets at midnight UTC, or upgrade for a bigger allowance. You are emailed at 75%, 90% and 100% before it runs out.                  |
| `paused`   | The project is **paused** — read-only, and accepting no events. It still has all of its data. | Not retryable. **Resume the project** from your projects page. If it was paused because you're over your plan's project limit, free a slot or upgrade. |
| `archived` | The project is **archived** — permanently read-only.                                          | Not retryable. Archiving is one-way; point your SDK at a live project.                                                                                 |

Only an **active** project accepts events. Pausing or archiving a project stops
ingestion for it immediately, and that applies to the API exactly as it does to
the dashboard.

## What to do with each

- **`401` / `403` / `422`** are **configuration or payload bugs** — fix the
  request. They are **not** retryable; retrying sends the same broken request.
- **`429`** is **retryable**: back off and honour the `Retry-After` header (see
  **[Rate Limits & Quotas](/docs/rate-limits)**).
- **`5xx` and network errors** are **retryable** with exponential backoff.
- **A drop (`dropped: true`)** is **never** retryable, whatever the reason.
  Retrying a paused project just burns requests against your per-minute limit.

> **Never crash the app you're monitoring.** When you call the API directly,
> wrap reporting in a `try/catch` and swallow failures. The SDKs do this for you —
> reporting is fire-and-forget and errors surface only on the SDK's debug channel.

## SDK exception mapping

Every SDK maps these statuses to typed exceptions so you can handle them
consistently:

| HTTP          | JavaScript                | PHP                   | Carries                          |
| ------------- | ------------------------- | --------------------- | -------------------------------- |
| 401, 403      | `BugBoardAuthError`       | `AuthException`       | message                          |
| 422           | `BugBoardValidationError` | `ValidationException` | `fieldErrors` (the `errors` map) |
| 429           | `BugBoardRateLimitError`  | `RateLimitException`  | `retryAfter` (seconds)           |
| 5xx / network | `BugBoardServerError`     | `ServerException`     | message, underlying cause        |

> A **drop maps to no exception** — it is a `200`, and the report simply went
> nowhere. Nothing is thrown, nothing is retried. Read `dropped` / `reason` off
> the body if you want to log why.
>
> Treat `401`/`403`/`422` as non-retryable and retry only `429`, `5xx`, and
> network errors. `Retry-After` is honoured verbatim in seconds and takes
> precedence over the SDK's own backoff; a non-numeric value is ignored.

## Common 422 causes

- **Missing `severity`** — it's the one required field.
- **Invalid enum** — `severity` or `priority` outside its allowed set (see the
  **[API Reference](/docs/api-reference)**). `description_type` and `status` are
  decided by the server and **ignored** if sent, so they never cause a `422`.
- **Oversized field** — `title` > 255, a tag > 50, or
  `description` > 65535 characters.
