# Errors & Rate Limits

Every error family on the platform, what causes it, and how to get unstuck.

## Response envelopes by API family

| Family | Error shape |
|--------|-------------|
| Business REST (support tickets, secret manager, auth verify) | `{ "statusCode": 401, "error": "Unauthorized", "message": "…", "correlationId": "req-…", "timestamp": "…" }` (+ `details` on validation errors) |
| BugWatch ingest / symbols / deploy | `{ "error": "…" }` with the HTTP status carrying the semantics |
| OTLP (`/v1/logs` · `/v1/traces` · `/v1/metrics`) | `{ "partialSuccess": { "rejectedLogRecords": 0, "errorMessage": "…" } }` with the mapped status |
| Prometheus query | `{ "status": "error", "errorType": "…", "error": "…" }`, where `errorType` is `unauthorized` or `execution` |
| Jaeger | `{ "data": null, "errors": [{ "msg": "…" }] }` |
| MCP | JSON-RPC error object, e.g. `{ "jsonrpc": "2.0", "error": { "code": -32001, "message": "…" }, "id": null }` |

Every REST response echoes `X-Correlation-ID` (send your own `x-correlation-id` header to propagate yours). **Quote it when contacting support**: it is the fastest path to your exact request in the logs.

## Error catalogue

| Status | Message you'll see | Cause | Fix |
|--------|--------------------|-------|-----|
| 401 | `Missing API key.` | No `x-api-key` header | Send `x-api-key: KEYID:secret` |
| 401 | `Invalid API key format. Expected: <keyId>:<secretKey>` | Key ID sent without the secret to a server endpoint | Use the full `keyId:secret` string (shown once at creation) |
| 401 | `Invalid or inactive API key` | Unknown key ID, revoked, or expired | Check Dashboard → API Keys; create/rotate if needed |
| 401 | `Invalid API key` | Key ID exists but the secret is wrong | Re-copy the secret; rotating invalidates old secrets immediately |
| 403 | `Business account is not active` | Org suspended | Resolve account status in the dashboard |
| 403 | `API key does not have access to BugWatch` | Key has no bugwatch product scope | Recreate/edit the key with the right product |
| 403 | `API key lacks the … permission` | Key is missing a granular scope (`ingest:write`, `deploy:write`, `symbols:*`) | Add the scope, or use the project's DSN key |
| 403 | `origin_not_allowed` | Browser ingest `Origin` not in the project's allowed origins | Add your site origin at Dashboard → project → Settings (empty list = unrestricted) |
| 400 | `use the DSN key from Settings > DSN keys` | Key has zero or multiple BugWatch resource scopes | Ingest requires a per-project DSN key, not an org-wide key |
| 404 | `BugWatch project not found` | Project deleted or key's resource scope stale | Recreate a DSN key from a live project |
| 404 | `deployment not found` / `project not found` | Wrong `deployId` or another project's key | Ids are per-project; use the id returned by *Start deployment* |
| 409 | (symbols `complete`) | The file was never PUT to `uploadUrl` | PUT the bytes, then call complete |
| 429 | `rate_limited` / `Too Many Requests` | See limits below | Honour `Retry-After`; batch events instead of sending singly |
| 413 | Payload too large | Body over the per-route cap | Ingest ≤ 2 MB, OTLP ≤ 4 MB, RUM ≤ 64 KB; split batches |

## Rate limits

| Surface | Limit | Keyed by | On 429 |
|---------|-------|----------|--------|
| REST API (business endpoints) | 100 requests / 60 s | organization | `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, `X-RateLimit-Reset` headers |
| OTLP / Prometheus / Jaeger / RUM | 600,000 / min | BugWatch project | protocol-shaped 429 body, no headers |
| Mobile ingest | 12,000 / 60 s | BugWatch project | `{ "error": "rate_limited" }` |

Successful business-REST responses carry `X-RateLimit-Limit` only, so don't parse for a remaining count on 2xx.

## Troubleshooting quick paths

- **Works in curl, fails in the browser** → browsers must use the session flow (`x-bugwatch-session`), never `x-api-key`. The API's CORS policy does not accept `x-api-key` from browsers by design.
- **Browser ingest suddenly 403s** → session tokens expire after ~1 h. Re-fetch from your mint endpoint on 403 and retry once.
- **Mobile events silently missing** → device clock skew: tokens are rejected if `iat` is > 60 s in the future or past expiry (5 min). The SDKs re-sign per batch, so persistent loss usually means a rotated `mobileAppSecret`, so re-issue credentials to the app.
- **Events accepted (202) but not visible** → check the key's BugWatch environment (test-mode keys write to the project's non-production env), then `eventId` dedup (duplicates within 10 min increment `deduped`, not `ingested`).
- **Stack traces unreadable** → symbols: mappings/source maps match on **release + platform** (must equal the SDK's `release` exactly); Apple/native match by **debug UUID**. Check *Get upload status* for `CHECKSUM_MISMATCH`, `UNRECOGNIZED_ARCHIVE`, `ARCHIVE_PLATFORM_MISMATCH`, then *Reprocess* to fix already-captured crashes.
- **Webhook receiver never fires** → destination must be public HTTPS (private/internal IPs are rejected), and delivery is single-shot with a 5 s timeout, so a cold-starting receiver misses events.
- **Chat widget button never appears** → the widget hides until key auth passes; check the `onAuth` callback. Rejection reasons surface as `MISSING | MALFORMED | UNKNOWN_ENVIRONMENT | KEY_NOT_FOUND | INACTIVE | EXPIRED | SECRET_MISMATCH | ORG_NOT_FOUND`.
- **AI chat replies with a manipulation warning** → the platform scans prompts for injection; malicious content is rejected (and for signed-in dashboard users, repeated attempts block the account and a super-admin must lift it).
