openapi: 3.1.0
info:
  title: CanvasBuilder Agent API
  version: "1.0.0"
  description: >
    Programmatic website building for AI agents. An agent links to a user's
    CanvasBuilder account via a QR/device-link consent flow, receives a scoped
    bearer token, then builds, refines, publishes, and downloads sites on the
    user's behalf. Builds consume the user's credits.
  contact:
    name: CanvasBuilder
    url: https://canvasbuilder.co
servers:
  - url: https://canvasbuilder.co/api/v1/agents
    description: Production
security:
  - bearerAuth: []
paths:
  /link/start:
    post:
      summary: Begin a device-link request
      description: >
        Public. Returns a short code, a link URL, and a QR image URL. Show the
        QR/link to the user; they approve it in their CanvasBuilder account.
        Then poll /link/poll until a token is issued.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentName]
              properties:
                agentName:
                  type: string
                  example: "Acme Assistant"
                scopes:
                  type: array
                  items:
                    type: string
                    enum: [sites:build, sites:publish, sites:domain]
                  default: [sites:build]
      responses:
        "200":
          description: Link request created
          content:
            application/json:
              schema:
                type: object
                properties:
                  code: { type: string, example: "WXYZ-1234" }
                  linkUrl: { type: string, format: uri }
                  qrUrl: { type: string, format: uri }
                  scopes: { type: array, items: { type: string } }
                  expiresAt: { type: string, format: date-time }
  /link/poll:
    post:
      summary: Poll for the issued token
      description: >
        Public. Call repeatedly after /link/start. While the user has not yet
        approved, returns { status: "pending" }. Once approved, returns the
        plaintext token EXACTLY ONCE — store it securely; it cannot be
        retrieved again.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "200":
          description: Status (and token once approved)
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    properties:
                      status: { type: string, enum: [pending] }
                  - type: object
                    properties:
                      status: { type: string, enum: [approved] }
                      accessToken: { type: string, example: "cb_agent_..." }
                      scopes: { type: array, items: { type: string } }
                      expiresAt: { type: string, format: date-time }
  /me:
    get:
      summary: Token identity + credit balance
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  userId: { type: string }
                  scopes: { type: array, items: { type: string } }
                  creditBalance: { type: number }
                  tokenName: { type: string }
                  expiresAt: { type: string, format: date-time }
  /sites:
    get:
      summary: List the user's sites
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  sites:
                    type: array
                    items:
                      type: object
                      properties:
                        siteId: { type: string }
                        title: { type: string }
                        updatedAt: { type: string, format: date-time }
    post:
      summary: Start a new build
      description: >
        Requires scope sites:build. Returns immediately with a siteId and
        status "building". Poll GET /sites/{id} until status is "complete".
        Consumes credits based on the size/complexity of what you build.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description:
                  type: string
                  maxLength: 8000
                  example: "A landing page for a coffee subscription startup."
                title: { type: string }
                brandKitId: { type: string }
      responses:
        "202":
          description: Build started
          content:
            application/json:
              schema:
                type: object
                properties:
                  siteId: { type: string }
                  status: { type: string, enum: [building] }
        "402": { description: Insufficient credits }
  /sites/{id}:
    get:
      summary: Build status, live URL, and preview HTML
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  siteId: { type: string }
                  status: { type: string, enum: [building, complete, error] }
                  title: { type: string }
                  liveUrl: { type: string, format: uri }
                  previewHtml: { type: string }
                  updatedAt: { type: string, format: date-time }
  /sites/{id}/changes:
    post:
      summary: Refine an existing site
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [description]
              properties:
                description: { type: string, maxLength: 8000 }
      responses:
        "202":
          description: Change started
          content:
            application/json:
              schema:
                type: object
                properties:
                  siteId: { type: string }
                  status: { type: string, enum: [building] }
  /sites/{id}/publish:
    post:
      summary: Publish to a cnvs.site subdomain
      description: Requires scope sites:publish.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [subdomain]
              properties:
                subdomain: { type: string, example: "acme-coffee" }
                password: { type: string }
                showBadge: { type: boolean }
      responses:
        "200":
          description: Published
          content:
            application/json:
              schema:
                type: object
                properties:
                  siteId: { type: string }
                  publishedSiteId: { type: string }
                  subdomain: { type: string }
                  url: { type: string, format: uri }
                  pages: { type: integer }
  /sites/{id}/domain:
    post:
      summary: Attach a custom domain
      description: >
        Requires scope sites:domain and a paid plan. The site must already be
        published. Returns DNS instructions; SSL + activation happen
        automatically once DNS points to us.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [hostname]
              properties:
                hostname: { type: string, example: "shop.acme.com" }
      responses:
        "200":
          description: Domain pending DNS
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: { type: object }
                  instructions: { type: object }
                  message: { type: string }
  /sites/{id}/download:
    get:
      summary: Download the site as a .zip
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: minify, in: query, required: false, schema: { type: string, enum: ["0", "false", "no"] } }
      responses:
        "200":
          description: ZIP archive
          content:
            application/zip:
              schema: { type: string, format: binary }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Use the token from /link/poll as `Authorization: Bearer cb_agent_...`.
        Tokens are scoped and expire after 90 days. Builds consume the linked
        user's credits.
