## Envelope

Every endpoint shares one response envelope:

| Case | HTTP | Body |
|---|---|---|
| Sync success | 200 | `{ "success": true, "data": …, "credit_cost": 1 }` |
| Async accepted | 202 | `{ "success": true, "data": { "job_id": "…", "status_url": "…" } }` |
| Error | 4xx/5xx | `{ "success": false, "error": { "code": "…", "message": "…" } }` |

## Error codes

| Code | HTTP | Meaning / what to do |
|---|---|---|
| `INVALID_REQUEST` | 400 | Schema/validation failure — `message` names the field. |
| `UNAUTHORIZED` | 401 | Missing, malformed, or revoked key. |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits — call was **not** billed. Top up or reduce `limit`. |
| `FORBIDDEN` | 403 | e.g. playground key used externally, protected resource. |
| `JOB_NOT_FOUND` / `NOT_FOUND` | 404 | Job/monitor/key not found (or belongs to another team). |
| `TARGET_TIMEOUT` | 408 | Target page didn't render in time — **not** billed; retry with larger `timeout`/`wait_for`. |
| `RATE_LIMITED` | 429 | Per-plan rate cap — honor `Retry-After`, back off. |
| `TARGET_BLOCKED` | 403 | Target refused the request (bot protection) — we don't bypass it. |
| `TARGET_UNREACHABLE` | 502 | DNS/TLS/unreachable target. |
| `PROVIDER_ERROR` | 502 | Upstream capture provider failed — **not** billed; retry with backoff (honor `Retry-After` when present). |
| `INTERNAL_ERROR` | 500 | Our bug — **not** billed; retry with backoff. |



> **Note:** Whether a call was billed is always visible via `credit_cost` in the envelope and in
`GET /v1/account/activity`.