# Error Code Catalog (Canonical)

## Purpose
Define the canonical set of machine-readable `code` values returned in `ErrorResponse.code`
across all API endpoints. Every error response must include one of these codes so that clients,
SDKs, and monitoring systems can react programmatically without parsing `message` strings.

## ErrorResponse Envelope
```json
{
  "code": "allocation_not_found",
  "message": "The requested allocation does not exist or is not accessible.",
  "correlation_id": "01HXYZ...",
  "details": {}
}
```
- `code` — machine-readable, one of the values below (stable across versions)
- `message` — human-readable, English, may change without notice
- `correlation_id` — always present; use for support and trace lookup
- `details` — optional structured context (e.g. field validation errors)

---

## Code Catalog

### Authentication — 401

| Code | When returned |
|---|---|
| `token_missing` | No `Authorization` header present on a protected endpoint |
| `token_invalid` | JWT signature or format is invalid |
| `token_expired` | JWT has passed its `exp` claim |
| `token_scope_invalid` | Token lacks the required scope (e.g. terminal token presented outside its allocation) |
| `auth_personal_disabled` | Personal auth flow is disabled by feature flag in the current environment |
| `auth_enterprise_required` | Request used personal auth path but input must use enterprise SSO flow |
| `auth_personal_required` | Request used enterprise auth path but input must use personal auth flow |

### Authorization — 403

| Code | When returned |
|---|---|
| `insufficient_permissions` | Authenticated user lacks the required role or policy permission |
| `admin_required` | Endpoint requires the `admin` role |
| `ownership_required` | Resource belongs to a different user and caller is not an admin |
| `step_up_required` | Sensitive operation requires a fresh MFA step-up grant before mutation |

### Validation — 400

| Code | When returned |
|---|---|
| `validation_error` | One or more request fields failed schema or constraint validation; `details` contains per-field errors |
| `invalid_request` | Request body is malformed or unparseable (e.g. not valid JSON) |

### Allocation — 404 / 409 / 422

| Code | HTTP | When returned |
|---|---|---|
| `allocation_not_found` | 404 | Allocation UUID does not exist or is not visible to the caller |
| `allocation_not_active` | 409 | Requested action (terminal token, SSH key download) requires `active` allocation state |
| `allocation_already_releasing` | 409 | Release attempted on an allocation already in `releasing` or `released` state |
| `allocation_concurrency_limit` | 409 | User has reached the configured maximum concurrent allocation count |
| `insufficient_balance` | 409 | User balance is too low to cover the requested provision |
| `sku_unavailable` | 409 | No available nodes match the requested SKU |

### Node — 404 / 409

| Code | HTTP | When returned |
|---|---|---|
| `node_not_found` | 404 | Node UUID does not exist |
| `node_offline` | 409 | Node was unreachable at provisioning or probe time |
| `node_in_use` | 409 | Node is currently assigned to an allocation; deletion or reassignment blocked |
| `node_already_exists` | 409 | A node with the same host already exists |

### User — 404 / 409

| Code | HTTP | When returned |
|---|---|---|
| `user_not_found` | 404 | User UUID does not exist |
| `user_already_exists` | 409 | A user with the same email or username already exists |

### Billing and Payments — 400 / 409

| Code | HTTP | When returned |
|---|---|---|
| `stripe_signature_invalid` | 400 | Webhook raw-body signature verification failed |
| `refund_window_exceeded` | 409 | Refund request is outside the configured refund eligibility window |

### Storage — 404 / 409

| Code | HTTP | When returned |
|---|---|---|
| `storage_object_not_found` | 404 | Storage path does not exist |
| `storage_path_traversal` | 400 | Path contains traversal sequences (`..`) that escape the user root |
| `storage_already_exists` | 409 | Target path already exists (mkdir, rename destination) |
| `storage_quota_exceeded` | 409 | Upload would exceed the user's storage quota |

### Catalog — 404

| Code | HTTP | When returned |
|---|---|---|
| `sku_not_found` | 404 | SKU identifier does not exist |
| `provider_resource_not_found` | 404 | Provider resource lifecycle record does not exist |

### Apps — 404 / 409

| Code | HTTP | When returned |
|---|---|---|
| `app_not_found` | 404 | App catalog slug does not exist or is disabled for caller visibility |
| `app_version_not_found` | 404 | Requested app version does not exist for the app |
| `app_not_entitled` | 409 | Project is not entitled to create/use the requested app |
| `app_instance_not_found` | 404 | App instance UUID does not exist or is not visible to caller |
| `shared_runtime_not_found` | 404 | Tenant-owned shared runtime UUID does not exist or is not visible to caller |
| `shared_runtime_attachment_not_found` | 404 | Shared runtime attachment UUID does not exist or is not visible to caller |
| `shared_runtime_worker_not_found` | 404 | Shared runtime worker UUID does not exist or is not visible to caller |
| `shared_runtime_worker_operation_not_found` | 404 | Shared runtime worker operation UUID does not exist or is not visible to caller |
| `app_instance_state_invalid` | 409 | Requested operation is not valid for current app instance lifecycle state |
| `app_instance_quota_exceeded` | 409 | Project/tenant app instance quota or policy limit exceeded |
| `app_artifact_not_found` | 404 | App artifact UUID does not exist or is not visible to caller |
| `app_artifact_already_exists` | 409 | An artifact with the same project and digest is already registered |
| `app_artifact_state_invalid` | 409 | Requested operation is not valid for the current app artifact lifecycle state |
| `os_image_not_found` | 404 | OS image catalog entry UUID does not exist |
| `os_image_already_exists` | 409 | OS image catalog entry with the same slug already exists |

### Rate Limiting — 429

| Code | HTTP | When returned |
|---|---|---|
| `rate_limit_exceeded` | 429 | Caller has exceeded the configured rate limit; see `Retry-After` header |

### Server — 500 / 502 / 503

| Code | HTTP | When returned |
|---|---|---|
| `internal_error` | 500 | Unexpected server-side failure; always include `correlation_id` in support requests |
| `upstream_error` | 502 | A downstream dependency (Stripe, GPU node SSH, Temporal) returned an unexpected failure |
| `service_unavailable` | 503 | Service temporarily unavailable; safe to retry after back-off |

---

## Rules

1. **Stable codes**: once shipped, a `code` value must not be renamed or removed — it is a breaking change.
2. **Additive only**: new codes may be added in any version without a major bump.
3. **`details` for validation**: `validation_error` responses must populate `details` with at minimum `{ "fields": [{ "field": "...", "issue": "..." }] }`.
4. **No free-form codes**: implementors must not invent codes outside this catalog. Submit a PR to extend it.
5. **HTTP status is the primary signal**: `code` refines within a status — never use `code` as a substitute for the correct HTTP status.
