openapi: 3.1.0 info: title: "@webhouse/cms API" version: "0.3.0" description: | WebHouse CMS REST API — headless content, admin operations, AI chat, and deploy triggers. ## Authentication ### Bearer Token (recommended for sites & integrations) Create a `wh_` Access Token in **Account Preferences → Access Tokens**. Send it as a Bearer token in the `Authorization` header: ``` Authorization: Bearer wh_xxxxxxxxxxxxxxxxxxxx ``` Tokens are scoped to specific sites and permissions. Never expose them client-side. ### Session Cookie (admin UI) Login via `POST /api/auth/login` → sets `cms-session` httpOnly cookie. ### Service Token (machine-to-machine) Set `X-CMS-Service-Token: {CMS_JWT_SECRET}` header for cron/heartbeat calls. ## Field-level AI lock `_fieldMeta` is not included in standard document responses. Use the dedicated `/_fieldMeta` endpoints to read and manipulate lock state. servers: - url: https://webhouse.app description: WebHouse Cloud (production) - url: https://localhost:3010 description: Local dev (HTTPS, mkcert) - url: "{baseUrl}" description: Self-hosted variables: baseUrl: default: https://localhost:3010 tags: - name: Headless description: | Content API for sites and integrations. Use Bearer `wh_` token. See [Headless Site API guide](https://docs.webhouse.app/docs/headless-api). - name: Content description: Low-level content CRUD (legacy `/api/content/` prefix) - name: FieldMeta description: Field-level lock and AI-provenance metadata - name: Schema description: Collection JSON Schemas (read-only) - name: Manifest description: CMS configuration and manifest - name: Auth description: Authentication (login, logout, session) - name: Admin description: Admin operations (profile, team, config, deploy, access tokens) - name: Deploy description: Deploy triggers, status, and completion webhooks - name: Search description: Full-text search across all collections and media - name: Chat description: AI chat — same model and tools as CMS Admin UI - name: Scheduler description: Heartbeat, scheduled tasks, calendar feed - name: Media description: File upload, media library, serving - name: AI description: AI generation, rewriting, agents, curation - name: MCP description: Model Context Protocol servers (public + admin) # --------------------------------------------------------------------------- # Reusable components + security schemes # --------------------------------------------------------------------------- components: securitySchemes: BearerToken: type: http scheme: bearer description: | `wh_` Access Token created in Account Preferences → Access Tokens. Scoped to specific sites and permissions. Example: `Authorization: Bearer wh_76a2153cc365ed47...` SessionCookie: type: apiKey in: cookie name: cms-session description: JWT cookie set after login via POST /api/auth/login ServiceToken: type: apiKey in: header name: X-CMS-Service-Token description: CMS_JWT_SECRET value — for machine-to-machine (cron, heartbeat) parameters: collection: name: collection in: path required: true schema: type: string example: posts description: Navn på collection som defineret i cms.config.ts slug: name: slug in: path required: true schema: type: string example: hello-world description: Dokumentets URL-slug fieldPath: name: fieldPath in: path required: true schema: type: string example: title description: Feltnavnet der skal låses/åbnes status: name: status in: query schema: $ref: "#/components/schemas/DocumentStatus" description: Filtrer på status limit: name: limit in: query schema: type: integer minimum: 1 example: 20 description: Maks antal dokumenter i svaret offset: name: offset in: query schema: type: integer minimum: 0 example: 0 description: Antal dokumenter at springe over (pagination) orderBy: name: orderBy in: query schema: type: string example: createdAt description: Felt at sortere på (createdAt, updatedAt, eller et data-felt) order: name: order in: query schema: type: string enum: [asc, desc] default: desc description: Sorteringsretning locale: name: locale in: query schema: type: string example: "da" description: Filter by locale (BCP 47, e.g. "en", "da") tags: name: tags in: query schema: type: string example: "typescript,cms" description: Komma-separeret liste af tags at filtrere på (AND logik) schemas: DocumentStatus: type: string enum: [draft, published, archived] FieldMeta: type: object description: | Per-felt metadata. Beskriver om feltet er låst og/eller AI-genereret. `lockedBy` angiver hvem der låste feltet — er den sat, vil AI-writes blive afvist. properties: lockedBy: type: string enum: [user, ai, import] description: Hvem der har låst feltet lockedAt: type: string format: date-time description: ISO-timestamp for hvornår feltet blev låst userId: type: string description: ID på brugeren der låste feltet (audit trail) reason: type: string description: Human-readable årsag til låsning (f.eks. "user-edit", "import", "manual-lock") aiGenerated: type: boolean description: Om feltets nuværende indhold er AI-genereret aiGeneratedAt: type: string format: date-time description: ISO-timestamp for hvornår AI genererede indholdet aiModel: type: string description: AI-modellen der genererede indholdet (f.eks. "claude-sonnet-4-6") DocumentFieldMeta: type: object description: Map fra feltnavn til FieldMeta additionalProperties: $ref: "#/components/schemas/FieldMeta" example: title: lockedBy: user lockedAt: "2024-06-01T10:00:00Z" userId: user-abc reason: user-edit aiGenerated: true aiGeneratedAt: "2024-05-31T09:00:00Z" aiModel: claude-sonnet-4-6 content: aiGenerated: true aiGeneratedAt: "2024-05-31T09:00:00Z" aiModel: claude-sonnet-4-6 Document: type: object required: [id, slug, collection, status, data, createdAt, updatedAt] properties: id: type: string description: Unik dokument-ID (nanoid) example: V1StGXR8_Z5jdHi6B-myT slug: type: string description: URL-venligt slug (unikt inden for collection) example: hello-world collection: type: string example: posts status: $ref: "#/components/schemas/DocumentStatus" data: type: object description: Dokumentets felt-data (fri form, defineret af collection-schema) additionalProperties: true example: title: Hello World content: "# Min første post" createdAt: type: string format: date-time updatedAt: type: string format: date-time locale: type: string description: BCP 47 locale tag (e.g. "en", "da") example: en translationOf: type: string description: Slug of the source document (for translations) example: hello-world publishAt: type: string format: date-time description: Scheduled publish timestamp (ISO) description: | `_fieldMeta` returneres **ikke** i dette objekt. Brug `GET /{collection}/{slug}/_fieldMeta` for at hente det. DocumentList: type: object required: [documents, total] properties: documents: type: array items: $ref: "#/components/schemas/Document" total: type: integer description: Samlet antal dokumenter (uanset limit/offset) CreateDocumentBody: type: object required: [data] properties: slug: type: string description: Valgfrit. Autogenereres fra title-felt hvis udeladt. status: $ref: "#/components/schemas/DocumentStatus" data: type: object additionalProperties: true UpdateDocumentBody: type: object properties: slug: type: string status: $ref: "#/components/schemas/DocumentStatus" data: type: object additionalProperties: true description: Felter merges med eksisterende data (partial update) locale: type: string description: BCP 47 locale tag translationOf: type: string description: Slug of source document publishAt: type: string format: date-time description: Schedule publish at this time (null to clear) LockFieldBody: type: object properties: userId: type: string description: ID på den bruger der udfører låsningen (audit trail) reason: type: string description: Årsag til manuel låsning example: sensitive-copy LockAllBody: type: object properties: userId: type: string description: ID på den bruger der udfører bulk-låsningen ErrorResponse: type: object required: [error] properties: error: type: string responses: NotFound: description: Dokument eller collection ikke fundet content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" BadRequest: description: Ugyldig input content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" # --------------------------------------------------------------------------- # Root # --------------------------------------------------------------------------- paths: /: get: summary: API info operationId: getInfo tags: [Content] responses: "200": description: Navn, version og API-prefix content: application/json: schema: type: object properties: name: type: string example: "@webhouse/cms" version: type: string example: "0.1.0" api: type: string example: /api # --------------------------------------------------------------------------- # Content # --------------------------------------------------------------------------- /api/content/{collection}: get: summary: List dokumenter operationId: listDocuments tags: [Content] parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/status" - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/orderBy" - $ref: "#/components/parameters/order" - $ref: "#/components/parameters/tags" responses: "200": description: Pagineret liste af dokumenter (uden _fieldMeta) content: application/json: schema: $ref: "#/components/schemas/DocumentList" "404": $ref: "#/components/responses/NotFound" post: summary: Opret dokument operationId: createDocument tags: [Content] parameters: - $ref: "#/components/parameters/collection" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDocumentBody" responses: "201": description: Oprettet dokument (uden _fieldMeta) content: application/json: schema: $ref: "#/components/schemas/Document" "400": $ref: "#/components/responses/BadRequest" /api/content/{collection}/{slug}: get: summary: Hent dokument operationId: getDocument tags: [Content] parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" responses: "200": description: Dokument (uden _fieldMeta) content: application/json: schema: $ref: "#/components/schemas/Document" "404": $ref: "#/components/responses/NotFound" put: summary: Opdater dokument operationId: updateDocument tags: [Content] description: | Felter i `data` merges med eksisterende data (shallow merge). Felter der er låst af en bruger vil **ikke** blive overskrevet — kaldet sker med `actor: user`, så eksisterende user-locks respekteres ikke her. Brug `/_fieldMeta`-endpoints til at styre locks manuelt. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateDocumentBody" responses: "200": description: Opdateret dokument (uden _fieldMeta) content: application/json: schema: $ref: "#/components/schemas/Document" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" delete: summary: Slet dokument operationId: deleteDocument tags: [Content] parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" responses: "200": description: Slettet content: application/json: schema: type: object properties: success: type: boolean "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" # --------------------------------------------------------------------------- # FieldMeta # --------------------------------------------------------------------------- /api/content/{collection}/{slug}/_fieldMeta: get: summary: Hent field metadata operationId: getFieldMeta tags: [FieldMeta] description: | Returnerer `_fieldMeta` for dokumentet: lock-state og AI-provenance pr. felt. Denne information er stripet fra alle standard GET-responses. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" responses: "200": description: Field metadata map content: application/json: schema: $ref: "#/components/schemas/DocumentFieldMeta" "404": $ref: "#/components/responses/NotFound" /api/content/{collection}/{slug}/_fieldMeta/lock-all: put: summary: Lås alle AI-genererede felter operationId: lockAllFields tags: [FieldMeta] description: "Sætter `lockedBy: user` på alle felter der har `aiGenerated: true`." parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" requestBody: content: application/json: schema: $ref: "#/components/schemas/LockAllBody" responses: "200": description: Opdateret field metadata map content: application/json: schema: $ref: "#/components/schemas/DocumentFieldMeta" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" /api/content/{collection}/{slug}/_fieldMeta/unlock-all: put: summary: Fjern alle locks operationId: unlockAllFields tags: [FieldMeta] description: Fjerner `lockedBy`, `lockedAt`, `userId` og `reason` fra alle felter. AI-genererings-metadata bevares. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" responses: "200": description: Opdateret field metadata map content: application/json: schema: $ref: "#/components/schemas/DocumentFieldMeta" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" /api/content/{collection}/{slug}/_fieldMeta/{fieldPath}/lock: put: summary: Lås enkelt felt manuelt operationId: lockField tags: [FieldMeta] description: | Sætter `lockedBy: user` på det angivne felt. Fremtidige AI-writes til dette felt vil blive afvist. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" - $ref: "#/components/parameters/fieldPath" requestBody: content: application/json: schema: $ref: "#/components/schemas/LockFieldBody" responses: "200": description: Opdateret FieldMeta for det låste felt content: application/json: schema: $ref: "#/components/schemas/FieldMeta" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" /api/content/{collection}/{slug}/_fieldMeta/{fieldPath}/unlock: put: summary: Fjern lock fra enkelt felt operationId: unlockField tags: [FieldMeta] description: | Fjerner `lockedBy`, `lockedAt`, `userId` og `reason` fra feltet. AI-genererings-metadata (`aiGenerated`, `aiModel` osv.) bevares intakt. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" - $ref: "#/components/parameters/fieldPath" responses: "200": description: Opdateret FieldMeta for det åbnede felt content: application/json: schema: $ref: "#/components/schemas/FieldMeta" "400": $ref: "#/components/responses/BadRequest" "404": $ref: "#/components/responses/NotFound" # --------------------------------------------------------------------------- # Schema # --------------------------------------------------------------------------- /api/schema: get: summary: Hent alle collection-schemas operationId: listSchemas tags: [Schema] responses: "200": description: Alle collections med JSON Schema content: application/json: schema: type: object properties: collections: type: array items: type: object properties: name: type: string label: type: string slug: type: string jsonSchema: type: object description: JSON Schema for collectionens data-felter blocks: type: array items: type: object /api/schema/{collection}: get: summary: Hent schema for én collection operationId: getSchema tags: [Schema] parameters: - $ref: "#/components/parameters/collection" responses: "200": description: Collection med JSON Schema content: application/json: schema: type: object properties: name: type: string label: type: string jsonSchema: type: object "404": $ref: "#/components/responses/NotFound" # --------------------------------------------------------------------------- # Manifest # --------------------------------------------------------------------------- /api/manifest: get: summary: Get CMS manifest operationId: getManifest tags: [Manifest] description: Returns full CMS configuration (collections, build config, storage type, etc.) responses: "200": description: CMS manifest content: application/json: schema: type: object description: Serialized CmsConfig # --------------------------------------------------------------------------- # Auth # --------------------------------------------------------------------------- /api/auth/login: post: summary: Log in operationId: login tags: [Auth] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: { type: string, example: "admin@example.com" } password: { type: string, example: "my-password" } responses: "200": description: Login successful — sets cms-session cookie content: application/json: schema: type: object properties: ok: { type: boolean } email: { type: string } name: { type: string } "401": description: Invalid credentials /api/auth/logout: post: summary: Log out operationId: logout tags: [Auth] responses: "200": description: Session cookie cleared /api/auth/me: get: summary: Get current user operationId: getMe tags: [Auth] responses: "200": description: "Authenticated user profile (or user: null)" content: application/json: schema: type: object properties: user: type: object nullable: true properties: id: { type: string } email: { type: string } name: { type: string } role: { type: string, enum: [admin, editor, viewer] } siteRole: { type: string, nullable: true } zoom: { type: integer } /api/auth/setup: get: summary: Check if initial setup is needed operationId: checkSetup tags: [Auth] responses: "200": content: application/json: schema: type: object properties: hasUsers: { type: boolean } post: summary: Create first admin account operationId: setupAdmin tags: [Auth] requestBody: required: true content: application/json: schema: type: object required: [email, password, name] properties: email: { type: string } password: { type: string, minLength: 8 } name: { type: string } responses: "200": description: Account created — sets cms-session cookie "403": description: Setup already complete # --------------------------------------------------------------------------- # Scheduler & Heartbeat # --------------------------------------------------------------------------- /api/cms/heartbeat: get: summary: Run all pending scheduled tasks operationId: heartbeat tags: [Scheduler] description: | Executes all pending scheduled tasks immediately: 1. Publish/unpublish documents past their scheduled date 2. Run due AI agents 3. Run tools scheduler (backup, link check) 4. Update calendar snapshot Designed to be called by an external cron (macOS crontab, GitHub Actions, or any uptime monitor) to keep scheduled tasks running even when the CMS admin has no interactive traffic. **Auth:** Requires `X-CMS-Service-Token` header matching `CMS_JWT_SECRET`. responses: "200": description: Tasks executed content: application/json: schema: type: object properties: ok: { type: boolean } durationMs: { type: integer, example: 1234 } ran: type: array items: { type: string } example: ["published - 2 doc(s)", "backup - completed", "snapshot - updated"] errors: type: array items: { type: string } timestamp: { type: string, format: date-time } "401": description: Missing or invalid service token /api/cms/scheduled/calendar.ics: get: summary: iCalendar feed of scheduled events operationId: calendarFeed tags: [Scheduler] description: | Returns an iCalendar (.ics) feed with all scheduled publish/unpublish events, upcoming backups, and link checks. Subscribe from Apple Calendar, Google Calendar, etc. **Auth:** Token-based via `?token=` query parameter (HMAC of user ID). parameters: - name: token in: query required: true schema: { type: string } description: HMAC token for calendar access - name: org in: query schema: { type: string } description: Organization ID - name: site in: query schema: { type: string } description: Site ID responses: "200": description: iCalendar feed content: text/calendar: schema: type: string "401": description: Invalid token # --------------------------------------------------------------------------- # Media # --------------------------------------------------------------------------- /api/upload: post: summary: Upload file operationId: uploadFile tags: [Media] description: Upload a file to the media library. Supports images, audio, documents, SVG. requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary folder: type: string description: Target folder (e.g. "images", "audio") responses: "200": description: Upload successful content: application/json: schema: type: object properties: url: { type: string, example: "/uploads/images/photo.jpg" } filename: { type: string } size: { type: integer } /api/media: get: summary: List media files operationId: listMedia tags: [Media] responses: "200": description: All media files with metadata content: application/json: schema: type: object properties: files: type: array items: type: object properties: name: { type: string } url: { type: string } folder: { type: string } size: { type: integer } type: { type: string, enum: [image, audio, document, svg] } # --------------------------------------------------------------------------- # AI Agents # --------------------------------------------------------------------------- /api/cms/agents: get: summary: List AI agents operationId: listAgents tags: [AI] responses: "200": description: All agent configurations post: summary: Create AI agent operationId: createAgent tags: [AI] responses: "201": description: Agent created /api/cms/agents/{id}/run: post: summary: Run AI agent operationId: runAgent tags: [AI] description: Execute an agent with a prompt. Results go to curation queue or publish directly based on autonomy setting. parameters: - name: id in: path required: true schema: { type: string } requestBody: content: application/json: schema: type: object properties: prompt: { type: string } collection: { type: string } responses: "200": description: Agent execution result # --------------------------------------------------------------------------- # Curation # --------------------------------------------------------------------------- /api/cms/curation: get: summary: List curation queue operationId: listCuration tags: [AI] description: AI-generated content waiting for human review. parameters: - name: stats in: query schema: { type: boolean } description: Include queue statistics responses: "200": description: Queue items /api/cms/curation/{id}/approve: post: summary: Approve curation item operationId: approveCuration tags: [AI] parameters: - name: id in: path required: true schema: { type: string } responses: "200": description: Item approved and published/drafted /api/cms/curation/{id}/reject: post: summary: Reject curation item operationId: rejectCuration tags: [AI] parameters: - name: id in: path required: true schema: { type: string } responses: "200": description: Item rejected # --------------------------------------------------------------------------- # Admin # --------------------------------------------------------------------------- /api/admin/profile: post: summary: Update user profile operationId: updateProfile tags: [Admin] description: Update name, email, password, zoom, or last active site. requestBody: content: application/json: schema: type: object properties: name: { type: string } email: { type: string } currentPassword: { type: string } newPassword: { type: string, minLength: 8 } zoom: { type: integer } lastActiveOrg: { type: string } lastActiveSite: { type: string } responses: "200": description: Profile updated — new JWT cookie set "400": description: Validation error (wrong password, etc.) /api/admin/site-config: get: summary: Get site configuration operationId: getSiteConfig tags: [Admin] description: Returns site-specific settings (backup schedule, link check, revalidation, etc.) plus resolvedContentDir. responses: "200": description: Site configuration post: summary: Update site configuration operationId: updateSiteConfig tags: [Admin] responses: "200": description: Updated configuration "403": description: Admin role required /api/admin/backups: get: summary: List backups operationId: listBackups tags: [Admin] responses: "200": description: Backup manifest with snapshots post: summary: Create backup operationId: createBackup tags: [Admin] requestBody: content: application/json: schema: type: object properties: trigger: { type: string, enum: [manual, scheduled] } responses: "200": description: Backup created /api/admin/deploy: post: summary: Trigger deploy operationId: triggerDeploy tags: [Admin] description: Trigger a site deployment via configured deploy hook (Vercel, Netlify, Fly.io, etc.) responses: "200": description: Deploy triggered "400": description: No deploy hook configured # --------------------------------------------------------------------------- # MCP (Model Context Protocol) # --------------------------------------------------------------------------- /api/mcp/info: get: summary: MCP server info operationId: mcpInfo tags: [MCP] description: Returns available MCP tools, collections, auth status, and rate limit info. responses: "200": description: MCP server metadata # --------------------------------------------------------------------------- # Headless Content API (F139) — use Bearer wh_ token # --------------------------------------------------------------------------- /api/cms/{collection}: get: summary: List documents (headless) operationId: headlessListDocuments tags: [Headless] security: - BearerToken: [] - SessionCookie: [] description: | List documents in a collection. Requires `content:read` permission. Add `?site=` when calling from a token with multi-site scope. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/status" - $ref: "#/components/parameters/locale" - $ref: "#/components/parameters/limit" - $ref: "#/components/parameters/offset" - $ref: "#/components/parameters/tags" - name: site in: query schema: { type: string } description: Site ID (required for multi-site tokens) responses: "200": description: Document list content: application/json: schema: $ref: "#/components/schemas/DocumentList" "401": description: Invalid or missing token "403": description: Token lacks content:read permission post: summary: Create document (headless) operationId: headlessCreateDocument tags: [Headless] security: - BearerToken: [] - SessionCookie: [] description: Requires `content:write` permission. parameters: - $ref: "#/components/parameters/collection" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDocumentBody" responses: "200": description: Created document content: application/json: schema: $ref: "#/components/schemas/Document" "403": description: Token lacks content:write permission /api/cms/{collection}/{slug}: get: summary: Get document by slug (headless) operationId: headlessGetDocument tags: [Headless] security: - BearerToken: [] - SessionCookie: [] parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" responses: "200": description: Document content: application/json: schema: $ref: "#/components/schemas/Document" "404": $ref: "#/components/responses/NotFound" patch: summary: Update document (headless) operationId: headlessUpdateDocument tags: [Headless] security: - BearerToken: [] - SessionCookie: [] description: Partial update — only supplied `data` fields are merged. Requires `content:write`. parameters: - $ref: "#/components/parameters/collection" - $ref: "#/components/parameters/slug" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateDocumentBody" responses: "200": description: Updated document content: application/json: schema: $ref: "#/components/schemas/Document" # --------------------------------------------------------------------------- # Full-text Search (F139) # --------------------------------------------------------------------------- /api/search: get: summary: Search all collections + media operationId: searchContent tags: [Search] security: - BearerToken: [] - SessionCookie: [] description: | Multi-token AND search across all collections. Tokens are split on spaces and commas — all must appear somewhere in the document (slug, title, or any field). Richtext body is fully indexed (TipTap JSON traversed to leaf text nodes). parameters: - name: q in: query required: true schema: { type: string } description: Search query. Comma/space-separated terms are ANDed. example: "Qigong, sund mad" responses: "200": description: Search results content: application/json: schema: type: array items: type: object properties: collection: { type: string } collectionLabel: { type: string } slug: { type: string } title: { type: string } status: { type: string } matchedIn: type: string enum: [title, body] description: Whether the match was in the title or body/fields # --------------------------------------------------------------------------- # Deploy (F139, deploy notifications) # --------------------------------------------------------------------------- /api/admin/deploy/notify: get: summary: SSE stream — deploy completion events operationId: deployNotifyStream tags: [Deploy] security: - BearerToken: [] - SessionCookie: [] description: | Server-Sent Events stream. Receives `deploy-done` events when GitHub Actions (or any CI) calls `POST /api/admin/deploy/notify` on completion. Connect once per tab/session; the CMS admin UI uses this to show the "Published! 🚀" toast without polling. responses: "200": description: SSE stream (text/event-stream) post: summary: Webhook — notify CMS of completed deploy operationId: deployNotifyWebhook tags: [Deploy] description: | Called by GitHub Actions (or other CI) at the end of a deploy workflow. Broadcasts a `deploy-done` SSE event to all connected admin tabs. Authenticate with `Authorization: Bearer {DEPLOY_NOTIFY_SECRET}`. requestBody: content: application/json: schema: type: object properties: status: type: string enum: [success, failure] url: type: string description: Live site URL to show in the toast app: type: string description: Fly.io app name or other identifier duration: type: integer description: Build duration in seconds responses: "200": description: Event broadcast to connected clients content: application/json: schema: type: object properties: ok: { type: boolean } clients: { type: integer, description: "Number of SSE clients notified" } "401": description: Invalid DEPLOY_NOTIFY_SECRET /api/admin/deploy/github-status: get: summary: Latest GitHub Actions run status operationId: deployGitHubStatus tags: [Deploy] security: - BearerToken: [] - SessionCookie: [] description: | Polls the latest workflow run for the active site's `deployAppName` repo. Used by the Deploy button to show "Building..." / "Queued..." live status. Requires `deployApiToken` (GitHub PAT) configured in site settings. responses: "200": description: Latest run status content: application/json: schema: type: object properties: found: { type: boolean } runId: { type: integer } status: type: string enum: [queued, in_progress, completed] conclusion: type: string enum: [success, failure, cancelled, skipped] nullable: true url: { type: string, description: "GitHub Actions run URL" } startedAt: { type: string, format: date-time } # --------------------------------------------------------------------------- # Access Tokens (F134) # --------------------------------------------------------------------------- /api/admin/access-tokens: get: summary: List access tokens operationId: listAccessTokens tags: [Admin] security: - SessionCookie: [] description: Returns all tokens for the current user (tokens masked — first 10 + last 4 chars shown). responses: "200": description: Token list post: summary: Create access token operationId: createAccessToken tags: [Admin] security: - SessionCookie: [] description: | Create a `wh_` Bearer token for headless site access, CI integrations, or MCP clients. Returns the full token value **once only** — store it immediately. requestBody: required: true content: application/json: schema: type: object required: [name, permissions] properties: name: type: string example: "Sanne Andersen site headless" permissions: type: array items: type: string enum: - content:read - content:write - content:publish - media:read - media:write - media:delete - deploy:trigger - deploy:read - forms:read - forms:write - team:manage - tokens:manage - sites:read - sites:write - org:settings:read - org:settings:write example: ["content:read", "forms:read", "deploy:trigger", "deploy:read"] resources: type: array description: Restrict token to specific sites or orgs items: type: object required: [scope, effect, targets] properties: scope: type: string enum: [org, site, admin-area] effect: type: string enum: [include, exclude] targets: oneOf: - type: string enum: ["*"] - type: array items: { type: string } example: - scope: site effect: include targets: ["sanneandersen"] notAfter: type: string format: date-time description: Optional expiry timestamp responses: "200": description: Token created — full value returned once content: application/json: schema: type: object properties: token: type: string description: Full wh_ token (only returned on creation) example: "wh_76a2153cc365ed47..." id: { type: string } name: { type: string } permissions: type: array items: { type: string } /api/admin/access-tokens/{id}: delete: summary: Revoke access token operationId: revokeAccessToken tags: [Admin] security: - SessionCookie: [] parameters: - name: id in: path required: true schema: { type: string } responses: "200": description: Token revoked # --------------------------------------------------------------------------- # AI Chat (F139 — embed in sites) # --------------------------------------------------------------------------- /api/cms/chat: post: summary: Chat — streaming AI response operationId: chat tags: [Chat] security: - BearerToken: [] - SessionCookie: [] description: | Runs the same Claude model and tool set as CMS Admin chat. Returns a Server-Sent Events stream. Parse events: - `event: text` `data: {"text":"..."}` — streamed text chunk - `event: tool_call` `data: {"tool":"...","input":{}}` — tool executing - `event: tool_result` `data: {"tool":"...","result":"..."}` — tool result - `event: done` `data: {}` — stream complete **Restrict tools** by only granting the permissions you need on the Bearer token. Example: omit `content:write` to make the chat read-only. For site embedding, proxy through a server route — never call from client-side with the token directly. See [docs](https://docs.webhouse.app/docs/headless-api#5-embed-the-ai-chat). requestBody: required: true content: application/json: schema: type: object required: [messages] properties: messages: type: array items: type: object required: [role, content] properties: role: type: string enum: [user, assistant] content: type: string conversationId: type: string description: UUID for conversation persistence and memory extraction model: type: string description: Override model (must be in allowed list) example: claude-haiku-4-5-20251001 responses: "200": description: SSE stream (text/event-stream) headers: Content-Type: schema: type: string example: text/event-stream "401": description: Invalid token "503": description: Anthropic API key not configured