openapi: 3.1.0
info:
  title: Cuppa API
  version: 1.0.0
  description: |
    Use the Cuppa API to generate content and images using AI. The capabilities of the API are defined in the next sections.

    ### Interfaces

    The Cuppa API can be accessed three ways:

    - **REST API** (this spec): Direct HTTP calls for custom integrations, automation platforms (Zapier, Make), and ChatGPT Actions.
    - **CLI**: `npm install -g @cuppa-ai/cli` for terminal workflows, AI agents with shell access (Claude Code, Cursor), and scripting. 75+ commands including high-level workflows like `cuppa generate`, `cuppa optimize`, `cuppa campaign`, and `cuppa seo`.
    - **MCP Server**: `npx @cuppa-ai/mcp-server` for AI editors (Cursor, Claude Desktop, Windsurf). 75+ tools auto-discovered by MCP-compatible clients.

    All three use the same API key and write to the same database. Content created via any interface is visible in the Cuppa web app.

    ### Authentication
    To use the API, you need to authenticate using an API key. Go to your Cuppa [settings](https://app.cuppa.ai/team/settings/api/keys) to obtain an API key. Include the API key in the `X-API-KEY` header of your requests as follows:

    ```
    curl -X GET "https://api.cuppa.ai/v1/meta" \
      -H "accept: application/json" \
      -H "X-API-KEY: YOUR_API_KEY"
    ```

    For the CLI, authenticate with `cuppa auth login` or set the `CUPPA_API_KEY` environment variable.

    ### Rate Limiting
    The API has rate limits in place to ensure fair usage.

    ### Error Handling
    The API returns standard HTTP status codes to indicate the success or failure of a request. In case of an error, the response will contain an error object with details about the error.

servers:
  - url: https://api.cuppa.ai

security:
  - ApiKeyAuth: []

tags:
  - name: Metadata
    description: API metadata and available options
  - name: Contents
    description: Create and manage articles
  - name: Briefs
    description: Parse content briefs into structured article parameters
  - name: Projects
    description: Bulk content generation projects
  - name: Sites
    description: Site/brand management and Brand DNA
  - name: Images
    description: AI image generation
  - name: Presets
    description: Content generation presets
  - name: Knowledge
    description: Knowledge sources for RAG
  - name: Clusters
    description: Keyword cluster management
  - name: Planner
    description: Content planning and scheduling
  - name: Chat
    description: Chat with Cuppa AI research agent
  - name: Social
    description: Social media content generation and publishing
  - name: Links
    description: Link Engine for internal linking
  - name: Research
    description: Keyword and competitor research
  - name: Templates
    description: Custom content templates (Workbench)
  - name: Agents
    description: Research agents for automated content research
  - name: Brand Notebook
    description: Client notes and meeting transcripts
  - name: Video
    description: AI video generation (scripts, generation, status)
  - name: Mood Board
    description: Visual inspiration mood boards

webhooks:
  contentGenerationCompleted:
    post:
      summary: Content Generation Completed
      description: |
        This webhook is triggered when the content generation is completed. It provides the generated content and its status.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [content.generation_completed]
                data:
                  $ref: '#/components/schemas/Content'

      responses:
        '200':
          description: Acknowledge receipt of the webhook

  contentGenerationFailed:
    post:
      summary: Content Generation Failed
      description: |
        This webhook is triggered when the content generation fails. It provides the error details.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [content.generation_failed]
                data:
                  $ref: '#/components/schemas/ContentStatusResponse'

      responses:
        '200':
          description: Acknowledge receipt of the webhook

  projectExportCompleted:
    post:
      summary: Project Export Completed
      description: |
        This webhook is triggered when a bulk export to an external system (e.g., Airtable) completes.
        It provides details about the export including the number of articles exported and any errors.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [project.export_completed]
                data:
                  type: object
                  properties:
                    project_id:
                      type: string
                      description: The ID of the exported project
                    destination:
                      type: string
                      enum: [airtable]
                      description: The export destination
                    total_articles:
                      type: integer
                      description: Total number of articles that were queued for export
                    exported_articles:
                      type: integer
                      description: Number of articles successfully exported
                    status:
                      type: string
                      enum: [complete, failed]
                      description: Overall status of the export
                    errors:
                      type: array
                      items:
                        type: string
                      description: List of error messages if any

      responses:
        '200':
          description: Acknowledge receipt of the webhook

paths:
  /v1/meta:
    get:
      summary: Get all metadata
      responses:
        '200':
          description: Metadata object
          content:
            application/json:
              schema:
                type: object
                properties:
                  languages:
                    type: array
                    items:
                      type: object
                      required:
                        - name
                        - code
                      properties:
                        name:
                          type: string
                        code:
                          type: string
                  regions:
                    type: array
                    items:
                      type: object
                      required:
                        - name
                        - code
                      properties:
                        name:
                          type: string
                        code:
                          type: string
                  models:
                    type: array
                    items:
                      type: string
                  image_models:
                    type: array
                    items:
                      type: string
                required:
                  - languages
                  - regions
                  - models
                  - image_models
      tags:
        - Metadata
  /v1/meta/models:
    get:
      summary: Get available models
      responses:
        '200':
          description: A list of model names
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
      tags:
        - Metadata
  /v1/meta/image_models:
    get:
      summary: Get available image models
      description: |
        Returns all available image generation models. Models are grouped by provider:

        **OpenAI models** (require OpenAI API key):
        - `gpt-image-1`: GPT Image 1, photorealistic quality. Sizes: 1024x1024, 1536x1024, 1024x1536

        **Replicate models** (require Replicate API key):
        - `black-forest-labs/flux-1.1-pro-ultra`: Flux 1.1 Pro Ultra, highest quality Flux model
        - `black-forest-labs/flux-1.1-pro`: Flux 1.1 Pro, high quality with custom dimensions
        - `black-forest-labs/flux-dev`: Flux Dev, fast iteration
        - `black-forest-labs/flux-pro`: Flux Pro, balanced quality and speed
        - `black-forest-labs/flux-schnell`: Flux Schnell, fastest Flux model
        - `stability-ai/stable-diffusion-3`: Stable Diffusion 3
        - `replicate:nano-banana-pro`: Nano Banana Pro, supports 1K/2K/4K resolution output
        - `replicate:imagen-4`: Google Imagen 4 via Replicate

        **Google models** (require Google AI API key):
        - `imagen-4.0-generate-001`: Google Imagen 4, native Google API
      responses:
        '200':
          description: A list of image model names
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string
                example:
                  - gpt-image-1
                  - black-forest-labs/flux-1.1-pro-ultra
                  - black-forest-labs/flux-1.1-pro
                  - black-forest-labs/flux-dev
                  - black-forest-labs/flux-pro
                  - black-forest-labs/flux-schnell
                  - stability-ai/stable-diffusion-3
                  - replicate:nano-banana-pro
                  - replicate:imagen-4
                  - imagen-4.0-generate-001
      tags:
        - Metadata
  /v1/meta/languages:
    get:
      summary: Get available languages
      responses:
        '200':
          description: A list of supported languages
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - name
                    - code
                  properties:
                    name:
                      type: string
                    code:
                      type: string
      tags:
        - Metadata
  /v1/meta/regions:
    get:
      summary: Get available regions
      responses:
        '200':
          description: A list of supported regions
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - name
                    - code
                  properties:
                    name:
                      type: string
                    code:
                      type: string
      tags:
        - Metadata

  /v1/images:
    get:
      summary: Fetch list of generated images
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            maximum: 50
          description: Maximum number of images to return (max 50, default 20)
      responses:
        '200':
          description: A list of generated images
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                    - id
                    - url
                  properties:
                    id:
                      type: string
                    url:
                      type: string
      tags:
        - Images
    post:
      summary: Create an image
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - model
                - prompt
              properties:
                model:
                  type: string
                  description: |
                    The model to use for image generation. Use the `/v1/meta/image_models` endpoint to get the full list.
                    OpenAI models use the `size` setting. Replicate/Google models use the `aspect_ratio` setting.
                  example: gpt-image-1
                prompt:
                  type: string
                  maxLength: 1000
                  description: The prompt to use for image generation
                settings:
                  type: object
                  description: Image generation settings. Use `size` for OpenAI models, `aspect_ratio` for Replicate/Google models.
                  properties:
                    size:
                      type: string
                      enum: ['1024x1024', '1536x1024', '1024x1536']
                      default: '1536x1024'
                      description: |
                        Pixel size of the image. Valid for OpenAI models (gpt-image-1) only.
                        1024x1024 (square), 1536x1024 (landscape), 1024x1536 (portrait).
                    aspect_ratio:
                      type: string
                      enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '5:4', '4:5', '21:9', '9:21']
                      default: '16:9'
                      description: |
                        Aspect ratio of the image. Valid for Replicate and Google models.
                        Not all ratios are supported by all models. Common options: 16:9 (landscape), 9:16 (portrait/stories), 1:1 (square), 4:3 (standard), 3:2 (classic).
                    resolution:
                      type: string
                      enum: ['1K', '2K', '4K']
                      description: |
                        Output resolution. Only valid for `replicate:nano-banana-pro`.
                        1K: Fast, good for previews. 2K: Balanced (default). 4K: Highest quality, slower.
      responses:
        '200':
          description: Successfully generated image
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  url:
                    type: string
      tags:
        - Images

  /v1/contents:
    get:
      summary: Fetch list of articles
      parameters:
        - in: query
          name: page
          schema:
            type: integer
            nullable: true
          description: Page number for pagination
        - in: query
          name: lang
          schema:
            type: string
            nullable: true
          description: Filter by language code
        - in: query
          name: project
          schema:
            type: string
            nullable: true
          description: Filter by project ID. If not provided, it will return the articles not part of any project. Use `all` to get all articles.
        - in: query
          name: site
          schema:
            type: string
            format: uuid
            nullable: true
          description: Filter by site/brand ID. Use `GET /v1/sites` to get available site IDs.
      responses:
        '200':
          description: A list of articles
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Content'
      tags:
        - Contents

    post:
      summary: Create a new article
      description: |
        Create content using different templates:
        - **article** (default): Full-featured articles with support for listicles, reviews, how-tos. Use `settings.article_type` to specify.
        - **local_news**: Location-focused news articles. Simpler settings, uses `local_news_settings`.
        - **recipe**: Structured recipe content with ingredients, instructions. Uses `recipe_settings`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - target_keyword
              properties:
                target_keyword:
                  type: string
                  description: The target keyword for the content.
                template:
                  type: string
                  enum: [article, local_news, recipe]
                  default: article
                  description: |
                    The content template to use:
                    - `article`: General articles with full settings (listicle, review, howto, general)
                    - `local_news`: Location-focused news content
                    - `recipe`: Structured recipe with ingredients and instructions
                site_id:
                  type: string
                  format: uuid
                  nullable: true
                  description: Optional site/brand ID. When provided, enables Brand Voice, site context, target audience, and Link Engine. Pass null to explicitly create content without any brand context (Organization Mode). When omitted, auto-assigns your default site.
                model:
                  type: string
                  description: The AI model to use.
                  default: gpt-4o-mini
                outline:
                  type: array
                  description: Optional outline (only for template=article).
                  items:
                    oneOf:
                      - type: object
                        required: [type, level, title]
                        properties:
                          type:
                            type: string
                            enum: [heading]
                          level:
                            type: integer
                            enum: [2, 3, 4]
                          title:
                            type: string
                      - type: object
                        required: [type, code]
                        properties:
                          type:
                            type: string
                            enum: [custom_code]
                          name:
                            type: string
                            nullable: true
                          code:
                            type: string
                      - type: object
                        required: [type, code]
                        properties:
                          type:
                            type: string
                            enum: [shortcode]
                          name:
                            type: string
                            nullable: true
                          code:
                            type: string
                settings_preset:
                  type: number
                  description: Preset ID (only for template=article).
                  nullable: true
                settings:
                  $ref: '#/components/schemas/ContentSettings'
                  description: Article settings (only for template=article).
                local_news_settings:
                  type: object
                  description: Settings for template=local_news.
                  properties:
                    target_audience:
                      type: string
                      maxLength: 500
                      description: Target audience for the local news article.
                    extra_prompt:
                      type: string
                      maxLength: 50000
                      description: Additional AI instructions.
                recipe_settings:
                  type: object
                  description: Settings for template=recipe.
                  properties:
                    recipe_template:
                      type: string
                      enum: [original, card, minimal, modern, elegant, magazine, vintage, compact]
                      default: modern
                      description: Display template for the recipe.
                use_serp:
                  type: boolean
                  description: When true, fetches top SERP competitors before generation and uses their content as context to write a more competitive article. Requires a Power plan or higher.
                  default: false
                is_draft:
                  type: boolean
                  description: Whether to save as draft (won't generate immediately)
                  default: false
      responses:
        '200':
          description: Content created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentStatusResponse'
      tags:
        - Contents

  /v1/contents/import:
    post:
      summary: Import article from URL
      description: Import an existing article by URL. Scrapes the page and creates a Cuppa article.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
              properties:
                url:
                  type: string
                  description: URL to import
                language:
                  type: string
                  default: en
                  description: Language code
                site_id:
                  type: string
                  format: uuid
                  description: Site to associate with
      responses:
        '201':
          description: Article imported
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Created article ID
                  status:
                    type: string
                    enum: [imported]
                  url:
                    type: string
                    description: Original URL
      tags:
        - Contents

  /v1/briefs:
    post:
      summary: Parse a content brief
      description: |
        Parse a content brief (any format: formal document, bullet points, or narrative) into structured article generation parameters.
        Returns extracted outline, keywords, tone, audience, content gaps, and instructions that can be used with POST /v1/contents.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              properties:
                content:
                  type: string
                  description: The raw content brief text to parse (minimum 50 characters)
                model:
                  type: string
                  description: AI model for parsing (e.g. gpt-5-mini, claude-sonnet-4-5). Defaults to team default.
      responses:
        '200':
          description: Brief parsed successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                    description: Target article title or H1
                  target_keyword:
                    type: string
                    description: Primary target keyword
                  secondary_keywords:
                    type: array
                    items:
                      type: string
                    description: Secondary keywords
                  word_count:
                    type: integer
                    description: Target word count
                  tone:
                    type: string
                    description: Tone/style directive
                  target_audience:
                    type: string
                    description: Target audience description
                  article_type:
                    type: string
                    enum: [general, listicle, review, howto]
                  outline:
                    type: array
                    items:
                      type: object
                      properties:
                        level:
                          type: integer
                          description: Heading level (2 for H2, 3 for H3)
                        title:
                          type: string
                          description: Heading text
                        instructions:
                          type: string
                          description: Section-specific instructions
                  content_gaps:
                    type: array
                    items:
                      type: string
                    description: Required differentiation points
                  paa_questions:
                    type: array
                    items:
                      type: string
                    description: People Also Ask questions
                  extra_instructions:
                    type: string
                    description: Catch-all for other editorial rules and requirements
      tags:
        - Briefs

  /v1/contents/{id}:
    get:
      summary: Get an article by ID
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The ID of the article
      responses:
        '200':
          description: The article object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Content'
      tags:
        - Contents
    patch:
      summary: Update an article
      description: Update title, HTML content, meta description, or target keyword of an existing article. At least one field is required. Content updates persist to the article body and are reflected in subsequent GET requests.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: Article title
                content:
                  type: string
                  description: HTML content
                meta_description:
                  type: string
                  description: SEO meta description
                target_keyword:
                  type: string
                  description: Target keyword
      responses:
        '200':
          description: The updated article
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Content'
      tags:
        - Contents

  /v1/contents/{id}/status:
    get:
      summary: Get the current status of an article
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The ID of the article
      responses:
        '200':
          description: The status of the article
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentStatusResponse'
      tags:
        - Contents

  /v1/contents/{id}/grade:
    get:
      summary: Get content grade for an article
      description: |
        Returns the content grade and SERP optimization scores for an article.
        Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      responses:
        '200':
          description: Article grade
          content:
            application/json:
              schema:
                type: object
                properties:
                  grade:
                    $ref: '#/components/schemas/ContentGrade'
                  exists:
                    type: boolean
      tags:
        - Contents
    post:
      summary: Trigger grading for an article
      description: Analyzes the article and generates a content grade. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      responses:
        '200':
          description: Article graded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  grade:
                    $ref: '#/components/schemas/ContentGrade'
                  exists:
                    type: boolean
      tags:
        - Contents

  /v1/contents/{id}/serp:
    get:
      summary: Get SERP analysis data for an article
      description: |
        Returns competitor analysis data including top-ranking pages, word counts, and search intent.
        Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      responses:
        '200':
          description: SERP analysis data
          content:
            application/json:
              schema:
                type: object
                properties:
                  serp_data:
                    $ref: '#/components/schemas/SerpData'
                  status:
                    type: string
                    enum: [not_fetched, pending, complete, failed]
      tags:
        - Contents
    post:
      summary: Fetch SERP data for an article
      description: |
        Triggers competitor analysis by scraping top 10 search results for the article's keyword.
        Data is cached for 3 days. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                location:
                  type: string
                  description: Country/region code (e.g. "us", "uk"). Required if not set on article.
      responses:
        '200':
          description: SERP fetch started
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  status:
                    type: string
                  article_id:
                    type: string
      tags:
        - Contents

  /v1/contents/{id}/optimize:
    post:
      summary: Get AI optimization suggestions
      description: |
        Generates actionable suggestions to improve content based on SERP competitor analysis.
        Requires SERP data to be fetched first (POST /v1/contents/{id}/serp).
        Uses Anthropic API key if available for AI-powered suggestions, otherwise returns quick suggestions.
        Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                use_ai:
                  type: boolean
                  default: true
                  description: Use AI for suggestions (requires Anthropic API key)
      responses:
        '200':
          description: Optimization suggestions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OptimizationResult'
      tags:
        - Contents

  /v1/projects:
    get:
      summary: Fetch list of projects
      responses:
        '200':
          description: A list of projects
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    name:
                      type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
                      nullable: true
      tags:
        - Projects

    post:
      summary: Create a new project
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_name:
                  type: string
                  description: Name of the project
                  maxLength: 100
                site_id:
                  type: string
                  format: uuid
                  nullable: true
                  description: Optional site/brand ID. When provided, enables Brand Voice, site context, target audience, and Link Engine for all articles in the project. Use `GET /v1/sites` to list available sites.
                model:
                  type: string
                  description: The model to use for the article. Use the `/v1/meta/models` endpoint to get available models.
                  default: gpt-4o-mini
                common_settings_preset:
                  type: number
                  description: The settings preset to use for the project. Use the `common_settings` object to override specific settings. Go to your [presets](https://app.cuppa.ai/presets/my) to create a preset.
                  nullable: true
                common_settings:
                  $ref: '#/components/schemas/ContentSettings'
                  description: The common settings to use for the project. Use the `common_settings_preset` property to specify a preset.
                target_keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 600
                  description: The target keywords to generate content for.
              required:
                - project_name
                - target_keywords
      responses:
        '200':
          description: Project created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
      tags:
        - Projects
  /v1/projects/{id}:
    get:
      summary: Get a project by ID
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The ID of the project
      responses:
        '200':
          description: The project object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
      tags:
        - Projects

  /v1/projects/{id}/export:
    get:
      summary: Export project articles
      description: |
        Export all completed articles from a project in JSON or CSV format. 
        This endpoint is useful for bulk exporting content to external systems like Airtable, spreadsheets, or custom CMS integrations.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The ID of the project to export
        - in: query
          name: format
          schema:
            type: string
            enum: [json, csv]
            default: json
          description: The export format. Use `csv` for spreadsheet-compatible output.
        - in: query
          name: fields
          schema:
            type: string
          description: |
            Comma-separated list of fields to include in the export. 
            Available fields: `title`, `slug`, `content`, `excerpt`, `date`, `image`, `target_keyword`, `keywords`, `language`, `model`, `pov`, `tone`.
            Default: `title,slug,content,excerpt,image,target_keyword,language,model`
        - in: query
          name: session_id
          schema:
            type: string
            nullable: true
          description: Filter articles by bulk session ID (optional)
        - in: query
          name: status
          schema:
            type: string
            enum: [complete, all]
            default: complete
          description: Filter by article status. Use `complete` to only export finished articles, or `all` to include in-progress articles.
      responses:
        '200':
          description: Exported articles
          content:
            application/json:
              schema:
                type: object
                properties:
                  articles:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExportedArticle'
            text/csv:
              schema:
                type: string
                description: CSV formatted article data
        '404':
          description: No articles found for this project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Projects

  /v1/sites:
    get:
      summary: List all sites/brands
      description: Returns a list of all sites (brands) for your team. Use site IDs when creating content to enable Brand DNA features.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: Page number for pagination
      responses:
        '200':
          description: A list of sites
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Site'
      tags:
        - Sites
    post:
      summary: Create a new site/brand
      description: |
        Create a new site/brand with full Brand DNA onboarding. The brand analysis (company info, competitors, voice, visual style) is automatically queued and completes in 1-2 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - domain
              properties:
                domain:
                  type: string
                  description: The domain of the site (e.g., "example.com")
                name:
                  type: string
                  description: Optional display name for the brand
      responses:
        '201':
          description: Site created and Brand DNA queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  domain:
                    type: string
                  status:
                    type: string
                    enum: [queued]
                  message:
                    type: string
        '409':
          description: Site already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Sites

  /v1/sites/{id}:
    get:
      summary: Get a site by ID
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The site ID
      responses:
        '200':
          description: The site object
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
      tags:
        - Sites

  /v1/sites/{id}/brand:
    get:
      summary: Get all brand information for a site
      description: Returns brand context, voices, and visual style in a single request.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The site ID
      responses:
        '200':
          description: Brand information
          content:
            application/json:
              schema:
                type: object
                properties:
                  context:
                    $ref: '#/components/schemas/BrandContext'
                  voices:
                    type: array
                    items:
                      $ref: '#/components/schemas/BrandVoice'
                  visual_style:
                    $ref: '#/components/schemas/BrandVisualStyle'
      tags:
        - Sites

  /v1/presets:
    get:
      summary: List all presets
      description: Returns a list of AI instruction presets that can be used when creating content.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: Page number for pagination
      responses:
        '200':
          description: A list of presets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Preset'
      tags:
        - Presets
    post:
      summary: Create a new preset
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePreset'
      responses:
        '201':
          description: Preset created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresetDetail'
      tags:
        - Presets

  /v1/presets/{id}:
    get:
      summary: Get a preset by ID
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: The preset ID
      responses:
        '200':
          description: The preset with full settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresetDetail'
      tags:
        - Presets
    put:
      summary: Update a preset
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: The preset ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreatePreset'
      responses:
        '200':
          description: Preset updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresetDetail'
      tags:
        - Presets
    delete:
      summary: Delete a preset
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
          description: The preset ID
      responses:
        '204':
          description: Preset deleted
      tags:
        - Presets

  /v1/knowledge:
    get:
      summary: List knowledge sources
      description: Returns a list of knowledge sources (RAG documents). Requires Power plan or higher.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: Page number for pagination
      responses:
        '200':
          description: A list of knowledge sources
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/KnowledgeSource'
      tags:
        - Knowledge
    post:
      summary: Create a text knowledge source
      description: Create a knowledge source from text content. For file uploads, use POST /v1/knowledge/upload.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - content
              properties:
                name:
                  type: string
                  maxLength: 200
                description:
                  type: string
                  maxLength: 500
                content:
                  type: string
                  maxLength: 100000
                  description: The text content to index
      responses:
        '201':
          description: Knowledge source created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSource'
      tags:
        - Knowledge

  /v1/knowledge/upload:
    post:
      summary: Request upload URL for file-based knowledge
      description: |
        Get a signed URL to upload a PDF or TXT file. After uploading, call POST /v1/knowledge/{id}/confirm to trigger processing.
        Maximum file size: 50MB.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - filename
                - content_type
              properties:
                name:
                  type: string
                  maxLength: 200
                description:
                  type: string
                  maxLength: 500
                filename:
                  type: string
                content_type:
                  type: string
                  enum: [application/pdf, text/plain]
      responses:
        '201':
          description: Upload URL generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  knowledge_id:
                    type: integer
                  upload_url:
                    type: string
                    description: Signed URL for file upload
                  expires_at:
                    type: string
                    format: date-time
      tags:
        - Knowledge

  /v1/images/bulk:
    post:
      summary: Start bulk image generation
      description: |
        Generate featured images for multiple articles at once. Returns a request ID to poll for status.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - article_ids
              properties:
                article_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 50
                  description: Array of article IDs to generate images for
                model:
                  type: string
                  default: gpt-image-1
                  description: Image generation model to use
                use_brand_colors:
                  type: boolean
                  default: false
                  description: Whether to incorporate brand colors into images
                prompt_template:
                  type: string
                  description: Optional custom prompt template
      responses:
        '202':
          description: Bulk image generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                    format: uuid
                  total_articles:
                    type: number
                  status:
                    type: string
                    enum: [queued]
      tags:
        - Images

  /v1/images/bulk/{requestId}:
    get:
      summary: Get bulk image generation status
      description: Poll for the status of a bulk image generation request.
      parameters:
        - in: path
          name: requestId
          required: true
          schema:
            type: string
            format: uuid
          description: The bulk request ID
      responses:
        '200':
          description: Bulk image status
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                  status:
                    type: string
                    enum: [waiting, processing, completed, failed, canceled]
                  total_articles:
                    type: number
                  completed_articles:
                    type: number
                  failed_articles:
                    type: number
                  model:
                    type: string
                  articles:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        status:
                          type: string
                        image_url:
                          type: string
                          nullable: true
                        error:
                          type: string
                          nullable: true
      tags:
        - Images
    delete:
      summary: Cancel bulk image generation
      parameters:
        - in: path
          name: requestId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Cancelled successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
      tags:
        - Images

  /v1/clusters/generate:
    post:
      summary: Generate a content cluster
      description: |
        Create an interlinked content cluster with a pillar article and supporting articles.
        All articles are automatically interlinked using Link Engine.
        Requires Power plan or higher and Link Engine to be configured.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - pillar_keyword
                - supporting_keywords
              properties:
                site_id:
                  type: string
                  format: uuid
                pillar_keyword:
                  type: string
                  maxLength: 200
                  description: The main topic/keyword for the pillar article
                pillar_title:
                  type: string
                  maxLength: 200
                  description: Optional custom title for pillar article
                pillar_secondary_keywords:
                  type: array
                  items:
                    type: string
                  maxItems: 5
                supporting_keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 10
                  description: Keywords for supporting articles that link to the pillar
                options:
                  type: object
                  properties:
                    article_type:
                      type: string
                      enum: [general, listicle, review, howto]
                    word_count:
                      type: number
                      minimum: 500
                      maximum: 5000
                    language:
                      type: string
                    model:
                      type: string
      responses:
        '202':
          description: Cluster generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  cluster_id:
                    type: string
                  total_articles:
                    type: number
                  status:
                    type: string
                    enum: [queued]
                  message:
                    type: string
      tags:
        - Clusters

  /v1/clusters:
    get:
      summary: List keyword clusters
      description: Returns keyword clusters for content planning. Requires Power plan or higher.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: site
          schema:
            type: string
            format: uuid
          description: Filter by site ID
        - in: query
          name: in_plan
          schema:
            type: boolean
          description: Filter by active clusters
      responses:
        '200':
          description: A list of clusters
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Cluster'
      tags:
        - Clusters

  /v1/clusters/{id}:
    get:
      summary: Get a cluster with keywords
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The cluster ID
      responses:
        '200':
          description: Cluster with keywords
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClusterDetail'
      tags:
        - Clusters

  /v1/clusters/{id}/activate:
    post:
      summary: Activate a cluster
      description: Mark a cluster as "In Plan" for content planning. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The cluster ID
      responses:
        '200':
          description: Cluster activated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cluster'
      tags:
        - Clusters

  /v1/clusters/{id}/deactivate:
    post:
      summary: Deactivate a cluster
      description: Remove a cluster from "In Plan". Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The cluster ID
      responses:
        '200':
          description: Cluster deactivated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cluster'
      tags:
        - Clusters

  /v1/planner:
    get:
      summary: List content planner items
      description: Returns scheduled content items. Requires Power plan or higher.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: site
          schema:
            type: string
            format: uuid
        - in: query
          name: status
          schema:
            type: string
            enum: [scheduled, progress, created, published]
        - in: query
          name: from_date
          schema:
            type: string
            format: date
          description: Filter items scheduled on or after this date
        - in: query
          name: to_date
          schema:
            type: string
            format: date
          description: Filter items scheduled on or before this date
      responses:
        '200':
          description: A list of planner items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PlannerItem'
      tags:
        - Planner
    post:
      summary: Create a planner item
      description: Schedule a keyword for content creation. Each keyword can only be scheduled once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - keyword_id
                - cluster_id
                - date
              properties:
                site_id:
                  type: string
                  format: uuid
                keyword_id:
                  type: string
                  format: uuid
                  description: The keyword to schedule (must exist in site_keywords)
                cluster_id:
                  type: string
                  format: uuid
                  description: The cluster this keyword belongs to
                date:
                  type: string
                  format: date
                  description: Scheduled date for content creation
                status:
                  type: string
                  enum: [scheduled, progress, created, published]
                  default: scheduled
      responses:
        '201':
          description: Planner item created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerItem'
        '409':
          description: Planner item already exists for this keyword
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Planner

  /v1/planner/{id}:
    get:
      summary: Get a planner item
      description: Get a single planner item by keyword_id. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The keyword_id of the planner item
      responses:
        '200':
          description: The planner item
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerItem'
        '404':
          description: Planner item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Planner
    put:
      summary: Update a planner item
      description: Update the scheduled date or status of a planner item. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The keyword_id of the planner item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                date:
                  type: string
                  format: date
                  description: New scheduled date
                status:
                  type: string
                  enum: [scheduled, progress, created, published]
                  description: New status
      responses:
        '200':
          description: Planner item updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerItem'
        '400':
          description: Invalid status transition
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Planner
    delete:
      summary: Delete a planner item
      description: Remove a scheduled item from the content planner. Cannot delete created/published items. Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The keyword_id of the planner item
      responses:
        '200':
          description: Planner item deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          description: Cannot delete created or published items
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      tags:
        - Planner

  /v1/planner/calendar:
    get:
      summary: Get unified content calendar
      description: |
        Returns the unified content calendar showing all scheduled articles,
        local pages, and social posts grouped by date. Requires Solo plan
        or higher.
      parameters:
        - in: query
          name: site_id
          required: true
          schema:
            type: string
            format: uuid
          description: Site/brand ID
        - in: query
          name: status
          schema:
            type: string
            enum: [scheduled, created, published]
          description: Filter by status (default scheduled)
        - in: query
          name: from_date
          schema:
            type: string
            format: date
          description: Start date filter (YYYY-MM-DD)
        - in: query
          name: to_date
          schema:
            type: string
            format: date
          description: End date filter (YYYY-MM-DD)
      responses:
        '200':
          description: Unified content calendar
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: object
                    description: Items grouped by date (YYYY-MM-DD keys)
                    additionalProperties:
                      type: array
                      items:
                        type: object
                        properties:
                          type:
                            type: string
                            enum: [article, local, social_post]
                          id:
                            type: string
                          status:
                            type: string
                  counts:
                    type: object
                    properties:
                      article:
                        type: integer
                      local:
                        type: integer
                      social_post:
                        type: integer
                  totalCount:
                    type: integer
      tags:
        - Planner

  /v1/planner/events:
    get:
      summary: List planner events (holidays, custom dates, product launches)
      parameters:
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
        - in: query
          name: from_date
          schema:
            type: string
            format: date
          description: Include events on or after this date
        - in: query
          name: to_date
          schema:
            type: string
            format: date
          description: Include events on or before this date
        - in: query
          name: event_type
          schema:
            type: string
            enum: [holiday, cultural, industry, custom, product_launch, seasonal]
          description: Filter by event type
      responses:
        '200':
          description: A list of planner events
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/PlannerEvent'
      tags:
        - Planner
    post:
      summary: Create a planner event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
                - event_date
              properties:
                title:
                  type: string
                event_date:
                  type: string
                  format: date
                site_id:
                  type: string
                  format: uuid
                description:
                  type: string
                event_type:
                  type: string
                  enum: [holiday, cultural, industry, custom, product_launch, seasonal]
                recurrence:
                  type: string
                  enum: [none, annual, monthly, weekly]
      responses:
        '201':
          description: Planner event created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerEvent'
      tags:
        - Planner

  /v1/planner/events/{id}:
    get:
      summary: Get a planner event by ID
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Planner event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerEvent'
      tags:
        - Planner
    patch:
      summary: Update a planner event
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                event_date:
                  type: string
                  format: date
                event_type:
                  type: string
                  enum: [holiday, cultural, industry, custom, product_launch, seasonal]
                recurrence:
                  type: string
                  enum: [none, annual, monthly, weekly]
                metadata:
                  type: object
      responses:
        '200':
          description: Planner event updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlannerEvent'
      tags:
        - Planner
    delete:
      summary: Delete a planner event
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Planner event deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
              example:
                success: true
      tags:
        - Planner

  /v1/research/keywords:
    post:
      summary: Research keyword opportunities
      description: |
        Get keyword ideas from seed keywords using DataForSEO. Returns search volume, difficulty, and intent data.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - seed_keywords
              properties:
                seed_keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 10
                  description: Seed keywords to research
                location:
                  type: string
                  default: United States
                language:
                  type: string
                  default: en
                include_questions:
                  type: boolean
                  default: true
                limit:
                  type: number
                  minimum: 1
                  maximum: 500
                  default: 50
      responses:
        '200':
          description: Keyword research results
          content:
            application/json:
              schema:
                type: object
                properties:
                  keywords:
                    type: array
                    items:
                      type: object
                      properties:
                        keyword:
                          type: string
                        search_volume:
                          type: number
                        keyword_difficulty:
                          type: number
                        cpc:
                          type: number
                        competition:
                          type: string
                        search_intent:
                          type: string
                        is_question:
                          type: boolean
                  total:
                    type: number
                  seed_keywords:
                    type: array
                    items:
                      type: string
                  location:
                    type: string
      tags:
        - Research

  /v1/research/serp:
    post:
      summary: Analyze SERP for a keyword
      description: |
        Get detailed SERP analysis including organic results, SERP features, People Also Ask, and search intent.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - keyword
              properties:
                keyword:
                  type: string
                  maxLength: 200
                location:
                  type: string
                  default: United States
                language:
                  type: string
                  default: en
      responses:
        '200':
          description: SERP analysis results
          content:
            application/json:
              schema:
                type: object
                properties:
                  keyword:
                    type: string
                  location:
                    type: string
                  search_intent:
                    type: string
                    enum: [informational, commercial, transactional, local]
                  difficulty:
                    type: string
                    enum: [Low, Medium, High]
                  serp_features:
                    type: array
                    items:
                      type: string
                  organic_results:
                    type: array
                    items:
                      type: object
                      properties:
                        position:
                          type: number
                        url:
                          type: string
                        title:
                          type: string
                        domain:
                          type: string
                  people_also_ask:
                    type: array
                    items:
                      type: string
                  unique_domains:
                    type: number
      tags:
        - Research

  /v1/research/competitors:
    post:
      summary: Analyze competitor keywords
      description: |
        Get keywords that a competitor domain ranks for. Useful for competitive analysis and content gap identification.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - domain
              properties:
                domain:
                  type: string
                  description: Competitor domain to analyze (e.g., "competitor.com")
                location:
                  type: string
                  default: United States
                limit:
                  type: number
                  minimum: 1
                  maximum: 200
                  default: 50
      responses:
        '200':
          description: Competitor keyword analysis
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  keywords:
                    type: array
                    items:
                      type: object
                      properties:
                        keyword:
                          type: string
                        search_volume:
                          type: number
                        keyword_difficulty:
                          type: number
                        cpc:
                          type: number
                        competition:
                          type: string
                  total:
                    type: number
                  total_volume:
                    type: number
                  competition_breakdown:
                    type: object
                    additionalProperties:
                      type: number
      tags:
        - Research

  /v1/research/trends:
    post:
      summary: Google Trends data
      description: |
        Get Google Trends data for keywords: popularity over time, rising topics, breakout queries, and trend direction.
        Useful for validating content ideas, finding trending topics, and discovering what people are searching for.
        Supports web, news, YouTube, and image search types.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - keywords
              properties:
                keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 5
                  description: Keywords to check trends for (max 5)
                location:
                  type: string
                  description: Location name (e.g. "United States")
                time_range:
                  type: string
                  enum: [past_hour, past_4_hours, past_day, past_7_days, past_30_days, past_90_days, past_12_months, past_5_years]
                  default: past_12_months
                  description: Time range for trend data
                type:
                  type: string
                  enum: [web, news, youtube, images]
                  default: web
                  description: Search type
      responses:
        '200':
          description: Trend data with direction, related topics, and rising queries
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      keywords:
                        type: array
                        items:
                          type: string
                      location:
                        type: string
                      timeRange:
                        type: string
                      searchType:
                        type: string
                      items:
                        type: array
                        items:
                          type: object
      tags:
        - Research

  /v1/research/web-search:
    post:
      summary: Web search via Perplexity
      description: |
        Search the web for real-time information using Perplexity Sonar Pro.
        Returns a structured analysis with citations. Use for trending social media topics,
        industry news, competitor activity, viral content formats, or any question requiring current information.
        Requires a Perplexity API key in team settings.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - query
              properties:
                query:
                  type: string
                  minLength: 1
                  maxLength: 500
                  description: Search query for web research
                focus:
                  type: string
                  enum: [general, social_trends, industry_news, competitor_analysis]
                  default: general
                  description: Focus area to refine the search context
      responses:
        '200':
          description: Structured research result with citations
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      query:
                        type: string
                      focus:
                        type: string
                      result:
                        type: string
                        description: Structured analysis text
                      citations:
                        type: array
                        items:
                          type: string
                      usage:
                        type: object
                        properties:
                          promptTokens:
                            type: integer
                          completionTokens:
                            type: integer
      tags:
        - Research

  /v1/research/cluster:
    post:
      summary: Cluster keywords by intent and topic
      description: |
        Group keywords by search intent (informational, commercial, transactional, navigational) and topic clusters.
        Useful for content planning and site architecture.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - keywords
              properties:
                keywords:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  maxItems: 500
                method:
                  type: string
                  enum: [intent, topic, both]
                  default: both
      responses:
        '200':
          description: Keyword clustering results
          content:
            application/json:
              schema:
                type: object
                properties:
                  intent_clusters:
                    type: object
                    properties:
                      informational:
                        type: array
                        items:
                          type: string
                      commercial:
                        type: array
                        items:
                          type: string
                      transactional:
                        type: array
                        items:
                          type: string
                      navigational:
                        type: array
                        items:
                          type: string
                  topic_clusters:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        keywords:
                          type: array
                          items:
                            type: string
                        count:
                          type: number
                  total_keywords:
                    type: number
                  method:
                    type: string
      tags:
        - Research

  /v1/sites/{id}/gsc/queries:
    get:
      summary: Get GSC search queries for a site
      description: |
        Returns Google Search Console query data including clicks, impressions, position, and opportunity scores.
        Requires Power plan or higher and GSC to be connected.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: The site ID
        - in: query
          name: limit
          schema:
            type: integer
            maximum: 500
            default: 100
        - in: query
          name: min_impressions
          schema:
            type: integer
          description: Filter queries with at least this many impressions
        - in: query
          name: max_position
          schema:
            type: integer
          description: Filter queries ranking at or above this position
        - in: query
          name: sort_by
          schema:
            type: string
            enum: [position, impressions, clicks, opportunity_score]
            default: impressions
      responses:
        '200':
          description: GSC query data
          content:
            application/json:
              schema:
                type: object
                properties:
                  queries:
                    type: array
                    items:
                      type: object
                      properties:
                        query:
                          type: string
                        page_url:
                          type: string
                          nullable: true
                        clicks:
                          type: number
                        impressions:
                          type: number
                        ctr:
                          type: number
                        position:
                          type: number
                        opportunity_score:
                          type: number
                          nullable: true
                  total:
                    type: number
                  gsc_connected:
                    type: boolean
      tags:
        - Sites

  /v1/contents/{id}/publish:
    post:
      summary: Publish article to CMS platform
      description: |
        Push an article to an external CMS (Ghost, Webflow, Contentful, Sanity, Airtable).
        The integration must be configured in team settings first.
        Requires Power plan or higher.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: The article ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - platform
              properties:
                platform:
                  type: string
                  enum: [ghost, webflow, contentful, sanity, airtable, shopify]
                options:
                  type: object
                  properties:
                    status:
                      type: string
                      enum: [publish, draft]
                      default: publish
                    categories:
                      type: array
                      items:
                        type: string
                      description: WordPress/Ghost categories
                    tags:
                      type: array
                      items:
                        type: string
                      description: WordPress/Ghost tags
                    collection_id:
                      type: string
                      description: Webflow collection ID
      responses:
        '200':
          description: Article published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  platform:
                    type: string
                  external_id:
                    type: string
                    nullable: true
                  external_url:
                    type: string
                    nullable: true
                  error:
                    type: string
                    nullable: true
      tags:
        - Contents

  /v1/chat/completions:
    post:
      summary: Chat with Cuppa AI agent
      description: |
        Send messages to Cuppa AI agent with access to research tools.
        The agent can research keywords, analyze competitors, check SERP data, and provide content strategy insights.
        Requires Power plan or higher.
      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, system]
                      content:
                        type: string
                  minItems: 1
                site_id:
                  type: string
                  format: uuid
                  description: Site ID for brand context. If not provided, uses first active site.
                model:
                  type: string
                  default: gpt-5-mini
                  description: The model to use for chat
                tools:
                  type: array
                  items:
                    type: string
                    enum:
                      - research_keywords
                      - analyze_serp
                      - get_striking_distance
                      - cluster_keywords
                      - get_articles
                      - get_brand_context
                      - get_site_pages
                      - get_gsc_performance
                      - analyze_competitor_keywords
                  description: Which tools to enable. Defaults to all tools.
                max_tokens:
                  type: integer
                  minimum: 100
                  maximum: 16000
                  default: 4000
      responses:
        '200':
          description: Chat completion response
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: object
                    properties:
                      role:
                        type: string
                        enum: [assistant]
                      content:
                        type: string
                  tool_calls:
                    type: array
                    description: Tools that were called during the conversation
                    items:
                      type: object
                      properties:
                        tool:
                          type: string
                        input:
                          type: object
                        output:
                          type: object
                        success:
                          type: boolean
                  usage:
                    type: object
                    properties:
                      prompt_tokens:
                        type: integer
                      completion_tokens:
                        type: integer
                      total_tokens:
                        type: integer
                  model:
                    type: string
      tags:
        - Chat

  /v1/social/publish:
    post:
      summary: Publish social post immediately
      description: |
        Publish a social media post immediately to a connected platform via Late.dev.
        The social account must be connected in Brand Settings first.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - platform
                - content
              properties:
                site_id:
                  type: string
                  format: uuid
                platform:
                  type: string
                  enum: [twitter, linkedin, instagram, facebook, threads, tiktok, pinterest, youtube, bluesky, telegram]
                content:
                  type: string
                  maxLength: 5000
                hashtags:
                  type: array
                  items:
                    type: string
                image_url:
                  type: string
                  format: uri
                video_url:
                  type: string
                  format: uri
                  description: URL of a video to attach (preferred over image_url for video platforms like TikTok, YouTube)
      responses:
        '200':
          description: Post published
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  platform:
                    type: string
                  post_id:
                    type: string
                    nullable: true
                  late_post_id:
                    type: string
                    nullable: true
                  error:
                    type: string
                    nullable: true
      tags:
        - Social

  /v1/social/schedule:
    post:
      summary: Schedule social post for future publishing
      description: |
        Schedule a social media post for future publishing.
        The post is sent to Late.dev with the scheduled time.
        Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - platform
                - content
                - scheduled_at
              properties:
                site_id:
                  type: string
                  format: uuid
                platform:
                  type: string
                  enum: [twitter, linkedin, instagram, facebook, threads, tiktok, pinterest, youtube, bluesky, telegram]
                content:
                  type: string
                  maxLength: 5000
                scheduled_at:
                  type: string
                  format: date-time
                  description: ISO datetime when the post should be published
                hashtags:
                  type: array
                  items:
                    type: string
                image_url:
                  type: string
                  format: uri
                video_url:
                  type: string
                  format: uri
                  description: URL of a video to attach (preferred over image_url for video platforms like TikTok, YouTube)
                article_id:
                  type: string
                  format: uuid
                  description: Optional article ID to link this post to
      responses:
        '200':
          description: Post scheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  platform:
                    type: string
                  post_id:
                    type: string
                    nullable: true
                  late_post_id:
                    type: string
                    nullable: true
                  scheduled_at:
                    type: string
                  status:
                    type: string
                    enum: [scheduled, draft]
                  warning:
                    type: string
                    nullable: true
                    description: Warning message if post was saved as draft
      tags:
        - Social

  /v1/social:
    post:
      summary: Generate a social media post
      description: |
        Generate a social media post from article content or a standalone
        topic. Provide article_title + article_summary, or just a topic.
        Requires Power plan or higher. Uses your OpenAI API key for text
        generation, and optionally Replicate API key for image generation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateSocialPost'
      responses:
        '201':
          description: Social post generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialPost'
      tags:
        - Social

  /v1/social/multi:
    post:
      summary: Generate social posts for multiple platforms
      description: |
        Generate optimized social media posts for up to 5 platforms at once.
        Each platform gets tailored content based on its character limits
        and best practices. Provide article_title + article_summary,
        or topic for standalone posts. Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - platforms
              properties:
                site_id:
                  type: string
                  format: uuid
                  description: Site/brand ID
                topic:
                  type: string
                  maxLength: 500
                  description: Standalone topic (alternative to article fields)
                article_title:
                  type: string
                  description: Title of the article
                article_summary:
                  type: string
                  description: Brief summary of the article
                article_url:
                  type: string
                  description: Public URL of the article
                platforms:
                  type: array
                  items:
                    type: string
                    enum: [twitter, instagram, facebook, linkedin, tiktok, youtube, pinterest, bluesky, threads, telegram]
                  minItems: 1
                  maxItems: 5
                  description: Target platforms (1-5)
                tone:
                  type: string
                  enum: [casual, professional, witty, inspiring, educational, engaging]
                  default: engaging
                include_hashtags:
                  type: boolean
                  default: true
                generate_images:
                  type: boolean
                  default: false
                  description: Generate images for each platform (requires Replicate API key)
      responses:
        '201':
          description: Posts generated for each platform
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  $ref: '#/components/schemas/SocialPost'
                description: Map of platform name to generated post
      tags:
        - Social

  /v1/social/platforms:
    get:
      summary: Get supported social platforms
      description: |
        Returns all supported platforms with character limits, format support, and content tips.
      responses:
        '200':
          description: List of platforms
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        label:
                          type: string
                        max_characters:
                          type: integer
                        hashtags_supported:
                          type: boolean
                        max_hashtags:
                          type: integer
                        best_practices:
                          type: string
                        content_tips:
                          type: string
                          description: Platform-specific caption and hook guidance for Brand DNA and content tools
      tags:
        - Social

  /v1/social/posts:
    get:
      summary: List social posts
      description: List social media posts with optional filters.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: Page number for pagination
        - in: query
          name: limit
          schema:
            type: integer
            default: 20
            maximum: 100
          description: Maximum number of posts to return
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
        - in: query
          name: platform
          schema:
            type: string
          description: Filter by platform
        - in: query
          name: status
          schema:
            type: string
            enum: [draft, scheduled, published, failed]
          description: Filter by status
      responses:
        '200':
          description: A list of social posts
          content:
            application/json:
              schema:
                type: object
                properties:
                  posts:
                    type: array
                    items:
                      $ref: '#/components/schemas/SocialPostRecord'
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
      tags:
        - Social

  /v1/social/posts/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Social post ID
    get:
      summary: Get social post
      description: Get a single social post by ID.
      responses:
        '200':
          description: The social post
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialPostRecord'
      tags:
        - Social
    patch:
      summary: Update social post
      description: Update a draft or scheduled social post. Cannot edit published posts.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                content:
                  type: string
                hashtags:
                  type: array
                  items:
                    type: string
                scheduled_at:
                  type: string
                  format: date-time
                image_url:
                  type: string
      responses:
        '200':
          description: Post updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SocialPostRecord'
      tags:
        - Social
    delete:
      summary: Delete social post
      description: Delete a draft or scheduled social post. Cannot delete published posts.
      responses:
        '200':
          description: Post deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
      tags:
        - Social

  /v1/social/posts/{id}/schedule:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Social post ID (must be in draft status)
    post:
      summary: Schedule a draft social post
      description: |
        Takes an existing draft social post and schedules it via Late.dev.
        Uses the post's scheduled_at timestamp, or defaults to tomorrow 9am UTC.
        The post must be in "draft" status and have a connected platform account.
      responses:
        '200':
          description: Post scheduled
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [scheduled]
                  late_post_id:
                    type: string
                  scheduled_at:
                    type: string
                    format: date-time
        '422':
          description: Post cannot be scheduled (not draft, no account connected, etc.)
      tags:
        - Social

  /v1/social/posts/{id}/publish:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Social post ID (must be in draft status)
    post:
      summary: Publish a draft social post immediately
      description: |
        Takes an existing draft social post and publishes it immediately via Late.dev.
        The post must be in "draft" status and have a connected platform account.
      responses:
        '200':
          description: Post published
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [published]
                  late_post_id:
                    type: string
                  published_at:
                    type: string
                    format: date-time
        '422':
          description: Post cannot be published (not draft, no account connected, etc.)
      tags:
        - Social

  /v1/social/posts/bulk-schedule:
    post:
      summary: Bulk schedule or publish draft social posts
      description: |
        Schedule or publish multiple draft social posts at once. Processes posts
        sequentially to respect Late.dev rate limits. Returns partial success:
        individual post failures don't block others.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - post_ids
              properties:
                post_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 50
                  description: Array of draft social post IDs
                action:
                  type: string
                  enum: [schedule, publish]
                  default: schedule
                  description: Action to perform on each post
      responses:
        '200':
          description: Bulk operation results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        status:
                          type: string
                          enum: [scheduled, published, failed]
                        late_post_id:
                          type: string
                        scheduled_at:
                          type: string
                          format: date-time
                        published_at:
                          type: string
                          format: date-time
                        error:
                          type: string
      tags:
        - Social

  /v1/social/analytics:
    get:
      summary: Get social media analytics
      description: |
        Returns social media analytics for a site including impressions,
        engagements, clicks, shares, per-platform breakdowns, and top
        performing posts. Requires Solo plan or higher.
      parameters:
        - in: query
          name: site_id
          required: true
          schema:
            type: string
            format: uuid
          description: Site/brand ID
        - in: query
          name: platform
          schema:
            type: string
          description: Filter by platform (e.g. twitter, linkedin)
        - in: query
          name: from_date
          schema:
            type: string
            format: date-time
          description: Start date filter (ISO 8601)
        - in: query
          name: to_date
          schema:
            type: string
            format: date-time
          description: End date filter (ISO 8601)
        - in: query
          name: top_n
          schema:
            type: integer
            default: 5
            maximum: 20
          description: Number of top posts to include
      responses:
        '200':
          description: Social analytics summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  totalPosts:
                    type: integer
                  totalImpressions:
                    type: integer
                  totalEngagements:
                    type: integer
                  totalClicks:
                    type: integer
                  totalShares:
                    type: integer
                  averageEngagementRate:
                    type: number
                  byPlatform:
                    type: object
                    description: Per-platform breakdown
                    additionalProperties:
                      type: object
                      properties:
                        posts:
                          type: integer
                        impressions:
                          type: integer
                        engagements:
                          type: integer
                  topPosts:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        platform:
                          type: string
                        content:
                          type: string
                        analytics:
                          type: object
                          properties:
                            impressions:
                              type: integer
                            engagements:
                              type: integer
                            clicks:
                              type: integer
                            engagement_rate:
                              type: number
                  period:
                    type: object
                    properties:
                      from:
                        type: string
                        nullable: true
                      to:
                        type: string
                        nullable: true
      tags:
        - Social

  /v1/social/carousel:
    post:
      summary: Generate carousel
      description: |
        Generate a LinkedIn carousel with AI-generated slides. Requires Business plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - topic
              properties:
                site_id:
                  type: string
                  format: uuid
                topic:
                  type: string
                  description: Topic for the carousel
                hook:
                  type: string
                  description: Opening hook text
                target_audience:
                  type: string
                slide_count:
                  type: integer
                  minimum: 3
                  maximum: 15
                  default: 6
                tone:
                  type: string
                  enum: [casual, professional, educational, inspirational]
                include_stats:
                  type: boolean
                cta_text:
                  type: string
                cta_url:
                  type: string
                generate_images:
                  type: boolean
                  default: true
      responses:
        '201':
          description: Carousel generated
          content:
            application/json:
              schema:
                type: object
                description: Carousel data with slides
      tags:
        - Social

  /v1/social/carousel/publish:
    post:
      summary: Publish carousel
      description: Publish a carousel to LinkedIn.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - caption
                - image_urls
              properties:
                site_id:
                  type: string
                  format: uuid
                caption:
                  type: string
                image_urls:
                  type: array
                  items:
                    type: string
                hashtags:
                  type: array
                  items:
                    type: string
                scheduled_at:
                  type: string
                  format: date-time
                  description: Omit for immediate publishing
      responses:
        '201':
          description: Carousel published
          content:
            application/json:
              schema:
                type: object
                description: Publish result
      tags:
        - Social

  # ─── Video ──────────────────────────────────────────────────────────────────

  /v1/video/script:
    post:
      summary: Generate video script
      description: Generate a detailed video prompt and settings from a brief description.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - brief
              properties:
                brief:
                  type: string
                  description: Brief description of the video
                format:
                  type: string
                  enum: [product_showcase, lifestyle_aesthetic, ugc_style, before_after, cinematic_broll, logo_brand_reveal, tutorial_howto, testimonial_talking_head, seasonal_campaign, abstract_mood]
                platform:
                  type: string
                site_id:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Generated video script
          content:
            application/json:
              schema:
                type: object
                properties:
                  prompt:
                    type: string
                  suggested_duration:
                    type: number
                  suggested_aspect_ratio:
                    type: string
                  suggested_resolution:
                    type: string
                  shot_description:
                    type: string
                  negative_prompt:
                    type: string
      tags:
        - Video

  /v1/video:
    post:
      summary: Create video generation job
      description: Start a video generation job. Returns an ID to poll for status.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - prompt
                - model
                - duration_seconds
              properties:
                prompt:
                  type: string
                model:
                  type: string
                  description: 'Video model to use (e.g. replicate:runway-gen-4.5, replicate:seedance-2.0, replicate:seedance-2.0-fast, replicate:kling-v3, replicate:veo-3.1-fast, replicate:grok-imagine-video, replicate:p-video)'
                duration_seconds:
                  type: number
                aspect_ratio:
                  type: string
                  enum: ['16:9', '9:16', '1:1', '4:3', '3:4', '3:2', '2:3']
                resolution:
                  type: string
                  enum: [480p, 720p, 1080p]
                negative_prompt:
                  type: string
                site_id:
                  type: string
                  format: uuid
                use_brand_context:
                  type: boolean
      responses:
        '201':
          description: Video generation job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
                    enum: [progress]
      tags:
        - Video

  /v1/video/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
        description: Video job ID
    get:
      summary: Get video status
      description: Check the status of a video generation job.
      responses:
        '200':
          description: Video job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  url:
                    type: string
                    nullable: true
                  thumbnail_url:
                    type: string
                    nullable: true
                  status:
                    type: string
                  provider:
                    type: string
                    nullable: true
                  model:
                    type: string
                    nullable: true
                  video_type:
                    type: string
                    nullable: true
                  meta:
                    type: object
                    nullable: true
                  progress:
                    type: number
                    nullable: true
                  duration_seconds:
                    type: number
                    nullable: true
                  error_message:
                    type: string
                    nullable: true
                  created_at:
                    type: string
                    format: date-time
      tags:
        - Video
    delete:
      summary: Delete video
      responses:
        '200':
          description: Video deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
      tags:
        - Video

  # ─── Mood Board ─────────────────────────────────────────────────────────────

  /v1/mood-board:
    get:
      summary: List mood boards
      parameters:
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
      responses:
        '200':
          description: A list of mood boards
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MoodBoard'
      tags:
        - Mood Board
    post:
      summary: Create mood board
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                description:
                  type: string
                site_id:
                  type: string
                  format: uuid
      responses:
        '201':
          description: Mood board created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MoodBoard'
      tags:
        - Mood Board

  /v1/mood-board/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Mood board ID
    get:
      summary: Get mood board with items
      responses:
        '200':
          description: Mood board with items
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/MoodBoard'
                  - type: object
                    properties:
                      items:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            type:
                              type: string
                            url:
                              type: string
                              nullable: true
                            metadata:
                              type: object
                              nullable: true
      tags:
        - Mood Board
    delete:
      summary: Delete mood board
      responses:
        '200':
          description: Mood board deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
      tags:
        - Mood Board

  /v1/mood-board/generate:
    post:
      summary: Generate mood board images
      description: Generate mood board images and colors from brand context. Requires Replicate API key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - mood_board_id
                - site_id
              properties:
                mood_board_id:
                  type: string
                  format: uuid
                site_id:
                  type: string
                  format: uuid
      responses:
        '200':
          description: Generation result
          content:
            application/json:
              schema:
                type: object
                description: Generation result with generated images and colors
      tags:
        - Mood Board

  /v1/links:
    get:
      summary: List Link Engine sites
      description: Returns sites with Link Engine configuration. Requires Power plan or higher.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
        - in: query
          name: search
          schema:
            type: string
          description: Search by domain
        - in: query
          name: category
          schema:
            type: string
            format: uuid
        - in: query
          name: status
          schema:
            type: string
            enum: [all, active, paused, indexing, inactive, error]
      responses:
        '200':
          description: List of Link Engine sites
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/LinkEngineSite'
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                      page_size:
                        type: integer
                      total:
                        type: integer
                      total_pages:
                        type: integer
      tags:
        - Links

  /v1/links/stats:
    get:
      summary: Get Link Engine statistics
      responses:
        '200':
          description: Link Engine stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkEngineStats'
      tags:
        - Links

  /v1/links/categories:
    get:
      summary: List link categories
      responses:
        '200':
          description: List of categories
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/LinkCategory'
      tags:
        - Links
    post:
      summary: Create a link category
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 50
                description:
                  type: string
                  maxLength: 200
                color:
                  type: string
                  pattern: '^#[0-9A-Fa-f]{6}$'
                  default: '#6366f1'
      responses:
        '201':
          description: Category created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkCategory'
      tags:
        - Links

  /v1/links/inject:
    post:
      summary: Run Link Engine on articles
      description: |
        Inject internal and/or external links into a batch of completed articles.
        Returns a request_id for polling status via GET. Up to 500 articles per call.
        Articles must belong to the authenticated team and have status "complete".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - article_ids
                - site_id
              properties:
                article_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                  minItems: 1
                  maxItems: 500
                  description: Article IDs to inject links into
                site_id:
                  type: string
                  format: uuid
                  description: Site ID (must have Link Engine configured)
                max_links_per_article:
                  type: integer
                  minimum: 1
                  maximum: 20
                  default: 5
                  description: Maximum links to inject per article
                include_internal:
                  type: boolean
                  default: true
                  description: Include internal links
                include_external:
                  type: boolean
                  default: true
                  description: Include external links
      responses:
        '200':
          description: Link injection job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                    format: uuid
                  total_articles:
                    type: integer
                  status:
                    type: string
                    enum: [waiting]
        '400':
          description: Invalid request or Link Engine not configured
        '403':
          description: Plan does not support Link Engine
      tags:
        - Links
    get:
      summary: Get link injection status
      description: Poll the status of a link injection job.
      parameters:
        - in: query
          name: request_id
          required: true
          schema:
            type: string
            format: uuid
          description: Request ID from POST /v1/links/inject
        - in: query
          name: page
          schema:
            type: integer
            default: 1
          description: Page for per-article status
      responses:
        '200':
          description: Link injection job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  request_id:
                    type: string
                  status:
                    type: string
                    enum: [waiting, progress, complete, failed, canceled]
                  total_articles:
                    type: integer
                  completed_articles:
                    type: integer
                  failed_articles:
                    type: integer
                  total_links_added:
                    type: integer
                  articles:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        status:
                          type: string
                        linksAdded:
                          type: integer
                        error:
                          type: string
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                      page_size:
                        type: integer
                      total:
                        type: integer
                      total_pages:
                        type: integer
        '404':
          description: Job not found
      tags:
        - Links

  # ─── Templates ───────────────────────────────────────────────────────────────

  /v1/templates:
    get:
      summary: List custom templates
      description: List all custom templates for the team. Requires Business plan or higher.
      parameters:
        - in: query
          name: page
          schema:
            type: integer
          description: Page number for pagination
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
      responses:
        '200':
          description: A list of custom templates
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TemplateListItem'
      tags:
        - Templates
    post:
      summary: Create a custom template
      description: Create a new custom template. Requires Business plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - system_prompt
                - user_prompt
              properties:
                name:
                  type: string
                description:
                  type: string
                site_id:
                  type: string
                  format: uuid
                system_prompt:
                  type: string
                user_prompt:
                  type: string
                ideal_output:
                  type: string
                variables:
                  type: object
                  additionalProperties:
                    type: string
                enable_knowledge:
                  type: boolean
                  default: false
                enable_serp:
                  type: boolean
                  default: false
                enable_perplexity:
                  type: boolean
                  default: false
                include_faq:
                  type: boolean
                  default: false
                include_key_takeaways:
                  type: boolean
                  default: false
                include_toc:
                  type: boolean
                  default: false
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateDetail'
      tags:
        - Templates

  /v1/templates/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Template ID
    get:
      summary: Get a custom template
      description: Get template details including prompts and configuration.
      responses:
        '200':
          description: Template detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateDetail'
      tags:
        - Templates
    put:
      summary: Update a custom template
      description: Update an existing custom template. Only provided fields are updated.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                system_prompt:
                  type: string
                user_prompt:
                  type: string
                ideal_output:
                  type: string
                variables:
                  type: object
                  additionalProperties:
                    type: string
                enable_knowledge:
                  type: boolean
                enable_serp:
                  type: boolean
                enable_perplexity:
                  type: boolean
                include_faq:
                  type: boolean
                include_key_takeaways:
                  type: boolean
                include_toc:
                  type: boolean
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateDetail'
      tags:
        - Templates
    delete:
      summary: Delete a custom template
      responses:
        '204':
          description: Template deleted
      tags:
        - Templates

  # ─── Knowledge Source by ID ──────────────────────────────────────────────────

  /v1/knowledge/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: integer
        description: Knowledge source ID
    get:
      summary: Get a knowledge source
      description: Get a single knowledge source by ID. Requires Power plan or higher.
      responses:
        '200':
          description: Knowledge source detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSource'
      tags:
        - Knowledge
    patch:
      summary: Update a knowledge source
      description: Update knowledge source metadata (name, description, etc.). Requires Power plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                  nullable: true
                site_id:
                  type: string
                  format: uuid
                  nullable: true
                enabled:
                  type: boolean
      responses:
        '200':
          description: Knowledge source updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KnowledgeSource'
      tags:
        - Knowledge
    delete:
      summary: Delete a knowledge source
      description: Delete a knowledge source and its embeddings. Requires Power plan or higher.
      responses:
        '204':
          description: Knowledge source deleted
      tags:
        - Knowledge

  # ─── Brand Context & Visual Style Write ──────────────────────────────────────

  /v1/sites/{id}/brand/context:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
    get:
      summary: Get brand context
      description: Returns brand context (company info, competitors, keywords, etc.)
      responses:
        '200':
          description: Brand context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandContext'
      tags:
        - Sites
    patch:
      summary: Update brand context
      description: Update editable brand context fields. Creates context if it doesn't exist.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                company_name:
                  type: string
                description:
                  type: string
                tagline:
                  type: string
                industry:
                  type: string
                target_audience:
                  type: string
                value_proposition:
                  type: string
                differentiators:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Brand context updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandContext'
      tags:
        - Sites

  /v1/sites/{id}/brand/visual-style:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
    get:
      summary: Get brand visual style
      description: Returns brand visual style (colors, fonts, logo, etc.)
      responses:
        '200':
          description: Brand visual style
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandVisualStyle'
      tags:
        - Sites
    put:
      summary: Update brand visual style
      description: Update brand visual style. Only provided fields are updated.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                logo_url:
                  type: string
                primary_color:
                  type: string
                secondary_color:
                  type: string
                accent_color:
                  type: string
                primary_font:
                  type: string
                secondary_font:
                  type: string
                visual_mood:
                  type: string
      responses:
        '200':
          description: Brand visual style updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandVisualStyle'
      tags:
        - Sites

  /v1/sites/{id}/brand/voices:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
    get:
      summary: Get brand voices
      description: Returns brand voices (tone, persona, formality, etc.)
      responses:
        '200':
          description: List of brand voices
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/BrandVoice'
      tags:
        - Sites

  /v1/sites/{id}/content-strategy:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
    get:
      summary: Get content strategy settings
      description: |
        Returns the content strategy settings for a site including article
        defaults, social publishing config, newsletter settings, and
        automation rules. Requires Solo plan or higher.
      responses:
        '200':
          description: Content strategy settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentStrategySettings'
      tags:
        - Sites
    put:
      summary: Update content strategy settings
      description: |
        Update content strategy settings for a site. Supports partial
        updates: only send the sections you want to change. Requires
        Solo plan or higher.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContentStrategySettings'
      responses:
        '200':
          description: Updated content strategy settings
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentStrategySettings'
      tags:
        - Sites

  # ─── Brand Notebook ──────────────────────────────────────────────────────────

  /v1/brands/{siteId}/notebook:
    parameters:
      - in: path
        name: siteId
        required: true
        schema:
          type: string
          format: uuid
        description: Site ID
    get:
      summary: List notebook entries
      description: List client notes / meeting transcripts for a site. Requires Agency plan or higher.
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
        - in: query
          name: source
          schema:
            type: string
          description: Filter by source (api, manual, etc.)
      responses:
        '200':
          description: List of notebook entries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/NotebookEntry'
      tags:
        - Brand Notebook
    post:
      summary: Create a notebook entry
      description: Add a new client note or meeting transcript. Content is automatically indexed for RAG.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - title
                - content
              properties:
                title:
                  type: string
                  maxLength: 500
                content:
                  type: string
                  maxLength: 100000
                meeting_date:
                  type: string
                  format: date
                  nullable: true
      responses:
        '201':
          description: Entry created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotebookEntry'
      tags:
        - Brand Notebook

  /v1/brands/{siteId}/notebook/{entryId}:
    parameters:
      - in: path
        name: siteId
        required: true
        schema:
          type: string
          format: uuid
      - in: path
        name: entryId
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Get a notebook entry
      responses:
        '200':
          description: Notebook entry detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotebookEntry'
      tags:
        - Brand Notebook
    put:
      summary: Update a notebook entry
      description: Update an existing notebook entry. If content changes, it is re-indexed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  maxLength: 500
                content:
                  type: string
                  maxLength: 100000
                meeting_date:
                  type: string
                  format: date
                  nullable: true
      responses:
        '200':
          description: Entry updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotebookEntry'
      tags:
        - Brand Notebook
    delete:
      summary: Delete a notebook entry
      responses:
        '204':
          description: Entry deleted
      tags:
        - Brand Notebook

  # ─── Agents ─────────────────────────────────────────────────────────────────

  /v1/agents:
    get:
      summary: List research agents
      description: List all research agents for the team. Requires Power plan or higher.
      parameters:
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
      responses:
        '200':
          description: List of agents
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Agent'
      tags:
        - Agents

  /v1/agents/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
        description: Agent ID
    get:
      summary: Get an agent
      description: Get agent details including latest snapshot and research brief.
      responses:
        '200':
          description: Agent detail with latest results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentDetail'
      tags:
        - Agents

  /v1/agents/{id}/run:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    post:
      summary: Trigger an agent run
      description: Manually trigger an agent to run immediately. Returns 409 if already running.
      responses:
        '200':
          description: Agent run triggered
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '409':
          description: Agent is already running
      tags:
        - Agents

  /v1/pages:
    post:
      summary: Create a page
      description: >
        Generate a landing page, product page, comparison, or alternatives page.
        Returns a page ID immediately. Poll GET /v1/pages/{id} until status is 'complete'.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_id
                - target_keyword
              properties:
                site_id:
                  type: string
                  format: uuid
                  description: Brand / site ID
                target_keyword:
                  type: string
                  description: Primary keyword for the page
                page_type:
                  type: string
                  enum: [landing_page, product_page, comparison, alternatives]
                  default: landing_page
                title:
                  type: string
                  description: Optional title; otherwise derived from keyword
                model:
                  type: string
                  description: AI model to use for generation. Defaults to gpt-5-mini. Examples - gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash.
      responses:
        '200':
          description: Page created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      status:
                        type: string
        '400':
          description: Bad request
      tags:
        - Pages
    get:
      summary: List pages
      description: Returns all pages for the team, optionally filtered by site.
      parameters:
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
          description: Filter by site ID
        - in: query
          name: status
          schema:
            type: string
            enum: [draft, generating, complete, error]
          description: Filter by status
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of pages
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Page'
                  total:
                    type: integer
      tags:
        - Pages

  /v1/pages/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Get page by ID
      description: Returns full page details including sections, meta, and schema markup.
      parameters:
        - in: query
          name: site_id
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Page details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Page'
                      - type: object
                        properties:
                          schema_markup:
                            type: array
                            items:
                              type: object
                            description: JSON-LD schema objects (WebPage, FAQPage, Product)
        '404':
          description: Page not found
      tags:
        - Pages

  /v1/pages/{id}/status:
    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Get page generation status
      description: Quick status check for a page (draft, generating, complete, error).
      responses:
        '200':
          description: Page status
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                      status:
                        type: string
        '404':
          description: Page not found
      tags:
        - Pages

  /v1/research/serp-cluster-bulk:
    post:
      summary: Bulk SERP clustering with site assignment
      description: |
        Submit a large batch of keywords (15K-60K) for SERP-based clustering and automatic site assignment.
        Keywords are clustered by URL overlap, then assigned to sites based on category matching from an Airtable source.
        Results are written to an Airtable output table.

        This is an async operation. Use the returned job_id to poll for status.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - site_source
                - output
              properties:
                keywords_csv:
                  type: string
                  description: Raw CSV content (Ahrefs/SEMrush format). Either this or keywords array is required.
                keywords:
                  type: array
                  items:
                    type: object
                    required: [keyword, volume, kd]
                    properties:
                      keyword:
                        type: string
                      volume:
                        type: number
                      kd:
                        type: number
                  description: Array of keywords with volume and KD. Either this or keywords_csv is required.
                config:
                  type: object
                  description: Filter thresholds. Defaults are very permissive since customers typically pre-filter in Ahrefs.
                  properties:
                    max_kd:
                      type: number
                      default: 100
                      description: Max keyword difficulty (0-100). Set lower to filter.
                    min_volume:
                      type: number
                      default: 0
                      description: Min search volume. Set higher to filter.
                    max_volume:
                      type: number
                      default: 999999
                      description: Max search volume. Set lower to filter.
                    overlap_threshold:
                      type: number
                      default: 0.3
                    serp_depth:
                      type: number
                      default: 10
                    location_code:
                      type: number
                      default: 2840
                    language_code:
                      type: string
                      default: en
                assignment:
                  type: object
                  description: Controls how clusters are distributed across sites
                  properties:
                    priority_sites:
                      type: array
                      description: Sites that should receive a scoring boost during assignment
                      items:
                        type: object
                        required: [domain]
                        properties:
                          domain:
                            type: string
                            description: Site domain to boost
                          boost:
                            type: number
                            default: 2
                            description: Score multiplier (1-10). Higher means more likely to win clusters.
                    max_clusters_per_site:
                      type: number
                      description: Max clusters any single site can receive per run (velocity cap). Unset means no limit.
                    min_clusters_per_site:
                      type: number
                      description: Minimum clusters each site should receive. Unassigned clusters are redistributed to sites below this floor using a 4-tier matching cascade (subcategory, category token, keyword overlap, round-robin). Unset means no floor.
                    scarcity_strength:
                      type: number
                      default: 1
                      description: Controls distribution spread. 0 = pure relevance (best-match site always wins), 1 = balanced (default), 2-3 = aggressive spread (empty sites get stronger boost).
                site_source:
                  type: object
                  required: [airtable_base_id, airtable_table_name, columns]
                  properties:
                    airtable_base_id:
                      type: string
                    airtable_table_name:
                      type: string
                    columns:
                      type: object
                      required: [website, categories, subcategories]
                      properties:
                        website:
                          type: string
                        site_name:
                          type: string
                        categories:
                          type: string
                        subcategories:
                          type: string
                output:
                  type: object
                  required: [airtable_base_id, airtable_table_name]
                  properties:
                    airtable_base_id:
                      type: string
                    airtable_table_name:
                      type: string
                    clear_before_write:
                      type: boolean
                      default: false
      responses:
        '201':
          description: Job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                  estimated_time_minutes:
                    type: number
                  estimated_cost:
                    type: number
        '400':
          description: Validation error
        '429':
          description: Rate limit exceeded
      tags:
        - Research

  /v1/research/serp-cluster-bulk/{jobId}:
    parameters:
      - in: path
        name: jobId
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Get cluster bulk job status
      description: Poll for the status of a bulk SERP clustering job. Returns progress info and summary when complete.
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                  status:
                    type: string
                    enum: [pending, parsing, fetching_serps, clustering, reading_sites, assigning, writing_results, completed, failed]
                  progress:
                    type: object
                    nullable: true
                    properties:
                      current:
                        type: number
                      total:
                        type: number
                  message:
                    type: string
                    nullable: true
                  summary:
                    type: object
                    nullable: true
                    description: Available when status is completed
                  error:
                    type: string
                    nullable: true
        '404':
          description: Job not found
      tags:
        - Research

  /v1/research/serp-cluster-bulk/{jobId}/generate:
    parameters:
      - in: path
        name: jobId
        required: true
        schema:
          type: string
          format: uuid
    post:
      summary: Generate articles from approved clusters
      description: |
        Reads rows from the Airtable output table, creates articles in Cuppa, and dispatches generation.
        By default only rows with Status = Assigned AND the Approved checkbox = true are included.
        Articles appear in the Cuppa UI under the correct site. Status is written back to Airtable.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - settings
              properties:
                filter:
                  type: object
                  properties:
                    status:
                      type: string
                      default: Assigned
                      description: Must match clustering output single-select (usually Assigned)
                    require_approved_checkbox:
                      type: boolean
                      default: true
                      description: When true, require Approved checkbox. Set false for legacy Status-only tables.
                    approved_checkbox_field:
                      type: string
                      default: Approved
                    airtable_filter_formula:
                      type: string
                      description: Full Airtable filterByFormula; overrides status and checkbox logic
                    sites:
                      type: array
                      items:
                        type: string
                    max_articles:
                      type: number
                settings:
                  type: object
                  required: [language, region]
                  properties:
                    language:
                      type: string
                    region:
                      type: string
                    model:
                      type: string
                    tone:
                      type: string
                    pov:
                      type: string
                    generate_images:
                      type: boolean
                      default: false
                    include_introduction:
                      type: boolean
                      default: true
                    include_conclusion:
                      type: boolean
                      default: true
                airtable_status_column:
                  type: string
                  description: Column name in Airtable to write generation status updates
                cuppa_article_id_field:
                  type: string
                  default: Cuppa Article ID
                  description: Airtable column for article UUID; must match filter and write-back
      responses:
        '201':
          description: Generation job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                  articles_queued:
                    type: number
                  sites_involved:
                    type: number
        '400':
          description: Cluster job not complete or invalid request
        '404':
          description: Cluster job not found
      tags:
        - Research

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            status:
              type: number
            message:
              type: string

    TaskStatus:
      type: string
      enum: [initializing, waiting, progress, canceled, complete, failed]

    Page:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          nullable: true
        status:
          type: string
          enum: [draft, generating, complete, error]
        page_type:
          type: string
          enum: [landing_page, product_page, comparison, alternatives]
        sections:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              title:
                type: string
                nullable: true
              content:
                type: string
                nullable: true
        meta:
          type: object
          description: Page metadata (target_keyword, meta_title, meta_description, etc.)
        site_id:
          type: string
          format: uuid
        team_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true
        schema_markup:
          type: array
          items:
            type: object
          description: JSON-LD schema objects (WebPage, FAQPage, Product). Present on GET /v1/pages/{id}.

    Content:
      type: object
      properties:
        id:
          type: string
        is_draft:
          type: boolean
          description: Whether the article is a draft
        status:
          $ref: '#/components/schemas/TaskStatus'
        title:
          type: string
          nullable: true
          description: It is null if the article is not generated yet.
        content:
          type: string
          nullable: true
          description: It is null if the article is not generated yet.
        snippet:
          type: string
          nullable: true
        project_id:
          type: string
          nullable: true
          description: It is null if the article is not part of a project.
        site_id:
          type: string
          format: uuid
          nullable: true
          description: The site/brand ID associated with this article. Null if no brand context.
        meta_description:
          type: string
          nullable: true
        featured_images:
          type: array
          items:
            type: object
            properties:
              url:
                type: string
              alt:
                type: string
                nullable: true
        content_type:
          type: string
          enum: [article]
        settings:
          type: object
          properties:
            model:
              type: string
            target_keyword:
              type: string
            language:
              type: string
            region:
              type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    ContentStatusResponse:
      type: object
      properties:
        id:
          type: string
        is_draft:
          type: boolean
          description: Whether the article is a draft
        status:
          $ref: '#/components/schemas/TaskStatus'
        status_extra:
          type: object
          properties:
            error:
              type: string
              nullable: true
              description: Error message if status is failed

    ContentSettings:
      type: object
      required:
        - language
        - region
      properties:
        language:
          type: string
          description: The language of the article.
        region:
          type: string
          description: The region of the article.
        tone:
          type: string
          enum:
            [
              seo_optimized,
              casual,
              excited,
              formal,
              friendly,
              humorous,
              professional,
            ]
          default: seo_optimized
          description: The tone of the article.
        pov:
          type: string
          enum:
            [
              first_person_singular,
              first_person_plural,
              second_person,
              third_person,
            ]
          default: first_person_plural
          description: The point of view of the article.
        article_type:
          type: string
          enum: [general, listicle, review, howto, recipe]
          default: general
          description: The type of the article.
        fetch_perplexity:
          type: boolean
          default: false
          description: Whether to use Perplexity for fetching real-time research data. It requires a Perplexity API key.
        include_introduction:
          type: boolean
          default: true
          description: Whether to include an introduction section in the article.
        include_conclusion:
          type: boolean
          default: true
          description: Whether to include a conclusion section in the article.
        include_yt_suggestions:
          type: boolean
          default: false
          description: Whether to include YouTube suggestions in the article.
        generate_faqs:
          type: boolean
          default: false
          description: Whether to generate a FAQ section in the article.
        generate_meta_description:
          type: boolean
          default: false
          description: Whether to generate a meta description for the article.
        key_takeaways_position:
          type: string
          default: none
          enum: [top, bottom, none]
          description: The position of the key takeaways section in the article. Use `none` to disable it.
        include_stock_images:
          type: boolean
          default: false
          description: Whether to include stock featured images in the article.
        generate_images:
          type: boolean
          default: false
          description: Whether to generate AI images for the article. Use the `image_settings` object to configure image generation.
        image_settings:
          type: object
          properties:
            model:
              type: string
              default: gpt-image-1
              description: The image model to use for generation. Use the `/v1/meta/image_models` endpoint to get available image models.
            max_body_images_count:
              type: integer
              minimum: 0
              maximum: 7
              description: The maximum number of images to generate in the body. Use `0` to disable body images.
            style_preset:
              type: string
              enum:
                [
                  vintage,
                  professional,
                  photorealistic,
                  infographic,
                  minimalist_art,
                  comic,
                  watercolor_painting,
                  abstract_art,
                  pop_art,
                  model_3d,
                  line_art,
                ]
              nullable: true
              default: photorealistic
              description: The style preset for the images. Use `custom_style` if you prefer a custom style instead.
            custom_style:
              type: object
              required:
                - name
                - description
              properties:
                name:
                  type: string
                  description: The name of the custom style.
                description:
                  type: string
                  description: The description of the custom style. This should be a short but detailed description of the style you want to apply to the images.
              description: The custom style for the images.
        advanced_settings:
          type: object
          properties:
            title_prompt:
              type: string
              maxLength: 20000
              nullable: true
              description: The prompt to use for generating the title.
            introduction_prompt:
              type: string
              maxLength: 20000
              nullable: true
              description: The prompt to use for generating the introduction.
            section_prompt:
              type: string
              maxLength: 20000
              nullable: true
              description: The prompt to use for generating the sections.
            meta_description_prompt:
              type: string
              maxLength: 20000
              nullable: true
              description: The prompt to use for generating the meta description.

    Project:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        articles:
          type: object
          description: A record of article IDs and their corresponding task status
          additionalProperties:
            $ref: '#/components/schemas/TaskStatus'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    ExportedArticle:
      type: object
      description: An exported article with the requested fields
      properties:
        title:
          type: string
          description: The article title
        slug:
          type: string
          description: URL-friendly slug derived from the target keyword
        content:
          type: string
          description: The full HTML content of the article
        excerpt:
          type: string
          nullable: true
          description: The meta description or excerpt
        date:
          type: string
          format: date-time
          description: The article creation date
        image:
          type: string
          nullable: true
          description: URL of the featured image
        target_keyword:
          type: string
          description: The target keyword for the article
        keywords:
          type: string
          nullable: true
          description: Additional keywords
        language:
          type: string
          description: The language of the article (e.g., "English")
        model:
          type: string
          description: The AI model used for generation
        pov:
          type: string
          description: The point of view (e.g., "First Person Plural")
        tone:
          type: string
          description: The tone of voice (e.g., "Professional")

    Site:
      type: object
      properties:
        id:
          type: string
          format: uuid
        domain:
          type: string
          description: The domain/URL of the site
        icon_url:
          type: string
          nullable: true
          description: URL to the site favicon or icon
        status:
          type: string
          enum: [active, inactive]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    BrandContext:
      type: object
      properties:
        company_name:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
          description: Company/brand description
        industry:
          type: string
          nullable: true
        target_audience:
          type: string
          nullable: true
        value_proposition:
          type: string
          nullable: true
        competitors:
          type: object
          nullable: true
          description: JSON object containing competitor information
        ranking_keywords:
          type: object
          nullable: true
          description: JSON object containing keyword data
        pain_points:
          type: object
          nullable: true
          description: JSON object containing customer pain points
        faq_questions:
          type: object
          nullable: true
          description: JSON object containing FAQ data

    BrandVoice:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        is_active:
          type: boolean
        values:
          type: object
          description: JSON object containing voice parameters (tone, persona, formality, vocabulary_style, sentence_structure, etc.)

    BrandVisualStyle:
      type: object
      properties:
        logo_url:
          type: string
          nullable: true
        primary_color:
          type: string
          nullable: true
        secondary_color:
          type: string
          nullable: true
        accent_color:
          type: string
          nullable: true
        primary_font:
          type: string
          nullable: true
        visual_mood:
          type: string
          nullable: true

    ContentStrategySettings:
      type: object
      description: Content strategy settings for a site
      properties:
        article_defaults:
          type: object
          properties:
            model:
              type: string
              description: AI model for article generation
            generate_meta_description:
              type: boolean
            generate_faq:
              type: boolean
            include_introduction:
              type: boolean
            include_conclusion:
              type: boolean
            generate_ai_images:
              type: boolean
            target_length:
              type: string
              enum: [short, medium, long, comprehensive]
            extra_prompt:
              type: string
              nullable: true
        social_publishing:
          type: object
          properties:
            enabled:
              type: boolean
            auto_post_on_publish:
              type: boolean
            default_platforms:
              type: array
              items:
                type: string
            default_tone:
              type: string
              enum: [professional, casual, humorous, educational, inspirational]
            include_article_link:
              type: boolean
            include_hashtags:
              type: boolean
            max_hashtags:
              type: integer
            schedule_delay_minutes:
              type: integer
            generate_images:
              type: boolean
        newsletter:
          type: object
          properties:
            enabled:
              type: boolean
            cadence:
              type: string
              enum: [weekly, biweekly, monthly, none]
            include_recent_articles:
              type: boolean
            max_articles_per_newsletter:
              type: integer
            send_day:
              type: string
              enum: [monday, tuesday, wednesday, thursday, friday, saturday, sunday]
            send_hour:
              type: integer
        automation:
          type: object
          properties:
            planner_paused:
              type: boolean
            auto_schedule_from_clusters:
              type: boolean
            default_posts_per_week:
              type: integer
            preferred_publish_days:
              type: array
              items:
                type: string
            preferred_publish_hour:
              type: integer
            auto_generate_social_on_publish:
              type: boolean
            auto_add_to_newsletter:
              type: boolean

    Preset:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
          nullable: true
        target_content_type:
          type: string
          enum: [general, location_page]
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    PresetDetail:
      allOf:
        - $ref: '#/components/schemas/Preset'
        - type: object
          properties:
            presets:
              type: object
              properties:
                language:
                  type: string
                location:
                  type: string
                tone:
                  type: string
                  enum: [seo_optimized, casual, excited, formal, friendly, humorous, professional]
                pov:
                  type: string
                  enum: [first_person_singular, first_person_plural, second_person, third_person]
                article_type:
                  type: string
                  enum: [general, listicle, review, howto]
                target_length:
                  type: string
                  enum: [short, medium, long]
                enable_pplx_fetch:
                  type: boolean
                include_introduction:
                  type: boolean
                include_conclusion:
                  type: boolean
                include_faq:
                  type: boolean
                include_featured_image:
                  type: boolean
                include_meta_description:
                  type: boolean
                include_youtube_videos:
                  type: boolean
                position_key_takeaways:
                  type: string
                  enum: [none, top, bottom]
                include_ai_images:
                  type: boolean
                ai_image_meta:
                  type: object
                  description: Image generation settings stored in the preset.
                  properties:
                    image_model:
                      type: string
                      description: "Image model (e.g. gpt-image-1, flux-schnell, flux-dev, flux-pro)"
                    image_style:
                      type: string
                      description: "Image style (professional, photorealistic, cinematic, auto, etc.)"
                    image_count_body:
                      type: integer
                      minimum: 0
                      maximum: 20
                      description: Max number of images in the article body
                    use_brand_colors:
                      type: boolean
                      description: Apply brand kit colors to generated images
                    image_extra_prompt:
                      type: string
                      description: Custom instructions for image generation
                extra_prompt:
                  type: string
                knowledge_sources:
                  type: array
                  items:
                    type: integer

    CreatePreset:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          maxLength: 100
        description:
          type: string
          maxLength: 500
        presets:
          type: object
          description: Preset settings. All fields are optional.
          properties:
            language:
              type: string
            location:
              type: string
            tone:
              type: string
              enum: [seo_optimized, casual, excited, formal, friendly, humorous, professional]
            pov:
              type: string
              enum: [first_person_singular, first_person_plural, second_person, third_person]
            article_type:
              type: string
              enum: [general, listicle, review, howto]
            target_length:
              type: string
              enum: [short, medium, long]
            include_ai_images:
              type: boolean
            ai_image_meta:
              type: object
              description: Image generation settings.
              properties:
                image_model:
                  type: string
                image_style:
                  type: string
                image_count_body:
                  type: integer
                  minimum: 0
                  maximum: 20
                use_brand_colors:
                  type: boolean
                image_extra_prompt:
                  type: string
            include_introduction:
              type: boolean
            include_conclusion:
              type: boolean
            include_faq:
              type: boolean
            include_meta_description:
              type: boolean
            extra_prompt:
              type: string
              maxLength: 50000
            knowledge_sources:
              type: array
              items:
                type: integer

    KnowledgeSource:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
          nullable: true
        source_type:
          type: string
          enum: [text, file, url]
        indexing:
          type: object
          nullable: true
          properties:
            chunks_indexed:
              type: integer
            total_chunks:
              type: integer
            was_limited:
              type: boolean
            indexed_at:
              type: string
              format: date-time
              nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    Cluster:
      type: object
      properties:
        id:
          type: string
          format: uuid
        site_id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
          nullable: true
        in_plan:
          type: boolean
          description: Whether this cluster is active in content planning
        pillar_page:
          type: string
          nullable: true
          description: URL of the pillar page for this cluster
        total_keywords:
          type: integer
        total_search_volume:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    ClusterDetail:
      allOf:
        - $ref: '#/components/schemas/Cluster'
        - type: object
          properties:
            keywords:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  keyword:
                    type: string
                  search_volume:
                    type: integer
                    nullable: true
                  keyword_difficulty:
                    type: number
                    nullable: true
                  cpc:
                    type: number
                    nullable: true
                  search_intent:
                    type: string
                    nullable: true
                  article_id:
                    type: string
                    nullable: true
                    description: Article ID if content has been generated for this keyword

    PlannerEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        team_id:
          type: string
          format: uuid
        site_id:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
        description:
          type: string
          nullable: true
        event_date:
          type: string
          format: date
        event_type:
          type: string
          enum: [holiday, cultural, industry, custom, product_launch, seasonal]
        recurrence:
          type: string
          enum: [none, annual, monthly, weekly]
        source:
          type: string
          enum: [user, system, nager]
        country_code:
          type: string
          nullable: true
        metadata:
          type: object
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PlannerItem:
      type: object
      description: Content planner item. Uses keyword_id as the unique identifier.
      properties:
        keyword_id:
          type: string
          format: uuid
          description: The unique identifier for this planner item (also the keyword ID)
        site_id:
          type: string
          format: uuid
        cluster_id:
          type: string
          format: uuid
        date:
          type: string
          format: date
          description: Scheduled date for content creation
        status:
          type: string
          enum: [scheduled, progress, created, published]
        keyword:
          type: string
          nullable: true
          description: The keyword text
        cluster_title:
          type: string
          nullable: true
          description: Title of the parent cluster
        metadata:
          type: object
          nullable: true
          description: Additional metadata (may contain article_id after content is created)
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    GenerateSocialPost:
      type: object
      required:
        - site_id
        - platform
      description: |
        Provide article_title + article_summary for article-derived posts,
        or provide topic for standalone posts.
      properties:
        site_id:
          type: string
          format: uuid
        article_id:
          type: string
          format: uuid
          nullable: true
        topic:
          type: string
          maxLength: 500
          description: Standalone topic (alternative to article_title/article_summary)
        article_title:
          type: string
          description: Required if topic not provided
        article_summary:
          type: string
          description: Required if topic not provided
        article_url:
          type: string
          format: uri
          nullable: true
        platform:
          type: string
          enum:
            [
              twitter,
              instagram,
              facebook,
              linkedin,
              tiktok,
              youtube,
              pinterest,
              reddit,
              bluesky,
              threads,
            ]
        tone:
          type: string
          enum: [casual, professional, witty, inspiring, educational, engaging]
          default: engaging
        include_hashtags:
          type: boolean
          default: true
        max_hashtags:
          type: integer
          minimum: 0
          maximum: 30
        hook_type:
          type: string
          enum: [curiosity, story, value, contrarian, social_proof]
        generate_image:
          type: boolean
          default: false

    SocialPost:
      type: object
      properties:
        content:
          type: string
        hashtags:
          type: array
          items:
            type: string
        image_prompt:
          type: string
        image_url:
          type: string
          nullable: true
        platform:
          type: string
        character_count:
          type: integer
        hook:
          type: string
          nullable: true
        metadata:
          type: object
          properties:
            tokens_used:
              type: integer
            model:
              type: string
            generated_at:
              type: string
              format: date-time
            brand_context_used:
              type: boolean

    SocialPostRecord:
      type: object
      properties:
        id:
          type: string
          format: uuid
        platform:
          type: string
        content:
          type: string
        hashtags:
          type: array
          items:
            type: string
        image_url:
          type: string
          nullable: true
        status:
          type: string
          enum: [draft, scheduled, published, failed]
        scheduled_at:
          type: string
          format: date-time
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true
        article_id:
          type: string
          format: uuid
          nullable: true
        late_post_id:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
        metadata:
          type: object
          nullable: true
        post_type:
          type: string
          nullable: true
        voice_mode:
          type: string
          nullable: true
        site_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    MoodBoard:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        site_id:
          type: string
          format: uuid
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    LinkEngineSite:
      type: object
      properties:
        id:
          type: string
          format: uuid
        domain:
          type: string
        link_engine_status:
          type: string
          enum: [active, paused, indexing, inactive, error]
        link_engine_url_count:
          type: integer
        link_engine_last_indexed_at:
          type: string
          format: date-time
          nullable: true
        link_engine_error:
          type: string
          nullable: true
        category_id:
          type: string
          format: uuid
          nullable: true
        category_name:
          type: string
          nullable: true
        has_link_config:
          type: boolean
        created_at:
          type: string
          format: date-time

    LinkEngineStats:
      type: object
      properties:
        total_sites:
          type: integer
        active_sites:
          type: integer
        paused_sites:
          type: integer
        indexing_sites:
          type: integer
        error_sites:
          type: integer
        total_urls:
          type: integer

    LinkCategory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        description:
          type: string
          nullable: true
        color:
          type: string
        site_count:
          type: integer
        created_at:
          type: string
          format: date-time

    SerpData:
      type: object
      nullable: true
      description: SERP competitor analysis data
      properties:
        id:
          type: string
          format: uuid
        article_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, complete, failed]
        target_keyword:
          type: string
          nullable: true
        location:
          type: string
          nullable: true
        competitor_count:
          type: integer
        avg_word_count:
          type: integer
          nullable: true
        keyword_intent:
          type: string
          nullable: true
          enum: [informational, commercial, transactional, navigational]
        paa_questions:
          type: array
          items:
            type: string
          description: People Also Ask questions
        top_competitors:
          type: array
          items:
            type: object
            properties:
              rank:
                type: integer
              url:
                type: string
              title:
                type: string
              word_count:
                type: integer
                nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    OptimizationResult:
      type: object
      description: AI-powered optimization suggestions based on SERP analysis
      properties:
        mode:
          type: string
          enum: [ai, quick]
          description: Whether AI or quick rule-based suggestions were used
        summary:
          type: string
        estimated_score_improvement:
          type: integer
        suggestions:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              type:
                type: string
                enum: [content, structure, seo, readability]
              priority:
                type: string
                enum: [high, medium, low]
              title:
                type: string
              description:
                type: string
              content:
                type: string
                nullable: true
                description: Suggested content to add/modify
              location:
                type: string
                nullable: true
                description: Where in the article to apply this suggestion
              impact_score:
                type: number
                nullable: true
              terms:
                type: array
                items:
                  type: string
                description: Related terms for this suggestion
        serp_analysis:
          type: object
          properties:
            overall_score:
              type: number
            term_coverage:
              type: number
            intent_alignment:
              type: number
            structure_match:
              type: number
            paa_coverage:
              type: number
            missing_terms:
              type: array
              items:
                type: string
            missing_topics:
              type: array
              items:
                type: string
            unanswered_questions:
              type: array
              items:
                type: string

    ContentGrade:
      type: object
      description: Content grade with SEO, readability, and SERP optimization scores
      properties:
        overall_score:
          type: integer
          minimum: 0
          maximum: 100
          description: Weighted average of all category scores
        seo_score:
          type: integer
          minimum: 0
          maximum: 100
        readability_score:
          type: integer
          minimum: 0
          maximum: 100
        slop_score:
          type: integer
          minimum: 0
          maximum: 100
          description: AI writing quality score (higher = less generic AI patterns)
        ai_search_score:
          type: integer
          minimum: 0
          maximum: 100
          description: AI search optimization (E-E-A-T signals, direct answers, etc.)
        structure_score:
          type: integer
          minimum: 0
          maximum: 100
        technical_score:
          type: integer
          minimum: 0
          maximum: 100
        word_count:
          type: integer
          nullable: true
        target_keyword:
          type: string
          nullable: true
        graded_at:
          type: string
          format: date-time
        grader_version:
          type: string
        has_serp_data:
          type: boolean
          description: Whether SERP optimization analysis is included
        serp_score:
          type: object
          nullable: true
          description: SERP optimization scores (only present if has_serp_data is true)
          properties:
            overall:
              type: number
            term_coverage:
              type: number
              description: Coverage of competitor terms in article
            intent_alignment:
              type: number
              description: How well content matches search intent
            structure_match:
              type: number
              description: Similarity to top-ranking content structure
            paa_coverage:
              type: number
              description: Coverage of People Also Ask questions

    # ─── New Schemas ────────────────────────────────────────────────────────────

    TemplateListItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        site_id:
          type: string
          format: uuid
          nullable: true
        variables:
          type: object
          additionalProperties:
            type: string
        include_faq:
          type: boolean
        include_key_takeaways:
          type: boolean
        include_ai_images:
          type: boolean
        enable_knowledge:
          type: boolean
        enable_perplexity:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    TemplateDetail:
      allOf:
        - $ref: '#/components/schemas/TemplateListItem'
        - type: object
          properties:
            system_prompt:
              type: string
            user_prompt:
              type: string
            ideal_output:
              type: string
              nullable: true
            knowledge_sources:
              type: array
              items:
                type: integer
            include_toc:
              type: boolean
            enable_serp:
              type: boolean
            ai_image_style:
              type: string
              nullable: true
            ai_image_model:
              type: string
              nullable: true
            ai_image_count_body:
              type: integer

    NotebookEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        content:
          type: string
        meeting_date:
          type: string
          format: date
          nullable: true
        source:
          type: string
        processing_status:
          type: string
          enum: [pending, processing, completed, failed]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
          nullable: true

    Agent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        agent_type:
          type: string
          enum: [local_news, content_gap, trend_research, programmatic_seo]
        site_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [active, paused, running, error]
        schedule:
          type: string
          enum: [weekly, biweekly, monthly, manual]
        run_count:
          type: integer
        last_run_at:
          type: string
          format: date-time
          nullable: true
        next_run_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    AgentDetail:
      allOf:
        - $ref: '#/components/schemas/Agent'
        - type: object
          properties:
            config:
              type: object
              description: Agent-type-specific configuration
            sources:
              type: array
              items:
                type: object
              description: Data sources configuration
            latest_snapshot:
              type: object
              nullable: true
              description: Most recent agent run snapshot with crawl stats and extracted data
            latest_brief:
              type: object
              nullable: true
              description: Most recent research brief with content ideas and keywords

    PerformanceOverview:
      type: object
      properties:
        kpis:
          type: object
          properties:
            organicClicks:
              type: object
              properties:
                value: { type: integer }
                pctChange: { type: integer }
            organicImpressions:
              type: object
              properties:
                value: { type: integer }
                pctChange: { type: integer }
            ctr:
              type: object
              properties:
                value: { type: number }
                pctChange: { type: integer }
            aiCitations:
              type: object
              properties:
                value: { type: integer }
            socialReach:
              type: object
              properties:
                value: { type: integer }
                pctChange: { type: integer }
            sessions:
              type: object
              properties:
                value: { type: integer }
            avgContentGrade:
              type: object
              properties:
                value: { type: number }
        visibilityScore:
          type: object
          nullable: true
          properties:
            score: { type: number }
            organicScore: { type: number }
            aiVisibilityScore: { type: number }
            contentQualityScore: { type: number }
            socialScore: { type: number }
            change: { type: number }
        quickWins:
          type: object
          properties:
            pagesLosingTraffic: { type: integer }
            pagesNotCitedByAi: { type: integer }
            pagesAgingOut: { type: integer }
            strikingDistanceKeywords: { type: integer }

    ContentHealth:
      type: object
      properties:
        topPages:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              clicks: { type: integer }
              impressions: { type: integer }
              position: { type: number }
        contentDecay:
          type: array
          items:
            type: object
            properties:
              url: { type: string }
              currentClicks: { type: integer }
              previousClicks: { type: integer }
              dropPct: { type: integer }
        strikingDistance:
          type: array
          items:
            type: object
            properties:
              query: { type: string }
              page_url: { type: string }
              position: { type: number }
              impressions: { type: integer }
              clicks: { type: integer }
        cannibalization:
          type: array
          items:
            type: object
            properties:
              query: { type: string }
              pages:
                type: array
                items:
                  type: object
                  properties:
                    url: { type: string }
                    clicks: { type: integer }
                    impressions: { type: integer }
                    position: { type: number }

    AiVisibility:
      type: object
      properties:
        overview:
          type: object
          properties:
            totalMentions: { type: integer }
            chatgptMentions: { type: integer }
            googleAioMentions: { type: integer }
            uniquePrompts: { type: integer }
        topPrompts:
          type: array
          items:
            type: object
            properties:
              prompt: { type: string }
              llmSources:
                type: array
                items: { type: string }
              pageUrl: { type: string, nullable: true }
              firstSeen: { type: string }
        topCitedPages:
          type: array
          items:
            type: object
            properties:
              pageUrl: { type: string }
              citations: { type: integer }
        citationGaps:
          type: array
          items:
            type: object
            properties:
              prompt: { type: string }
              competitorDomain: { type: string }
              competitorUrl: { type: string, nullable: true }
              yourCoverage: { type: boolean }

    Backlinks:
      type: object
      properties:
        connected: { type: boolean }
        overview:
          type: object
          properties:
            domainRating: { type: number, nullable: true }
            ahrefsRank: { type: integer, nullable: true }
            backlinksLive: { type: integer, nullable: true }
            backlinksAllTime: { type: integer, nullable: true }
            refdomainsLive: { type: integer, nullable: true }
            refdomainsAllTime: { type: integer, nullable: true }
        referringDomains:
          type: array
          items:
            type: object
            properties:
              domain: { type: string }
              domainRating: { type: number, nullable: true }
              backlinks: { type: integer }
              firstSeen: { type: string, nullable: true }
              lastSeen: { type: string, nullable: true }

    Newsletter:
      type: object
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        week_start: { type: string, format: date }
        status: { type: string }
        subject: { type: string, nullable: true }
        article_ids:
          type: array
          nullable: true
          items: { type: string, format: uuid }
        metadata: { type: object, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time, nullable: true }

    NewsletterDetail:
      allOf:
        - $ref: '#/components/schemas/Newsletter'
        - type: object
          properties:
            content:
              type: object
              description: Structured newsletter content (intro, article summaries, outro)
            html_content:
              type: string
              nullable: true
              description: Full HTML email content

  # ──────── Performance Hub ────────

  /v1/performance/overview:
    get:
      summary: Get brand performance overview
      description: Returns KPIs (organic clicks, impressions, CTR, AI citations, social reach, sessions, content grade), Brand Visibility Score breakdown, and Quick Wins.
      tags: [Performance]
      parameters:
        - name: site_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Performance overview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PerformanceOverview'

  /v1/performance/content-health:
    get:
      summary: Get content health analysis
      description: Returns top pages by clicks, content decay (pages losing traffic), striking distance keywords, and keyword cannibalization.
      tags: [Performance]
      parameters:
        - name: site_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: Content health data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentHealth'

  /v1/performance/ai-visibility:
    get:
      summary: Get AI visibility data
      description: Returns LLM mentions (ChatGPT, Google AI Overview), top prompts, top cited pages, and citation gaps vs competitors.
      tags: [Performance]
      parameters:
        - name: site_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: days
          in: query
          schema:
            type: integer
            default: 30
            maximum: 365
        - name: limit
          in: query
          schema:
            type: integer
            default: 25
            maximum: 100
      responses:
        '200':
          description: AI visibility data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiVisibility'

  /v1/performance/backlinks:
    get:
      summary: Get backlink profile
      description: Returns domain rating, backlink counts, and top referring domains. Requires Ahrefs connection.
      tags: [Performance]
      parameters:
        - name: site_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
      responses:
        '200':
          description: Backlink data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Backlinks'

  # ──────── Newsletters ────────

  /v1/newsletters:
    get:
      summary: List newsletters
      description: List generated newsletters for a site.
      tags: [Newsletters]
      parameters:
        - name: site_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            type: string
            enum: [scheduled, ready, sent]
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Newsletter list
          content:
            application/json:
              schema:
                type: object
                properties:
                  newsletters:
                    type: array
                    items:
                      $ref: '#/components/schemas/Newsletter'
                  total:
                    type: integer

  /v1/newsletters/{id}:
    get:
      summary: Get newsletter detail
      description: Get full newsletter including content, HTML, and metadata.
      tags: [Newsletters]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Newsletter detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NewsletterDetail'

  /v1/newsletters/generate:
    post:
      summary: Generate newsletter
      description: Generate a weekly newsletter roundup from published articles. Uses brand context for tone.
      tags: [Newsletters]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [site_id]
              properties:
                site_id:
                  type: string
                  format: uuid
                week_start:
                  type: string
                  format: date
                  description: Monday of the target week (YYYY-MM-DD). Defaults to current week.
                model:
                  type: string
                  description: AI model for generation (default gpt-5)
      responses:
        '200':
          description: Newsletter generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  subject:
                    type: string
                  articleCount:
                    type: integer
                  status:
                    type: string
