# Public error codes (`/v1`)

All public JSON errors use the envelope:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed.",
    "details": {},
    "request_id": "req_…"
  }
}
```

`error.code` is stable API surface. New codes may be added; existing codes must
not be repurposed. Source of truth in code: `apilyn.api.error_codes.PUBLIC_ERROR_CODES`.

## Common / transport

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `validation_error` | 422 | Request body/query failed schema validation |
| `unauthorized` | 401 | Generic auth failure (fallback) |
| `missing_bearer` | 401 | `Authorization: Bearer …` missing |
| `invalid_token` | 401 | JWT/access token invalid or expired |
| `forbidden` | 403 | Generic authorization failure (fallback) |
| `permission_denied` | 403 | Actor lacks required RBAC permission |
| `user_actor_required` | 403 | Endpoint requires a user-backed actor (not API key alone) |
| `not_found` | 404 | Generic missing resource (fallback) |
| `rate_limited` | 429 | RPM / soft-rate limit |
| `http_error` | * | Unstructured HTTPException fallback |
| `internal_error` | 5xx | Unexpected server failure (message sanitized) |
| `missing_org_header` | 400 | `X-Org-Id` required for JWT actors |
| `invalid_org_header` | 400 | `X-Org-Id` is not a UUID |
| `org_mismatch` | 403 | `X-Org-Id` does not match API key org |
| `org_not_found` | 404 | Organization missing |
| `membership_required` | 403 | Actor is not an active org member |

## API keys / auth

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `invalid_api_key` | 401 | Key format/secret invalid |
| `api_key_revoked` | 401 | Key revoked or expired |
| `environment_mismatch` | 401 | Live/sandbox mismatch |
| `insufficient_scope` | 403 | API key lacks required scope |
| `unknown_scopes` / `scopes_required` | 403 | Scope validation on create/rotate |
| `api_key_not_found` | 404 | Key id unknown in org |

## Jobs / pipelines

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `invalid_idempotency_key` | 400 | Missing/invalid `Idempotency-Key` |
| `idempotency_conflict` | 409 | Same key, different payload fingerprint |
| `idempotency_race` | 409 | Concurrent create race |
| `pipeline_not_found` | 404 | Unknown pipeline name |
| `pipeline_version_mismatch` | 400 | Unsupported pipeline version |
| `invalid_inputs` / `input_not_found` / `input_not_ready` / `input_expired` | 400/404/409 | Upload inputs invalid |
| `job_not_found` | 404 | Unknown job id |
| `invalid_state` | 409 | Illegal state transition (e.g. cancel) |
| `hard_limit` / `concurrent_limit` / `quota_exceeded` | 429 | Usage caps |
| `output_not_found` / `output_not_ready` / `retention_expired` | 404/409/403 | Artifact download |

## Storage / intake

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `upload_not_found` / `upload_missing` / `upload_gone` | 404/410 | Upload lifecycle |
| `payload_too_large` / `size_limit_exceeded` | 413 | Size caps |
| `invalid_file_type` / `malformed_document` / `decompression_bomb` | 400 | Intake rejection |
| `malware_detected` | 422 | Malware scanner quarantine |
| `scanner_unavailable` | 503 | Fail-closed scanner unavailable |
| `retention_expired` | 403 | Outside retention window |

## Sync tools

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `tool_not_found` | 404 | Unknown tool name |
| `invalid_parameters` | 400 | Tool parameter validation |
| `ssrf_blocked` | 400 | URL/tool SSRF policy |
| `capability_unavailable` | 400 | Optional runtime/binary not provisioned |
| `tool_timeout` / `tool_failed` | 408/500 | Execution failure |

## Webhooks

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `blocked_hostname` / `blocked_ip` | 422 | SSRF on endpoint URL |
| `invalid_events` | 400 | Subscribed events invalid |
| `webhook_not_found` / `delivery_not_found` | 404 | Missing resources |

## Accounts / billing

| Code | Typical HTTP | Meaning |
| --- | ---: | --- |
| `plan_not_found` / `org_slug_taken` / `membership_exists` | 404/409 | Org/membership admission |
| `email_unverified` | 403 | Identity email not verified |
| `invalid_stripe_signature` / `portal_origin_denied` | 400 | Billing webhook/portal |

## Client guidance

1. Branch on HTTP status for coarse handling; use `error.code` for precise UX.
2. Always log/correlate `request_id`.
3. Treat unknown codes as `http_error`-class failures without crashing.
4. Never assume `message` is stable English copy — codes are the contract.
