# API Contract V1 Status: capability map and transport contract. Slice 3C, Slice 3D, and the schema-v6 Phase 3E core are implemented, merged, and production-cutover complete; the Phase 3E.3 transactional-outbox runtime correction is not deployed until its later cutover verification. Phase 3F has not started. ## Protocol conventions - Product API paths are `/api/v1/**`. Foundation probes remain `/api/health`, `/api/ready`, and `/api/version`. - JSON contracts are strict Zod schemas exported once from `packages/contracts`; unknown keys fail with the canonical error envelope from `ERROR_MODEL.md`. - UUID resource IDs are opaque. Unauthorized private resources are indistinguishable from missing resources (`404 RESOURCE_NOT_FOUND`). - All unsafe cookie-authenticated requests apply `AUTH_SESSION_CSRF.md`. - First-admin bootstrap is part of the verified Google callback transaction and has no separate HTTP route. Admin role/status mutation enforces the last-active-admin guard; emergency recovery is operator maintenance only. - Important mutations require exactly one `Idempotency-Key` containing 1-128 ASCII characters from `A-Z a-z 0-9 . _ : -`. Missing, empty, multiple, Unicode, whitespace, control, slash, backslash, quote, or oversized values are `400 VALIDATION_ERROR`. The server hashes the key and canonical semantic request; same key/same request replays the stored result and same key/different request is `409 IDEMPOTENCY_CONFLICT`. - Mutable resources expose an integer `version` and ETag. Updates require `If-Match` or `expectedVersion`; stale writes return `409 VERSION_CONFLICT` with current safe version, never silently overwrite. - Lists use an opaque base64url cursor containing versioned canonical sort position, filter fingerprint, and ID tie-breaker. Default/max page sizes are 25/100. Sorting is deterministic; cursor/filter mismatch is `400 INVALID_CURSOR`. - Times are ISO 8601 UTC strings at the HTTP boundary and epoch milliseconds in SQLite. - IDs, not storage keys/paths/provider secrets, cross the API boundary. ### Slice 3C Project projection Owner Project responses expose only `id`, `name`, nullable `description`, `favorite`, `status`, ISO-8601 UTC `createdAt`/`updatedAt`, and positive `version`. They never expose owner columns, retention/purge internals, storage keys, or filesystem paths. Owner routes return `404 RESOURCE_NOT_FOUND` for a different owner, including when the actor is an admin. Slice 3C lists only `active` Projects at API-011 and only `soft_deleted` Projects at API-062. Both use the authenticated opaque cursor contract and the owner list order `favorite DESC, updated_at DESC, id DESC`. Slice 3C does not implement purge scheduling/worker side effects despite the later-state schema foundation. ### Slice 3D Asset projection Owner Asset responses expose only the resource/project IDs, normalized original filename, kind, declared/verified MIME, immutable byte size/checksum, bounded technical metadata, ingestion/lifecycle status, safe failure code, timestamps, and optimistic version. They never expose owner columns, storage/staging keys, filesystem paths, reconciliation state, or capability hashes. Upload responses expose the UploadSession ID, Asset ID, canonical received offset, declared length, lifecycle status, expiry, and recommended chunk size only. ## Common errors The stable code, status, retryability, message key, action, severity, and allowed surface come only from `ERROR_MODEL.md`. Common mappings are `400 VALIDATION_ERROR`/`INVALID_CURSOR`, `401 AUTHENTICATION_REQUIRED`, `403 AUTHORIZATION_DENIED` or an account-status code, `404 RESOURCE_NOT_FOUND`, `409 RESOURCE_STATE_CONFLICT`/`VERSION_CONFLICT`/ `IDEMPOTENCY_CONFLICT`, `413 PAYLOAD_TOO_LARGE`, `415 UNSUPPORTED_MEDIA_TYPE`, `416 RANGE_NOT_SATISFIABLE`, typed 422 domain codes, `429 QUOTA_EXCEEDED`/`RATE_LIMITED`/`PROVIDER_RATE_LIMITED`, typed 502/503 provider or storage codes, and `500 INTERNAL_ERROR`. Every response contains safe `code`, localized `messageKey`, `suggestedAction`, `retryable`, `requestId`, and optional allowlisted field details; never a stack or provider body. ## Capability map Legend: `U` authenticated user/owner, `A` administrator, `P` public/operator probe, `O` OIDC provider callback. `Idem` means `Idempotency-Key` required. `Ver` means optimistic version required. “Audit” names the success/denial class; security denials may be sampled/rate-limited. | ID | Method and path | Actor; request -> response | Errors | Idempotency/concurrency | Audit; transaction/side effect | | ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------ | ------------------------------------------------------------ | | API-001 | `GET /api/health` | P; none -> liveness/build | 500 | none/read-only | metric only; no tx | | API-002 | `GET /api/ready` | P/operator; none -> DB/storage/migration readiness without secrets | 503 | none/read-only | operational log; no write tx | | API-003 | `GET /api/version` | P; none -> build SHA/schema compatibility | 500 | cache-safe/read-only | metric only; no tx | | API-004 | `GET /api/v1/auth/google/start` | browser; safe same-origin return path -> Google redirect | 400,429,503 | one-use OAuth transaction | login-start; short security-state write | | API-005 | `GET /api/v1/auth/google/callback` | O/browser; code/state -> session cookie + validated return path; every failure -> fixed `303 /auth/error` without query disclosure | fixed safe redirect | state/nonce/PKCE one-use | login success/failure; provider denial consumes state; TX-01 | | API-006 | `GET /api/v1/auth/session` | U/pending member; none -> safe user status/session expiry | 401 | read-only | none; session last-seen coalesced | | API-007 | `GET /api/v1/auth/csrf` | U/pending member; none -> synchronizer token | 401 | no-store; per session | none; may rotate token in a guarded session write | | API-008 | `POST /api/v1/auth/logout` | U/pending member; CSRF -> 204 | 401,403 | repeat succeeds | logout; TX-03 | | API-009 | `GET /api/v1/auth/sessions` | U; cursor -> own safe session list | 401,400 | cursor/read-only | none | | API-010 | `POST /api/v1/auth/sessions/revoke-all` | U; CSRF/confirm -> revoked count + current-session policy result | 401,403 | Idem | session revoke-all; TX-03 | | API-011 | `GET /api/v1/projects` | U; cursor/status -> project summaries | 400,401 | cursor/read-only | none | | API-012 | `POST /api/v1/projects` | U; name/description -> project | 400,409 | Idem | project.create; TX-04 | | API-013 | `GET /api/v1/projects/:projectId` | U owner; ID -> project snapshot | 401,404 | ETag/read-only | none | | API-014 | `PATCH /api/v1/projects/:projectId` | U owner; rename/favorite + expected version -> updated project | 400,401,404,409 | Idem + Ver | project.update; TX-05 | | API-015 | `DELETE /api/v1/projects/:projectId` | U owner; expected version/confirm -> soft-deleted project | 401,404,409 | Idem + Ver | project.delete_requested; TX-06, later queues purge | | API-016 | `POST /api/v1/projects/:projectId/restore` | U owner; expected version -> active project when policy permits | 401,404,409 | Idem + Ver | project.restore; TX-06 | | API-017 | `POST /api/v1/projects/:projectId/uploads` | U owner; file metadata/size/checksum -> Asset + upload session | 400,404,409,413,415 | Idem; project Ver guard | upload.create; TX-07 | | API-018 | `HEAD /api/v1/uploads/:uploadId` | U owner; ID -> offset/length/status headers | 401,404,409 | read-only authoritative offset | none | | API-019 | `PATCH /api/v1/uploads/:uploadId` | U owner; offset + binary chunk -> new offset | 400,404,409,413,415 | offset/chunk checksum retry; Ver | upload.append metric; file then TX-08 | | API-020 | `POST /api/v1/uploads/:uploadId/complete` | U owner; optional checksum -> asset/job | 404,409,415,422 | Idem + upload Ver | upload.complete; TX-09 | | API-021 | `DELETE /api/v1/uploads/:uploadId` | U owner; ID -> 204 | 404 | repeat succeeds | upload.abort; TX-10 | | API-022 | `GET /api/v1/projects/:projectId/assets` | U owner; cursor/media type/upload time/ingestion/lifecycle/original-filename search -> assets | 400,404 | cursor/read-only | none | | API-023 | `GET /api/v1/assets/:assetId` | U owner; ID -> metadata/status | 401,404 | ETag/read-only | none | | API-024 | `POST /api/v1/assets/:assetId/retry-ingestion` | U owner; failed active Asset/version -> same Asset + new ingestion attempt | 404,409 | Idem + Ver | asset.ingestion_retry; TX-10-style admission | | API-025 | `DELETE /api/v1/assets/:assetId` | U owner; expected version -> soft-deleted lifecycle | 404,409 | Idem + Ver | asset.delete_requested; later TX-20 schedule | | API-026 | `GET /api/v1/assets/:assetId/content` | U owner; Range/conditional headers -> bytes | 404,416,503 | immutable ETag/read-only | access metric; authorized range stream | | API-027 | `POST /api/v1/assets/:assetId/delivery-capabilities` | U owner; operation -> short opaque capability | 404,409 | Idem; resource state guard | capability.issue; TX-23 | | API-028 | `GET /api/v1/projects/:projectId/compositions` | U owner; cursor/status -> version summaries | 400,404 | cursor/read-only | none | | API-029 | `POST /api/v1/projects/:projectId/compositions` | U owner; CompositionDocumentV1/parent/version -> immutable version | 400,404,409,422 | Idem + project Ver | composition.create; TX-11 | | API-030 | `GET /api/v1/compositions/:compositionVersionId` | U owner; ID -> canonical document/validation | 404 | immutable ETag | none | | API-031 | `POST /api/v1/compositions/:compositionVersionId/derive` | U owner; structured patch/edit intent -> new version | 400,404,409,422 | Idem + expected parent/project Ver | composition.derive; TX-11 | | API-032 | `POST /api/v1/compositions/:compositionVersionId/validate` | U owner; ID -> durable validation result | 404,409,422 | Idem; immutable input | composition.validate; bounded job or deterministic service | | API-033 | `POST /api/v1/projects/:projectId/generation-jobs` | U owner; brief/options/asset IDs -> Job | 400,404,409,422,503 | Idem + project Ver | generation.request; TX-12 | | API-034 | `POST /api/v1/compositions/:compositionVersionId/regenerate` | U owner; structured change brief/options -> Job | 400,404,409,422,503 | Idem; immutable source + project Ver | generation.regenerate; TX-12 | | API-035 | `POST /api/v1/compositions/:compositionVersionId/preview` | U owner; ID -> preview artifact/status | 404,409,422 | Idem; immutable version | preview.request/materialize; TX-12/TX-17 | | API-036 | `GET /api/v1/previews/:previewArtifactId` | U owner; ID -> artifact metadata/player bootstrap | 404,409,503 | immutable ETag/read-only | preview.access; authorized bootstrap | | API-037 | `GET /api/v1/previews/:previewArtifactId/content` | U owner or scoped capability; Range -> immutable artifact bytes | 404,416,503 | ETag/read-only | safe access metric; range stream | | API-038 | `POST /api/v1/compositions/:compositionVersionId/render-jobs` | U owner; output preset -> Job | 400,404,409,422,503 | Idem; immutable composition | render.request; TX-12 | | API-039 | `GET /api/v1/jobs` | U; cursor/type/status/project -> summaries | 400 | cursor/read-only | none | | API-040 | `GET /api/v1/jobs/:jobId` | U owner; ID -> durable snapshot | 404 | ETag/read-only | none | | API-041 | `GET /api/v1/jobs/:jobId/steps` | U owner; cursor -> safe step summaries | 400,404 | cursor/read-only | none | | API-042 | `GET /api/v1/jobs/:jobId/event-history` | U owner; cursor -> durable events | 400,404 | cursor/read-only | none | | API-043 | `GET /api/v1/jobs/:jobId/events` | U owner; Last-Event-ID -> SSE | 400,404,409 | durable replay; no mutation | connection metric; no tx beyond reads | | API-044 | `POST /api/v1/jobs/:jobId/cancel` | U owner; cancellable job/version -> snapshot | 404,409 | Idem + Ver | job.cancel_requested; TX-16 | | API-045 | `GET /api/v1/projects/:projectId/outputs` | U owner; cursor/state -> output summaries | 400,404 | cursor/read-only | none | | API-046 | `GET /api/v1/outputs/:outputId` | U owner; ID -> immutable evidence + lifecycle | 404 | ETag/read-only | none | | API-047 | `GET /api/v1/outputs/:outputId/content` | U owner/capability; Range -> video bytes | 404,416,503 | immutable ETag | output.download/stream metric; no write tx | | API-048 | `POST /api/v1/outputs/:outputId/delivery-capabilities` | U owner; download/stream -> opaque capability | 404,409 | Idem | capability.issue; TX-23 | | API-049 | `DELETE /api/v1/outputs/:outputId` | U owner; expected state version -> purge-scheduled | 404,409 | Idem + Ver | output.delete_requested; TX-20 | | API-050 | `GET /api/v1/settings/profile` | U; none -> safe profile/preferences | 401 | ETag/read-only | none | | API-051 | `PATCH /api/v1/settings/profile` | U; display/preferences/version -> profile | 400,409 | Idem + Ver | user.profile_update; short user tx | | API-052 | `GET /api/v1/settings/provider-status` | U; none -> enabled capability health (no reference names) | 401 | short cache/read-only | none | | API-053 | `GET /api/v1/admin/users` | A; cursor/status/search -> safe users | 400,403 | cursor/read-only | admin.read_users | | API-054 | `PATCH /api/v1/admin/users/:userId` | A; role/status/version -> user; reject last-active-admin lockout | 400,403,404,409 | Idem + Ver | admin.user_update; TX-02 | | API-055 | `GET /api/v1/admin/jobs` | A; cursor/type/status/lease -> redacted operational summaries | 400,403 | cursor/read-only | admin.read_jobs | | API-056 | `POST /api/v1/admin/jobs/:jobId/reconcile` | A; reason/version -> redacted reconciliation intent | 403,404,409 | Idem + Ver | admin.job_reconcile; TX-13/outbox | | API-057 | `GET /api/v1/admin/provider-credentials` | A; cursor -> reference names/status/health | 403 | cursor/read-only | admin.read_provider_refs | | API-058 | `PUT /api/v1/admin/provider-credentials/:referenceId` | A; provider/purpose/env-name/status/version -> reference | 400,403,409 | Idem + Ver | admin.provider_ref_update; never writes HF secret | | API-059 | `GET /api/v1/admin/audit-events` | A; cursor/actor/action/resource/time -> redacted events | 400,403 | cursor/read-only | admin.read_audit | | API-060 | `GET /api/v1/admin/operations` | A; none -> queue/lease/outbox/storage/backup health summary | 403,503 | read-only snapshot | admin.read_operations | | API-062 | `GET /api/v1/trash/projects` | U; cursor -> own soft-deleted/purge-scheduled Projects | 400,401 | cursor/read-only | none | | API-063 | `GET /api/v1/projects/:projectId/compositions/current` | U owner; project ID -> selected immutable version or null | 404 | project ETag/read-only | none | | API-064 | `POST /api/v1/outputs/:outputId/pin` | U owner; state version -> pinned state | 404,409 | Idem + Ver | output.pin; guarded output-state tx | | API-065 | `POST /api/v1/outputs/:outputId/unpin` | U owner; state version -> unpinned state | 404,409 | Idem + Ver | output.unpin; guarded output-state tx | | API-066 | `GET /api/v1/admin/users/pending` | A; cursor -> pending members | 400,403 | cursor/read-only | admin.read_pending_users | | API-067 | `POST /api/v1/admin/provider-credentials/:referenceId/health-check` | A; reference/version -> safe health result/job | 403,404,409,503 | Idem + Ver | admin.provider_health_check; bounded outbox/job intent | | API-068 | `GET /api/v1/admin/quota-policies` | A; cursor/scope -> versioned policies | 400,403 | cursor/read-only | admin.read_quota_policies | | API-069 | `POST /api/v1/admin/quota-policies` | A; scope/policy/effective interval -> new policy version | 400,403,409 | Idem + expected prior Ver | admin.quota_policy_create; quota/audit tx | | API-070 | `POST /api/v1/jobs/:jobId/retry` | U owner; retryable terminal job -> new Job | 404,409,503 | Idem; immutable original request | job.retry; TX-12, never reopens terminal row | | API-071 | `GET /api/v1/admin/jobs/:jobId` | A; ID -> redacted state/step/code/timestamps/lease diagnostics | 403,404 | ETag/read-only | admin.read_job_diagnostics; no owner SSE/content | | API-072 | `GET /api/v1/outputs/:outputId/download` | U owner or download-scoped capability; conditional request -> attachment bytes | 404,416,503 | immutable ETag/read-only | output.download metric; authorized Range stream | ## Cursor shape The opaque cursor encodes versioned canonical sort position, filter hash, and UUID tie-breaker as base64url JSON plus HMAC-SHA256 under an HKDF-derived app key. The implemented admin-user cursor is `{v:1, sort:{createdAt,id}, filterHash}` and binds normalized status/search. It never contains email, display name, secret values, storage keys, provider operation IDs, or token material. Invalid encoding/MAC/version/key/filter is `400 INVALID_CURSOR`; default/max page sizes are 25/100. ## Contract ownership and evolution Request, response, event, cursor, error, and composition schemas live in `packages/contracts` and are imported by both Fastify and React. API v1 additions are backward compatible; breaking shape/meaning requires a new API or schema version. OpenAPI may be generated from the canonical Zod contracts in Phase 3, but generated output is not a competing source of truth. The capability map contains 72 sequential capabilities. Render cancellation and retry use only the generic Job commands; no parallel render-specific command routes exist. Owner-facing Project, CompositionVersion, Job history/status and SSE routes are owner-only. Administrators inspect Jobs only through the redacted admin list/detail/command surfaces, which exclude prompts, private bytes, storage paths/keys, raw provider payloads, and owner SSE.