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.

    ### 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"
    ```

    ### 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: 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

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. Enables Brand Voice, site context, and Link Engine.
                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.
                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/{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

  /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/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/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 (WordPress, 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: [wordpress, ghost, webflow, contentful, sanity, airtable]
                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
      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
                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. 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/platforms:
    get:
      summary: Get supported social platforms
      description: Returns all supported social platforms with their character limits and best practices.
      responses:
        '200':
          description: List of platforms
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    label:
                      type: string
                    max_characters:
                      type: integer
                    hashtags_supported:
                      type: boolean
                    max_hashtags:
                      type: integer
      tags:
        - Social

  /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

  # ─── 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

  # ─── 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.
                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 approved clusters from the Airtable output table, creates articles in Cuppa, and dispatches generation.
        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: approved
                    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
      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

    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
                pov:
                  type: string
                article_type:
                  type: string
                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

    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

    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
        - article_title
        - article_summary
        - platform
      properties:
        site_id:
          type: string
          format: uuid
        article_id:
          type: string
          format: uuid
          nullable: true
        article_title:
          type: string
        article_summary:
          type: string
        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

    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
