openapi: 3.0.3
info:
  title: Brandlit Studio HTTP API
  version: 2026-10-05.3
  description: 'Public HTTP API for Brandlit Studio: projects, Connect with Grok Bot
    (device code), Grok website edits, Brandlit-hosted publishing, and bring-your-own-domain
    attach via DNS CNAME.


    **Auth:** cookie session today. Guest identity uses the `bl_uid` cookie (or `X-Brandlit-Uid`
    header); signing in or completing Connect sets the HttpOnly `bl_session` cookie.
    Machine Bearer tokens are not available yet.


    **curl:** use a cookie jar (`-c jar.txt -b jar.txt`) on every call so identity
    sticks.


    **CLI:** `brandlit` v1.0.0 (zero-dependency Node 18+) wraps this API. See /docs/CLI-README.md.


    **Prices:** read the current catalog from `GET /api/billing/products`; do not
    hard-code amounts.'
  contact:
    name: BlackLabel Tech
    url: https://blacklabelbots.com
servers:
- url: https://brandlit.blacklabelbots.com
  description: Production Brandlit Studio
tags:
- name: Discovery
- name: Auth
- name: Connect
- name: DomOps
- name: KeyVault
- name: Projects
- name: Domains
- name: Billing
- name: Usage
- name: Templates
- name: Hosting
paths:
  /health:
    get:
      tags:
      - Discovery
      summary: Capability map
      operationId: getHealth
      security: []
      responses:
        '200':
          description: Live capability flags and service status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
  /api/mode:
    get:
      tags:
      - Discovery
      summary: Brain mode without requiring a signed-in session
      operationId: getMode
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Brain / site mode
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModeResponse'
  /api/auth/signup:
    post:
      tags:
      - Auth
      summary: Create email+password account; sets bl_session
      operationId: authSignup
      security:
      - cookieGuest: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 8
                  maxLength: 200
      responses:
        '200':
          description: Account created; Set-Cookie bl_session
          headers:
            Set-Cookie:
              schema:
                type: string
              description: bl_session=…; Path=/; HttpOnly; SameSite=Lax
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSessionResponse'
        '400':
          $ref: '#/components/responses/Error'
        '409':
          description: Account already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
  /api/auth/signin:
    post:
      tags:
      - Auth
      summary: Sign in; sets bl_session
      operationId: authSignin
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        '200':
          description: Signed in; Set-Cookie bl_session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthSessionResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/auth/signout:
    post:
      tags:
      - Auth
      summary: Clear bl_session
      operationId: authSignout
      security:
      - cookieSession: []
      responses:
        '200':
          description: Signed out; clears bl_session cookie
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  signedOut:
                    type: boolean
  /api/auth/me:
    get:
      tags:
      - Auth
      summary: Session + usage meters
      operationId: authMe
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Guest or authed identity plus usage
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthMeResponse'
  /api/auth/forgot:
    post:
      tags:
      - Auth
      summary: Password reset request (may return a copyable reset link when email
        delivery is unavailable)
      operationId: authForgot
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: Always ok-shaped (anti-enumeration); issued may be false
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  issued:
                    type: boolean
                  message:
                    type: string
                  smtpSent:
                    type: boolean
                  resetUrl:
                    type: string
                    description: Present when a reset link is returned directly instead
                      of by email
  /api/auth/reset:
    post:
      tags:
      - Auth
      summary: Complete password reset with KV token
      operationId: authReset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - token
              - password
              properties:
                token:
                  type: string
                password:
                  type: string
                  minLength: 8
      responses:
        '200':
          description: Password updated; may mint bl_session
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '400':
          $ref: '#/components/responses/Error'
  /api/auth/grokbot/start:
    get:
      tags:
      - Connect
      summary: Start device-code Connect with Grok Bot
      operationId: grokbotStart
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - name: mode
        in: query
        required: false
        schema:
          type: string
          enum:
          - oauth
        description: If Cursor OAuth client configured, redirects; production typically
          returns device_code JSON
      responses:
        '200':
          description: Device-code payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokBotStartResponse'
        '302':
          description: OAuth authorize redirect when mode=oauth and CURSOR_OAUTH_CLIENT_ID
            set
  /api/auth/grokbot/poll:
    get:
      tags:
      - Connect
      summary: Poll device code until approved; mints bl_session
      operationId: grokbotPoll
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - name: device_code
        in: query
        required: true
        schema:
          type: string
      responses:
        '200':
          description: pending or approved (approved sets bl_session)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokBotPollResponse'
        '403':
          description: Denied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '404':
          description: Expired or unknown device_code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
  /api/auth/grokbot/approve:
    post:
      tags:
      - Connect
      summary: Same-browser user approve
      description: Live Worker route. Body needs device_code or user_code.
      operationId: grokbotApprove
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                device_code:
                  type: string
                user_code:
                  type: string
      responses:
        '200':
          description: Device marked approved — client should poll
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '400':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /api/auth/grokbot/callback:
    get:
      tags:
      - Connect
      summary: OAuth callback (not enabled; returns 501, use device code)
      operationId: grokbotCallback
      security: []
      parameters:
      - name: code
        in: query
        schema:
          type: string
      - name: state
        in: query
        schema:
          type: string
      responses:
        '302':
          description: Redirect to app when code/state missing
        '501':
          description: Cursor OAuth not configured / token exchange not implemented
            — use device-code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
  /api/auth/grokbot/status:
    get:
      tags:
      - Connect
      summary: Link + platform brain flags
      operationId: grokbotStatus
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokBotStatusResponse'
  /api/auth/grokbot/disconnect:
    post:
      tags:
      - Connect
      summary: Clear Grok Bot entitlement for current identity
      operationId: grokbotDisconnect
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Disconnected
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  disconnected:
                    type: boolean
                  grokBotLinked:
                    type: boolean
                  hasGrokBrain:
                    type: boolean
                  brainMode:
                    type: string
  /api/grok:
    post:
      tags:
      - DomOps
      summary: DomOps inference (utterance → ops JSON)
      description: 'Requires Connect entitlement and/or key path depending on live
        gate.

        Observed error codes include `connect_grokbot`, `needs_key`, `free_exhausted`,
        `platform_brain_unconfigured`.

        Transport is platform xAI chat/completions when linked (not CLI subscription
        proxy).

        '
      operationId: grokRun
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - utterance
              properties:
                utterance:
                  type: string
                  description: Natural language edit request
                text:
                  type: string
                  description: Alias for utterance
                snapshot:
                  type: object
                  description: Current layout snapshot (opaque to API; forwarded to
                    model)
                model:
                  type: string
                  description: Optional model override (default grok-3-mini / Worker
                    XAI_MODEL)
      responses:
        '200':
          description: DomOps batch
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokSuccessResponse'
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '402':
          description: Free meter exhausted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '503':
          $ref: '#/components/responses/Error'
  /api/grok/test:
    get:
      tags:
      - DomOps
      summary: Ping platform/BYOK key
      operationId: grokTestGet
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Key/platform ping ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokTestResponse'
        '401':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
    post:
      tags:
      - DomOps
      summary: Ping with optional key in body
      operationId: grokTestPost
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                key:
                  type: string
                apiKey:
                  type: string
      responses:
        '200':
          description: Key/platform ping ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrokTestResponse'
        '401':
          $ref: '#/components/responses/Error'
  /api/key:
    post:
      tags:
      - KeyVault
      summary: Save advanced BYOK xAI key (AES-GCM in KV)
      operationId: keySave
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                key:
                  type: string
                apiKey:
                  type: string
                xaiKey:
                  type: string
      responses:
        '200':
          description: Stored (hint only returned)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  hasKey:
                    type: boolean
                  hint:
                    type: string
                  mode:
                    type: string
                    example: byok
        '400':
          $ref: '#/components/responses/Error'
    delete:
      tags:
      - KeyVault
      summary: Clear vaulted BYOK key
      operationId: keyClear
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Cleared
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  hasKey:
                    type: boolean
                  mode:
                    type: string
  /api/key/status:
    get:
      tags:
      - KeyVault
      summary: BYOK vault status (no raw key)
      operationId: keyStatus
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Status
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  hasKey:
                    type: boolean
                  hint:
                    type: string
                    nullable: true
                  uid:
                    type: string
                  mode:
                    type: string
                  designedFor:
                    type: string
                  note:
                    type: string
  /api/projects:
    get:
      tags:
      - Projects
      summary: List projects for current identity
      operationId: projectsList
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Project list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectListResponse'
    post:
      tags:
      - Projects
      summary: Create project
      description: 'Cookie identity owns the project. Live accepts `name`, `mode`,
        and (when catalog matches) `templateId`.

        Unknown `templateId` returns `code: unknown_template`. Blank create without
        templateId is the reliable path.

        '
      operationId: projectsCreate
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 80
                mode:
                  type: string
                  enum:
                  - blank
                  - salesswipe
                  default: blank
                templateId:
                  type: string
                  description: Optional; must match live template catalog ids
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectCreateResponse'
        '400':
          $ref: '#/components/responses/Error'
        '402':
          $ref: '#/components/responses/Error'
  /api/projects/{projectId}:
    get:
      tags:
      - Projects
      summary: Get project meta + HTML + ops + sidecar + assets
      operationId: projectsGet
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      responses:
        '200':
          description: Project, html, ops, sidecar, and asset list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectGetResponse'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
    put:
      tags:
      - Projects
      summary: Save snapshot (alias of POST …/save)
      operationId: projectsPut
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectSaveBody'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectCreateResponse'
  /api/projects/{projectId}/save:
    post:
      tags:
      - Projects
      summary: Save HTML/ops snapshot to KV (+ R2 mirror on save path when configured)
      operationId: projectsSave
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectSaveBody'
      responses:
        '200':
          description: Saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectCreateResponse'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /api/projects/{projectId}/publish:
    post:
      tags:
      - Projects
      - Hosting
      summary: Mark published; returns pathUrl + siteUrl + BYO CNAME target
      operationId: projectsPublish
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                html:
                  type: string
                  description: Optional HTML to save before publish
      responses:
        '200':
          description: Published URLs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPublishResponse'
  /p/{projectId}:
    get:
      tags:
      - Hosting
      summary: Public published HTML with absolute project-asset URLs
      operationId: publicPath
      security: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      responses:
        '200':
          description: 'text/html. Header `x-brandlit-asset-rewrite` = count of rewritten
            refs. Header `x-brandlit-ext: project-ext/1` when served by brandlit-project-ext.'
          content:
            text/html:
              schema:
                type: string
        '404':
          description: Not found
      description: 'Returns the project''s saved HTML for a published project (editor
        chrome stripped; edge may inject `.webmcp/bridge.js`). Serve-time only: matching
        relative asset refs (`src`, `href`, `srcset`, CSS `url()`) that correspond
        to uploaded `/api/projects/{id}/assets` paths are rewritten to absolute `https://brandlit.blacklabelbots.com/project-assets/{id}/{path}`.
        Absolute http(s), data:, and root-absolute `/…` URLs are left unchanged. Draft
        HTML from `GET /api/projects/{id}` stays relative (byte-equal to save). Unpublished
        → 404 for anonymous.'
  /api/domain/instructions:
    get:
      tags:
      - Domains
      summary: BYO CNAME + preview/site URL templates
      operationId: domainInstructions
      security: []
      parameters:
      - name: projectId
        in: query
        schema:
          type: string
        description: Substituted into CNAME/preview examples when valid
      responses:
        '200':
          description: Instructions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainInstructionsResponse'
  /api/domain/search:
    get:
      tags:
      - Domains
      summary: Search registrable names (CF Registrar when token works; else DNS suggestions)
      operationId: domainSearch
      security: []
      parameters:
      - name: q
        in: query
        required: true
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          default: 8
      responses:
        '200':
          description: Search rows
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainSearchResponse'
        '400':
          $ref: '#/components/responses/Error'
  /api/domain/check:
    post:
      tags:
      - Domains
      summary: Check domain(s) registrability / pricing
      operationId: domainCheck
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                domains:
                  oneOf:
                  - type: array
                    items:
                      type: string
                  - type: string
                domain:
                  type: string
      responses:
        '200':
          description: Check result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
  /api/domain/buy:
    post:
      tags:
      - Domains
      summary: Attempt register (often blocked → BYO guidance)
      description: 'Partial. When registrar write is missing or billing/contact blocks
        purchase,

        response still returns `byo` attach guidance (HTTP 200 with ok:false or guided
        payload).

        Reliable path for integrators: attach + verify CNAME.

        '
      operationId: domainBuy
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                domain:
                  type: string
                name:
                  type: string
                projectId:
                  type: string
      responses:
        '200':
          description: Registered, guided, or BYO-required payload
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '409':
          description: Not registrable
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
  /api/domain/attach:
    post:
      tags:
      - Domains
      summary: Bind custom hostname to owned project
      operationId: domainAttach
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - projectId
              properties:
                domain:
                  type: string
                hostname:
                  type: string
                projectId:
                  type: string
      responses:
        '200':
          description: Attached (verified false until DNS)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '400':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /api/domain/verify:
    post:
      tags:
      - Domains
      summary: DoH CNAME / Brandlit header verify
      operationId: domainVerify
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                domain:
                  type: string
                hostname:
                  type: string
                projectId:
                  type: string
      responses:
        '200':
          description: verified true/false with dns details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
  /api/billing/products:
    get:
      tags:
      - Billing
      summary: Product catalog JSON
      description: Current product catalog returned by the live service. Use this
        as the source for prices; do not hard-code amounts.
      operationId: billingProducts
      security: []
      responses:
        '200':
          description: Products
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingProductsResponse'
  /api/billing/checkout:
    post:
      tags:
      - Billing
      summary: Start Stripe Checkout
      description: Requires a signed-in `bl_session`. Amounts come from the live product
        catalog.
      operationId: billingCheckout
      security:
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                product:
                  type: string
                  description: Product id from GET /api/billing/products
      responses:
        '200':
          description: Checkout URL, or an already-owned result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '401':
          $ref: '#/components/responses/Error'
  /api/billing/confirm:
    get:
      tags:
      - Billing
      summary: Confirm payment / session (auth required)
      operationId: billingConfirmGet
      security:
      - cookieSession: []
      responses:
        '200':
          description: Confirmed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '401':
          description: Sign in required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
    post:
      tags:
      - Billing
      summary: Confirm payment / session (auth required)
      operationId: billingConfirmPost
      security:
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId:
                  type: string
      responses:
        '200':
          description: Confirmed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '401':
          $ref: '#/components/responses/Error'
  /api/usage:
    get:
      tags:
      - Usage
      summary: Usage meters for current identity
      operationId: usageGet
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  usage:
                    $ref: '#/components/schemas/UsagePublic'
  /api/usage/heartbeat:
    post:
      tags:
      - Usage
      summary: Advance editor time meter
      operationId: usageHeartbeat
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                focused:
                  type: boolean
                  default: true
                deltaSec:
                  type: number
                  description: Clamped (Worker HEARTBEAT_MAX_DELTA)
                seconds:
                  type: number
                  description: Alias for deltaSec
      responses:
        '200':
          description: Updated usage; may include code free_exhausted when gated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
  /api/templates:
    get:
      tags:
      - Templates
      summary: Template catalog (+ previews under /template-previews/)
      operationId: templatesList
      security: []
      responses:
        '200':
          description: Catalog
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplatesResponse'
  /api/profile:
    get:
      tags:
      - Auth
      summary: Onboarding profile
      operationId: profileGet
      security:
      - cookieGuest: []
      - cookieSession: []
      responses:
        '200':
          description: Profile + brain flags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
    post:
      tags:
      - Auth
      summary: Update profile fields
      operationId: profilePost
      security:
      - cookieGuest: []
      - cookieSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                onboarding:
                  type: object
                domain:
                  type: string
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
  /api/projects/{projectId}/assets:
    get:
      tags:
      - Projects
      summary: List project assets
      operationId: projectAssetsList
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      responses:
        '200':
          description: Asset list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetListResponse'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
    post:
      tags:
      - Projects
      summary: Upload one or more assets (multipart)
      operationId: projectAssetsUpload
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      description: 'multipart/form-data. Every file part is stored at its filename
        (may include folders, e.g. logos/brand.png). Optional text field `prefix`
        puts all files under that folder. Same path overwrites. Limits: 10 MB per
        file, 20 files per request, 25 MB per request, 200 files and 50 MB per project.
        Allowed: png jpg jpeg gif webp avif svg ico woff woff2 ttf otf css js mjs
        json txt mp4 webm mp3 wav pdf. HTML is rejected (415). Type is taken from
        the file extension.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                prefix:
                  type: string
                file:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetUploadResponse'
        '400':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '415':
          $ref: '#/components/responses/Error'
  /api/projects/{projectId}/assets/{assetPath}:
    get:
      tags:
      - Projects
      summary: Download an asset (owner)
      operationId: projectAssetGet
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      - name: assetPath
        in: path
        required: true
        description: Relative path, up to 5 segments of [A-Za-z0-9._-], e.g. logos/brand.png
        schema:
          type: string
      responses:
        '200':
          description: File bytes
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
    put:
      tags:
      - Projects
      summary: Upload or replace one asset (raw body)
      operationId: projectAssetPut
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      - name: assetPath
        in: path
        required: true
        description: Relative path, up to 5 segments of [A-Za-z0-9._-], e.g. logos/brand.png
        schema:
          type: string
      description: 'Allowed: png jpg jpeg gif webp avif svg ico woff woff2 ttf otf
        css js mjs json txt mp4 webm mp3 wav pdf. HTML is rejected (415). Type is
        taken from the file extension.'
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssetUploadResponse'
        '400':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '415':
          $ref: '#/components/responses/Error'
    delete:
      tags:
      - Projects
      summary: Delete an asset
      operationId: projectAssetDelete
      security:
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      - name: assetPath
        in: path
        required: true
        description: Relative path, up to 5 segments of [A-Za-z0-9._-], e.g. logos/brand.png
        schema:
          type: string
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
  /project-assets/{projectId}/{assetPath}:
    get:
      tags:
      - Projects
      summary: Public asset URL (use in published HTML)
      operationId: projectAssetPublic
      security:
      - {}
      - cookieGuest: []
      - cookieSession: []
      parameters:
      - $ref: '#/components/parameters/projectId'
      - name: assetPath
        in: path
        required: true
        description: Relative path, up to 5 segments of [A-Za-z0-9._-], e.g. logos/brand.png
        schema:
          type: string
      description: Public once the project is published (cache 5 min); before that,
        only the owner (cookie) can read it, and everyone else gets 404. Served with
        nosniff and a sandboxed CSP.
      responses:
        '200':
          description: File bytes
        '304':
          description: Not modified
        '404':
          description: Not found or not published
components:
  securitySchemes:
    cookieGuest:
      type: apiKey
      in: cookie
      name: bl_uid
      description: Guest identity cookie (also accept header X-Brandlit-Uid)
    cookieSession:
      type: apiKey
      in: cookie
      name: bl_session
      description: Signed-in / Connect session cookie. No Authorization Bearer yet.
    brandlitUidHeader:
      type: apiKey
      in: header
      name: X-Brandlit-Uid
      description: Optional guest uid when cookies unavailable (CORS allows this header)
  parameters:
    projectId:
      name: projectId
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9_-]{1,48}$
  responses:
    Error:
      description: Error envelope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
  schemas:
    OkEnvelope:
      type: object
      properties:
        ok:
          type: boolean
      additionalProperties: true
    ErrorBody:
      type: object
      properties:
        ok:
          type: boolean
          example: false
        error:
          type: string
        code:
          type: string
          description: Machine-readable code (connect_grokbot, needs_key, free_exhausted,
            auth_required, unknown_template, oauth_unconfigured, …)
        detail:
          type: string
      additionalProperties: true
    HealthResponse:
      type: object
      properties:
        ok:
          type: boolean
        service:
          type: string
          example: brandlit-studio
        protocol:
          type: string
          example: brandlit-canvas-room/1
        designedFor:
          type: string
          example: Grok
        vault:
          type: boolean
        platformBrain:
          type: boolean
        inference:
          type: string
        billing:
          type: string
          description: e.g. platform_api
        stripe:
          type: boolean
        stripeWebhook:
          type: boolean
        payg:
          type: object
          properties:
            freeSeconds:
              type: number
              nullable: true
            freeGrokOps:
              type: number
            centsPerMinute:
              type: number
        money:
          type: object
          description: Pricing pointers. For the current catalog use GET /api/billing/products.
          additionalProperties: true
        hosting:
          type: object
          additionalProperties: true
        site:
          type: object
          properties:
            mode:
              type: string
              example: proxy
            origin:
              type: string
            path:
              type: string
        oneButtonGrokBot:
          type: boolean
        browserApprove:
          type: boolean
      additionalProperties: true
    ModeResponse:
      type: object
      properties:
        ok:
          type: boolean
        grok:
          type: string
        hasKey:
          type: boolean
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
        brainMode:
          type: string
          example: needs_grokbot
        designedFor:
          type: string
        site:
          type: string
        siteOrigin:
          type: string
        product:
          type: string
        oneButtonGrokBot:
          type: boolean
    AuthSessionResponse:
      type: object
      properties:
        ok:
          type: boolean
        uid:
          type: string
        email:
          type: string
        method:
          type: string
        usage:
          $ref: '#/components/schemas/UsagePublic'
        message:
          type: string
    AuthMeResponse:
      type: object
      properties:
        ok:
          type: boolean
        authed:
          type: boolean
        guest:
          type: boolean
        email:
          type: string
          nullable: true
        uid:
          type: string
        usage:
          $ref: '#/components/schemas/UsagePublic'
        hasKey:
          type: boolean
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
        brainMode:
          type: string
        authMethod:
          type: string
      additionalProperties: true
    UsagePublic:
      type: object
      additionalProperties: true
      properties:
        secondsUsed:
          type: number
        grokOpsUsed:
          type: number
        freeGrokOps:
          type: number
        freeLeftOps:
          type: number
        gated:
          type: boolean
        tier:
          type: string
        centsPerMinute:
          type: number
        priceLabel:
          type: string
    GrokBotStartResponse:
      type: object
      properties:
        ok:
          type: boolean
        flow:
          type: string
          example: device_code
        device_code:
          type: string
        user_code:
          type: string
        verification_uri:
          type: string
        verification_uri_complete:
          type: string
        grokbot_deep_link:
          type: string
        interval:
          type: integer
          example: 2
        expires_in:
          type: integer
          example: 600
        message:
          type: string
        oauthReady:
          type: boolean
    GrokBotPollResponse:
      type: object
      properties:
        ok:
          type: boolean
        status:
          type: string
          enum:
          - pending
          - approved
          - denied
          - expired
        user_code:
          type: string
        message:
          type: string
        uid:
          type: string
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
        brainMode:
          type: string
        plan:
          type: string
    GrokBotStatusResponse:
      type: object
      properties:
        ok:
          type: boolean
        uid:
          type: string
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
        platformBrainConfigured:
          type: boolean
        brainMode:
          type: string
        link:
          type: object
          nullable: true
          additionalProperties: true
    GrokSuccessResponse:
      type: object
      properties:
        ok:
          type: boolean
        ops:
          type: array
          items:
            type: object
            additionalProperties: true
        speak:
          type: string
          nullable: true
        model:
          type: string
        mode:
          type: string
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
        billing:
          type: string
          description: Present on some live builds
    GrokTestResponse:
      type: object
      properties:
        ok:
          type: boolean
        tested:
          type: boolean
        model:
          type: string
        hint:
          type: string
        mode:
          type: string
        grokBotLinked:
          type: boolean
        hasGrokBrain:
          type: boolean
    Project:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        mode:
          type: string
        templateId:
          type: string
          nullable: true
        owner:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        published:
          type: boolean
        previewUrl:
          type: string
        siteUrl:
          type: string
        siteHost:
          type: string
        brandKit:
          type: object
          additionalProperties: true
        domain:
          type: string
          nullable: true
    ProjectListResponse:
      type: object
      properties:
        ok:
          type: boolean
        projects:
          type: array
          items:
            $ref: '#/components/schemas/Project'
        total:
          type: integer
        nextOffset:
          type: integer
          nullable: true
    ProjectCreateResponse:
      type: object
      properties:
        ok:
          type: boolean
        project:
          $ref: '#/components/schemas/Project'
    ProjectGetResponse:
      type: object
      properties:
        ok:
          type: boolean
        project:
          $ref: '#/components/schemas/Project'
        html:
          type: string
          nullable: true
        ops:
          type: array
          nullable: true
          items:
            type: object
            additionalProperties: true
          description: Last ops batch saved via save/PUT (null if never saved).
        sidecar:
          type: object
          nullable: true
          additionalProperties: true
          description: Free-form client metadata saved via save/PUT `sidecar` (≤64
            KB JSON). null if none.
        assets:
          type: array
          items:
            $ref: '#/components/schemas/ProjectAsset'
        assetsTruncated:
          type: boolean
        assetsTotalBytes:
          type: integer
        schema:
          type: string
          example: brandlit-web/1+ext1
    ProjectSaveBody:
      type: object
      properties:
        html:
          type: string
        ops:
          type: array
          items:
            type: object
            additionalProperties: true
        name:
          type: string
        brandKit:
          type: object
        published:
          type: boolean
        mode:
          type: string
        sidecar:
          type: object
          nullable: true
          additionalProperties: true
          description: Optional client metadata (city, phone, logoPath, …). Object
            replaces the stored sidecar; null deletes it; omit to leave unchanged.
            Max 64 KB JSON.
    ProjectPublishResponse:
      type: object
      properties:
        ok:
          type: boolean
        project:
          $ref: '#/components/schemas/Project'
        publicUrl:
          type: string
        pathUrl:
          type: string
        siteUrl:
          type: string
        customUrl:
          type: string
          nullable: true
        byoCnameTarget:
          type: string
    DomainInstructionsResponse:
      type: object
      properties:
        ok:
          type: boolean
        buy:
          type: string
        registrar:
          type: boolean
        previewUrl:
          type: string
        siteUrl:
          type: string
        byo:
          type: object
          additionalProperties: true
    DomainSearchResponse:
      type: object
      properties:
        ok:
          type: boolean
        query:
          type: string
        domains:
          type: array
          items:
            type: object
            additionalProperties: true
    BillingProductsResponse:
      type: object
      description: Live product catalog
      properties:
        ok:
          type: boolean
        currency:
          type: string
        products:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              unit_amount:
                type: integer
                description: Cents
              mode:
                type: string
              interval:
                type: string
              description:
                type: string
        freeGrokSeconds:
          type: number
        editorUnlimited:
          type: boolean
        freeSeconds:
          type: number
          nullable: true
        perApp:
          type: boolean
    TemplatesResponse:
      type: object
      properties:
        ok:
          type: boolean
        templates:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              category:
                type: string
              line:
                type: string
              palette:
                type: string
              pages:
                type: integer
              preview:
                type: string
    ProjectAsset:
      type: object
      properties:
        path:
          type: string
          example: logos/brand.png
        url:
          type: string
          format: uri
          description: Stable URL https://brandlit.blacklabelbots.com/project-assets/{projectId}/{path}.
            Public once the project is published; owner-only (cookie) before.
        size:
          type: integer
        contentType:
          type: string
        uploadedAt:
          type: string
          format: date-time
        etag:
          type: string
    AssetListResponse:
      type: object
      properties:
        ok:
          type: boolean
        projectId:
          type: string
        assets:
          type: array
          items:
            $ref: '#/components/schemas/ProjectAsset'
        truncated:
          type: boolean
        totalBytes:
          type: integer
        count:
          type: integer
        limits:
          type: object
          properties:
            maxFileBytes:
              type: integer
              example: 10485760
            maxFilesPerRequest:
              type: integer
              example: 20
            maxObjects:
              type: integer
              example: 200
            maxProjectBytes:
              type: integer
              example: 52428800
    AssetUploadResponse:
      type: object
      properties:
        ok:
          type: boolean
        projectId:
          type: string
        uploaded:
          type: array
          items:
            $ref: '#/components/schemas/ProjectAsset'
