openapi: 3.1.0
info:
  title: CookieComply Public API
  version: "1.4.0"
  description: |
    Public API for CookieComply workspaces — for MCP-compatible agents,
    coding assistants, scripts, and CI.

    Authenticate with `Authorization: Bearer <access_token>` from
    Settings → Connect AI & automation (Professional+).

    Full-access tokens can run analyses. Read-only practice tokens can list
    and export existing scans but cannot call analyze, cookie research, or ask-ai.

    Permissions (scopes) control what a token may do — view scans, run analyses,
    export reports, see usage, manage members.

    Capture cookies in a browser (Chrome add-on, Playwright, Browser Use), then
    `POST /analyze` with before/after cookie lists. CookieComply does not
    browse URLs itself.

    Human guide: https://cookie-comply.com/integrations
    Agent playbook: https://cookie-comply.com/skills/cookiecomply/SKILL.md
  contact:
    name: CookieComply
    url: https://cookie-comply.com
servers:
  - url: https://cookie-comply.com/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local
security:
  - bearerAuth: []
tags:
  - name: Scans
  - name: Analyze
  - name: Reports
  - name: Ask AI
  - name: Usage
  - name: Cookies
  - name: Workspace
  - name: Members
paths:
  /scans:
    get:
      operationId: listScans
      tags: [Scans]
      summary: List workspace scans
      description: Requires `scans:read`. Invalid page/limit return 400.
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
        - name: status
          in: query
          schema: { type: string }
        - name: q
          in: query
          description: URL search
          schema: { type: string }
      responses:
        "200":
          description: Paginated scans
          content:
            application/json:
              schema:
                type: object
                properties:
                  scans:
                    type: array
                    items: { $ref: "#/components/schemas/ScanSummary" }
                  totalCount: { type: integer }
                  page: { type: integer }
                  limit: { type: integer }
        "400":
          description: Invalid pagination
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /scans/{id}:
    get:
      operationId: getScan
      tags: [Scans]
      summary: Get scan with findings/report JSON
      description: Requires `scans:read`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Full scan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Scan" }
        "404":
          description: Not found
  /scans/{id}/export:
    get:
      operationId: exportScan
      tags: [Reports]
      summary: Export scan report (CSV, JSON, Markdown, or PDF)
      description: |
        Requires `reports:read`. Formats: `csv`, `json`, `md`, `pdf`.
        PDF is a lightweight text report generated server-side (not a browser print).
        Use `GET /scans/{id}` for raw findings JSON (`scans:read`).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: format
          in: query
          schema:
            type: string
            enum: [csv, json, md, pdf]
            default: csv
      responses:
        "200":
          description: Report file body
          content:
            text/csv: { schema: { type: string } }
            application/json: { schema: { type: object } }
            text/markdown: { schema: { type: string } }
            application/pdf:
              schema: { type: string, format: binary }
        "400":
          description: Invalid format
        "404":
          description: Not found
  /scans/{id}/ask-ai:
    post:
      operationId: askAi
      tags: [Ask AI]
      summary: Ask AI a question about a scan report
      description: |
        Requires `scans:read` and a **live** (`cc_live_`) key.
        Test keys return 403 `test_key_not_allowed` (Vertex burn).
        Subject to per-scan question limits by plan.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userQuestion]
              properties:
                userQuestion: { type: string }
      responses:
        "200":
          description: AI answer
          content:
            application/json:
              schema:
                type: object
                properties:
                  answer: { type: string }
                  questionCount: { type: integer }
                  messages: { type: array, items: { type: object } }
        "403":
          description: Limit or tier
        "404":
          description: Not found
  /analyze:
    post:
      operationId: analyzeCookies
      tags: [Analyze]
      summary: Submit cookie capture payload for analysis (consumes one scan)
      description: |
        Requires `scans:write` and a **live** (`cc_live_`) key.
        Returns 403 `no_scans` when balance is empty;
        403 `test_key_not_allowed` for `cc_test_` keys.
      parameters:
        - name: Idempotency-Key
          in: header
          schema: { type: string }
          description: Optional; concurrent retries share one claim (24h)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AnalyzeRequest" }
      responses:
        "200":
          description: Analysis result
        "400":
          description: Invalid payload
        "403":
          description: No scans remaining (`no_scans`) or not entitled
        "409":
          description: Idempotency conflict still in progress
  /usage:
    get:
      operationId: getUsage
      tags: [Usage]
      summary: Remaining scans and subscription summary
      description: |
        Requires `billing:read`. Safe read surface only — Stripe Customer Portal
        stays behind the authenticated dashboard (`billingPortal: dashboard_only`).
      responses:
        "200":
          description: Usage
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Usage" }
  /workspace:
    get:
      operationId: getWorkspace
      tags: [Workspace]
      summary: Workspace metadata for the API key
      description: Requires `workspace:read`.
      responses:
        "200":
          description: Workspace
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Workspace" }
  /members:
    get:
      operationId: listMembers
      tags: [Members]
      summary: List workspace members and pending invitations
      description: Requires `members:read` (opt-in; not in default key scopes).
      responses:
        "200":
          description: Members and invitations
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MembersList" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /members/invitations:
    post:
      operationId: inviteMember
      tags: [Members]
      summary: Invite a member by email
      description: |
        Requires `members:write`. Role is always MEMBER.
        Returns `acceptUrl` so callers can share the link if email delivery is skipped.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
                locale: { type: string }
      responses:
        "200":
          description: Invitation created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InviteResult" }
        "400":
          description: Invalid email
        "409":
          description: Already a member
  /members/invitations/{id}:
    delete:
      operationId: revokeInvitation
      tags: [Members]
      summary: Revoke a pending invitation
      description: Requires `members:write`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "404":
          description: Not found
  /members/{memberId}:
    delete:
      operationId: removeMember
      tags: [Members]
      summary: Remove a workspace member
      description: Requires `members:write`. Cannot remove the workspace owner.
      parameters:
        - name: memberId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400":
          description: Cannot remove owner
        "404":
          description: Not found
  /cookies/research:
    post:
      operationId: researchCookies
      tags: [Cookies]
      summary: Smart Cookie Review research for unknown cookies
      description: |
        Requires `scans:write` and a **live** (`cc_live_`) key.
        Vertex research is capped per request (batch size) and rate-limited
        per workspace (~30 batches/hour).
        Returns 429 `research_rate_limited` when exhausted;
        403 `test_key_not_allowed` for `cc_test_` keys.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cookies]
              properties:
                cookies:
                  type: array
                  maxItems: 40
                  items:
                    type: object
                    required: [name, domain]
                    properties:
                      name: { type: string }
                      domain: { type: string }
      responses:
        "200":
          description: Research results
        "429":
          description: Research rate limited (`research_rate_limited`)
  /cookies/confirm:
    post:
      operationId: confirmCookies
      tags: [Cookies]
      summary: Confirm cookie categories (learns patterns)
      description: |
        Requires `scans:write`. Allowed with `cc_test_` keys (no Vertex burn).
        Optional Idempotency-Key.
      parameters:
        - name: Idempotency-Key
          in: header
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [confirmations]
              properties:
                confirmations:
                  type: array
                  items:
                    type: object
                    required: [name, domain, category]
                    properties:
                      name: { type: string }
                      domain: { type: string }
                      category:
                        type: string
                        enum: [Essential, Functional, Analytics, Marketing, Preference]
                      previousCategory: { type: string }
      responses:
        "200":
          description: Confirmations recorded
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        API key (`cc_live_…` or `cc_test_…`).
        Test keys read live workspace data but cannot call analyze,
        cookie research, or ask-ai (`test_key_not_allowed`).
  responses:
    Unauthorized:
      description: Missing or invalid API key (`unauthorized`)
    Forbidden:
      description: |
        Insufficient scope (`insufficient_scope`), plan not entitled
        (`api_not_entitled`), no scans remaining (`no_scans`), or test key
        blocked on live-only routes (`test_key_not_allowed`)
  schemas:
    ScanSummary:
      type: object
      properties:
        id: { type: string }
        url: { type: string }
        status: { type: string }
        createdAt: { type: string, format: date-time }
    Scan:
      allOf:
        - $ref: "#/components/schemas/ScanSummary"
        - type: object
          properties:
            results: { type: object }
            analysisId: { type: string, nullable: true }
            beforeData: { type: object, nullable: true }
            afterData: { type: object, nullable: true }
            selectedCategories: { type: object, nullable: true }
    AnalyzeRequest:
      type: object
      required: [url, cookies]
      properties:
        url: { type: string, format: uri }
        locale: { type: string }
        analysisId: { type: string }
        clientResolved: { type: boolean }
        manualCategorization: { type: boolean }
        cookies:
          type: object
          description: >
            Same conceptual shape as the Chrome extension: cookies before
            consent and after Accept. Agents using Playwright should send
            context.cookies() mapped to CookieCaptureItem.
          required: [before]
          properties:
            before:
              type: array
              items: { $ref: "#/components/schemas/CookieCaptureItem" }
            after:
              type: array
              items: { $ref: "#/components/schemas/CookieCaptureItem" }
            localStorage:
              type: array
              items: { $ref: "#/components/schemas/StorageItem" }
            sessionStorage:
              type: array
              items: { $ref: "#/components/schemas/StorageItem" }
    CookieCaptureItem:
      type: object
      required: [name, domain]
      properties:
        name: { type: string }
        domain: { type: string }
        value: { type: string }
        path: { type: string }
        secure: { type: boolean }
        httpOnly: { type: boolean }
        sameSite: { type: string }
        expirationDate: { type: number }
        session: { type: boolean }
    StorageItem:
      type: object
      properties:
        key: { type: string }
        value: { type: string }
    Usage:
      type: object
      properties:
        scansRemaining: { type: integer, description: "-1 means unlimited" }
        tier: { type: string, nullable: true }
        billingPeriod: { type: string, nullable: true }
        subscriptionStatus: { type: string, nullable: true }
        billingPortal:
          type: string
          enum: [dashboard_only]
          description: Stripe portal is not available via API keys
        workspace:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            slug: { type: string }
    Workspace:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        slug: { type: string }
        scansRemaining: { type: integer }
        tier: { type: string, nullable: true }
    MembersList:
      type: object
      properties:
        workspace:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            slug: { type: string }
        members:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              role: { type: string }
              user:
                type: object
                properties:
                  id: { type: string }
                  name: { type: string }
                  email: { type: string }
        invitations:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              email: { type: string }
              role: { type: string }
              expiresAt: { type: string, format: date-time }
    InviteResult:
      type: object
      properties:
        ok: { type: boolean }
        invitation:
          type: object
          properties:
            id: { type: string }
            email: { type: string }
            role: { type: string }
            expiresAt: { type: string, format: date-time }
            acceptUrl: { type: string }
        email:
          type: object
          properties:
            sent: { type: boolean }
            configured: { type: boolean }
            skipped: { type: boolean }
