# Error reference

Every error response uses the same shape:

```json
{"error": {"code": "invalid_mode", "message": "..."}}
```

The HTTP status maps from the `code`. Match on `code`, not on `message`.

## HTTP status codes

### 400 Bad Request

Request body failed validation before vid2md touched any resource.

| Code | Cause |
|------|-------|
| `invalid_json` | Body was not parseable JSON |
| `invalid_email` | Email did not match `local@domain.tld` |
| `invalid_video_url` | Not HTTPS, or pointed at a private/loopback host |
| `invalid_upload_type` | Content-Type was not `video/mp4`, `video/quicktime`, or `video/webm` |
| `invalid_mode` | Mode was not one of `concise`, `regular`, `detailed`, `ultra-detailed` |
| `invalid_model` | Requested model differs from the configured default |
| `invalid_credit_package` | Package was not `starter`, `growth`, or `scale` |
| `invalid_checkout_kind` | Kind was not `credits` or `archive` |
| `invalid_api_key_name` | Name was empty after trim |
| `invalid_markdown` | `PUT result` body was empty |
| `markdown_too_large` | `PUT result` body exceeded 512 KiB |
| `invalid_webhook` | Polar signature failed |
| `resend_failed` | Login code email did not send |

### 401 Unauthorized

| Code | Cause |
|------|-------|
| `missing_api_key` | No `Authorization: Bearer v2m_...` header |
| `invalid_api_key` | Key not found or revoked |
| `missing_session` | No session token sent |
| `invalid_session` | Session expired or user gone |
| `invalid_auth_code` | Login code wrong, used, or expired (10 minute window) |

### 402 Payment Required

| Code | Cause |
|------|-------|
| `insufficient_credits` | Ledger balance below the reservation. Buy [credits](/docs/api) |
| `demo_used` | Free demo minute already spent on this account |
| `archive_required` | Result aged past 24 hours without an archive subscription |

### 404 Not Found

`job_not_found`, `result_not_found`, `api_key_not_found`, `upload_not_found`, `doc_not_found`. The resource is gone or belongs to another account.

### 413 Payload Too Large

`upload_too_large`. Uploads cap at 100 MB. Host the file on your own HTTPS origin and pass it as `video_url`.

### 429 Too Many Requests

`rate_limited`. Honor the `Retry-After` header and the `retry_after_seconds` field in the response body.

### 500 Internal Server Error

`processor_failed` for transient processor errors. `polar_product_missing` when the checkout product is misconfigured server-side. Retry processor failures after a delay; open a ticket for repeats.

## Terminal job states

`GET /v1/jobs/{job_id}` returns one of three terminal states once a job stops moving:

- `succeeded`: Markdown is in R2. Download with `GET /v1/jobs/{job_id}/result`. Charged credits equal the reservation.
- `failed`: see `error_code` and `error_message` on the job. Reserved credits are not auto-refunded for processor failures. Reopen a ticket with the `job_id` if the failure was on our side.
- `cancelled`: job stopped before completion. No result.

Retry guidance: rebuild the request with the same `video_url` and `mode`. Do not retry `content_policy_violation` or `invalid_video_url`. Retry `processor_failed` once after a 30 second backoff.

### `content_policy_violation`

The processor moderates extracted keyframes before generation. Jobs fail with `content_policy_violation` when keyframes show pornographic content, sexualized minors, child exploitation, graphic sexual violence, illegal drug manufacturing, weapon construction instructions, active violent crime, or other illegal material. The job lands in `failed` with `error_code = "content_policy_violation"`. Do not retry. Source videos are deleted on failure same as on success.

## Rate limits

Counted per the listed key over a fixed window. Hitting the cap returns `429 rate_limited` with `retry_after_seconds`.

| Scope | Key | Limit | Window |
|-------|-----|-------|--------|
| `account` | IP + email | 5 | 1 hour |
| `auth_start` | IP + email | 5 | 1 hour |
| `api_key` | user | 10 | 1 hour |
| `checkout` | user | 10 | 10 minutes |
| `upload` | user | 10 | 1 hour |
| `demo_upload` | IP | 5 | 1 hour |
| `job` | user | 20 | 1 minute |
| `demo` | IP | 10 | 1 hour |

See the [quickstart](/docs/quickstart) for the canonical poll loop and the [API reference](/docs/api) for full endpoint signatures.
