openapi: 3.1.0
info:
  title: CoreProse API
  description: |
    API for programmatic access to CoreProse content generation platform.

    ## Authentication

    All API requests require authentication via API key. Include your API key in the `X-API-Key` header:

    ```
    X-API-Key: cp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ```

    API keys can be created and managed from your [dashboard](/api-keys).

    Public endpoints (under `/public/*`) do not require authentication.

    ## Rate Limits

    Rate limits vary by plan:
    - **Free**: 100 requests/minute
    - **Starter**: 200 requests/minute
    - **Pro**: 500 requests/minute
    - **Business**: 2000 requests/minute

    ## Errors

    The API uses standard HTTP status codes:
    - `200` - Success
    - `400` - Bad Request (invalid parameters)
    - `401` - Unauthorized (missing or invalid API key)
    - `403` - Forbidden (insufficient permissions or quota exceeded)
    - `404` - Not Found
    - `409` - Conflict (duplicate resource)
    - `422` - Unprocessable Entity (e.g. KB coverage too low)
    - `429` - Too Many Requests (rate limit exceeded)
    - `500` - Internal Server Error

  version: 1.0.0
  contact:
    name: CoreProse Support
    email: support@coreprose.com
    url: https://www.coreprose.com
  license:
    name: Proprietary
    url: https://www.coreprose.com/terms

servers:
  - url: https://www.coreprose.com/api
    description: Production server

security:
  - ApiKeyAuth: []

tags:
  - name: Articles
    description: Article generation and management
  - name: Agents
    description: AI agent management
  - name: Niches
    description: Niche/topic management
  - name: KB Coverage
    description: Knowledge Base coverage and SEO analysis
  - name: Integrations
    description: Third-party integrations (WordPress, Shopify, etc.)
  - name: Documents
    description: Private document management
  - name: Tenant
    description: Account and quota information
  - name: Platform Admin
    description: Platform admin only — requires API key linked to a platform_admin user
  - name: Public
    description: Public endpoints (no authentication required)
  - name: Team
    description: Team member management and invitations
  - name: Account
    description: Account profile management
  - name: Reviews
    description: User reviews and ratings
  - name: Translation
    description: |
      Translate articles, text, and documents to 13 languages.
      Credits are consumed based on word count tiers (1-15 credits per translation).
      Plans: Starter (30/mo), Pro (150/mo), Business (500/mo).

paths:
  # ==================== ARTICLES ====================
  /articles:
    get:
      tags: [Articles]
      summary: List articles
      description: Get a paginated list of articles with optional filtering
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: nicheKey
          in: query
          description: Filter by niche key (e.g., "ia-sante")
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, generating, published, scheduled, failed]
        - name: search
          in: query
          description: Search in title/content
          schema:
            type: string
        - name: sortBy
          in: query
          schema:
            type: string
            enum: [createdAt, updatedAt, publishedAt]
            default: createdAt
        - name: sortOrder
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
      responses:
        '200':
          description: List of articles
          content:
            application/json:
              schema:
                type: object
                properties:
                  articles:
                    type: array
                    items:
                      $ref: '#/components/schemas/Article'
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
                  totalPages:
                    type: integer

  /writer/{id}:
    get:
      tags: [Articles]
      summary: Get article by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Article details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '404':
          description: Article not found

    patch:
      tags: [Articles]
      summary: Update article
      description: Update article title, content, status, or SEO metadata
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                content:
                  type: string
                excerpt:
                  type: string
                status:
                  type: string
                  enum: [draft, published, scheduled]
                scheduledFor:
                  type: string
                  format: date-time
                featuredImage:
                  type: string
                  description: Featured image URL. If not provided when isFreeGeneration is set, auto-fetched from Unsplash.
                trendSlug:
                  type: string
                  description: Associate article with a trend (validated against existing trends, 400 if not found)
                seo:
                  type: object
                  properties:
                    metaTitle:
                      type: string
                    metaDescription:
                      type: string
                # Platform admin only fields:
                isPublic:
                  type: boolean
                  description: (platform_admin) Make article publicly accessible
                publicCategory:
                  type: string
                  description: (platform_admin) Public category — trend-radar for /article/ route
                  enum: [trend-radar, hallucinations, rag-failures, prompt-injection, data-leaks, bias, regulation, safety, other]
                isFreeGeneration:
                  type: boolean
                  description: (platform_admin) Mark as free-generated article. Auto-sets freeTopic/trendSlug, checks duplicates (409), auto-fetches Unsplash image.
      responses:
        '200':
          description: Updated article
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'

    delete:
      tags: [Articles]
      summary: Delete article
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Article deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean

  /writer/generate:
    post:
      tags: [Articles]
      summary: Generate article directly
      description: |
        Generate a new article directly (without plan review). Returns immediately with `status: "generating"`.
        Poll `GET /writer/{id}` until `status` is `generated` or `failed`.

        When `agentKey` is provided with `topic`, runs KB coverage check server-side
        and blocks generation if coverage is low or insufficient (422).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nicheId]
              properties:
                nicheId:
                  type: string
                  description: Niche ID (required)
                topic:
                  type: string
                  description: Subject of the article (optional)
                agentKey:
                  type: string
                  description: "Agent key (e.g. 'ai-engineering', 'ia-FR'). When provided with topic, runs KB coverage check."
                tone:
                  type: string
                  enum: [professional, casual, educational, journalistic]
                language:
                  type: string
                  description: ISO language code
                  enum: [fr, en, es, de, it, pt, nl, pl, sv, no, da, fi, cs, hu, ro, el, tr, uk, zh, ja, ko, th, vi, id, hi, ar, ru, he]
                keywords:
                  type: array
                  items:
                    type: string
                includeMermaid:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  articleId:
                    type: string
                  status:
                    type: string
                    enum: [generating]
        '403':
          description: Monthly article quota exceeded
        '409':
          description: Same topic already generating for this tenant
        '422':
          description: KB coverage too low/insufficient (only when agentKey is provided)

  /writer/generate-plan:
    post:
      tags: [Articles]
      summary: Generate article plan
      description: |
        Creates an article draft and generates a content plan.
        Consumes 1 article from your monthly quota.

        With `autoValidate: true`, the article is generated immediately.
        Otherwise, returns a plan for review before generation.

        ## Recommended flow

        1. `POST /writer/check-kb-coverage` -> get `coverage.level` + `coverage.rerankedResults`
        2. `POST /writer/generate-plan` with:
           - `coverageLevel`: level from step 1 (triggers enforcement)
           - `coverageSources`: rerankedResults from step 1 (avoids duplicate KB search)
           - `allowLowCoverage`: true to force generation despite low/insufficient coverage

        ## Coverage enforcement

        | coverageLevel   | allowLowCoverage | Result                              |
        |-----------------|------------------|-------------------------------------|
        | high / medium   | any              | Generation proceeds                 |
        | low             | false (default)  | 422 - low coverage blocked          |
        | low             | true             | Generation forced (lower quality)   |
        | insufficient    | false (default)  | 422 - no sources found              |
        | insufficient    | true             | Generation forced (risky)           |
        | absent          | any              | KB re-queried automatically         |

        Business plan bypasses all coverage blocks regardless of `allowLowCoverage`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentKey]
              properties:
                agentKey:
                  type: string
                  description: Agent key in format "nicheKey-XX" (e.g., "ia-FR")
                  pattern: "^[a-z0-9-]+-[A-Z]{2}$"
                  example: "ia-FR"
                topic:
                  type: string
                  maxLength: 500
                  description: Article topic (optional, auto-suggested if not provided)
                tone:
                  type: string
                  enum: [professional, casual, expert, informative, persuasive, educational, creative, technical, friendly]
                  default: professional
                language:
                  type: string
                  enum: [fr, en, es, de, it, pt, nl, pl, sv, no, da, fi, cs, hu, ro, el, tr, uk, zh, ja, ko, th, vi, id, hi, ar, ru, he]
                  description: ISO language code
                  default: fr
                keywords:
                  type: array
                  items:
                    type: string
                    maxLength: 100
                  maxItems: 20
                  description: Keywords to include (max 20)
                articleLength:
                  type: string
                  enum: [short, standard, long, very_long]
                  default: standard
                autoValidate:
                  type: boolean
                  description: If true, skip plan review and generate immediately
                  default: false
                includeMermaid:
                  type: boolean
                  description: Include Mermaid diagrams in the article (Pro/Business)
                  default: false
                privateDocsMode:
                  type: string
                  enum: [disabled, manual, auto]
                  description: Private documents RAG mode
                  default: disabled
                documentIds:
                  type: array
                  items:
                    type: string
                  maxItems: 10
                  description: Document IDs for manual privateDocsMode (max 10)
                coverageLevel:
                  type: string
                  enum: [high, medium, low, insufficient]
                  description: |
                    Coverage level returned by check-kb-coverage.
                    If 'low' or 'insufficient', generation is blocked unless allowLowCoverage is true.
                coverageSources:
                  type: array
                  description: |
                    Pre-fetched KB sources from check-kb-coverage (coverage.rerankedResults).
                    Pass this to avoid a duplicate KB search during plan generation.
                  items:
                    type: object
                    properties:
                      parentId:
                        type: string
                        description: KB document ID
                      parentTitle:
                        type: string
                      parentUrl:
                        type: string
                      chunkIndex:
                        type: integer
                      content:
                        type: string
                      combinedScore:
                        type: number
                      rerankScore:
                        type: number
                      originalScore:
                        type: number
                allowLowCoverage:
                  type: boolean
                  default: false
                  description: |
                    If false (default): block generation when coverageLevel is 'low' or 'insufficient'.
                    If true: force generation despite poor coverage (caller accepts quality risk).
                    Business plan always bypasses this check.
      responses:
        '200':
          description: Article plan or generation started
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Plan for review (autoValidate=false)
                    properties:
                      type:
                        type: string
                        enum: [plan]
                      articleId:
                        type: string
                      plan:
                        $ref: '#/components/schemas/ArticlePlan'
                      kbSources:
                        type: array
                        items:
                          type: object
                  - type: object
                    description: Generation started (autoValidate=true)
                    properties:
                      type:
                        type: string
                        enum: [generating]
                      articleId:
                        type: string
                      message:
                        type: string
        '403':
          description: Quota exceeded or article length not available for plan
        '422':
          description: |
            KB coverage too low to generate a quality article.
            Pass allowLowCoverage: true to force generation, or enrich your KB first.

  /writer/generate-from-plan:
    post:
      tags: [Articles]
      summary: Generate article from plan
      description: Generate full article content from a previously created plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [articleId]
              properties:
                articleId:
                  type: string
                plan:
                  type: object
                  description: Modified plan (optional, uses original plan if not provided)
                nicheId:
                  type: string
      responses:
        '200':
          description: Generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  articleId:
                    type: string
                  status:
                    type: string

  /writer/generation-status/{articleId}:
    get:
      tags: [Articles]
      summary: Get article generation status
      description: Poll for article generation progress
      parameters:
        - name: articleId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Generation status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [draft, generating, published, failed]
                  progress:
                    type: integer
                    description: Progress percentage (0-100)
                  article:
                    $ref: '#/components/schemas/Article'

  /writer/check-kb-coverage:
    post:
      tags: [Articles]
      summary: Check KB coverage for topic
      description: |
        Pre-flight check to verify if Knowledge Base has sufficient coverage for a topic.
        Rate limited by plan (Free: 403, Starter: 5/day, Pro: 10/day, Business: unlimited).

        Use this before calling generate-plan. Pass the response fields directly:
        - `coverage.level` -> `coverageLevel` in generate-plan
        - `coverage.rerankedResults` -> `coverageSources` in generate-plan (avoids duplicate KB search)

        Validation blocks URLs, script tags, JSON blocks, SQL injection, template literals in topic.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentKey, topic]
              properties:
                agentKey:
                  type: string
                  example: "ia-FR"
                topic:
                  type: string
                  minLength: 10
                  maxLength: 500
                articleLength:
                  type: string
                  enum: [short, standard, long, very_long]
      responses:
        '200':
          description: Coverage analysis
          content:
            application/json:
              schema:
                type: object
                properties:
                  canGenerate:
                    type: boolean
                    description: Whether generation is recommended (false if insufficient and not Business)
                  coverage:
                    type: object
                    properties:
                      level:
                        type: string
                        enum: [insufficient, low, good, excellent]
                        description: Pass this as coverageLevel in generate-plan
                      score:
                        type: number
                        description: Coverage score 0-100
                      sourcesFound:
                        type: integer
                        description: Number of KB sources found for this topic
                      topSources:
                        type: array
                        description: Top 5 matching sources (title + relevance score)
                        items:
                          type: object
                          properties:
                            title:
                              type: string
                            relevance:
                              type: number
                            url:
                              type: string
                      rerankedResults:
                        type: array
                        description: |
                          Full reranked KB sources. Pass as coverageSources in generate-plan
                          to avoid a duplicate KB search during plan generation.
                        items:
                          type: object
                          properties:
                            parentId:
                              type: string
                              description: KB document ID
                            parentTitle:
                              type: string
                            parentUrl:
                              type: string
                            chunkIndex:
                              type: integer
                            content:
                              type: string
                            combinedScore:
                              type: number
                            rerankScore:
                              type: number
                            originalScore:
                              type: number
                  estimatedPlanQuality:
                    type: string
                    enum: [excellent, good, fair, poor]
                  recommendations:
                    type: array
                    items:
                      type: string
                  alternativeTopics:
                    type: array
                    description: Suggested alternative topics when coverage is low/insufficient
                    items:
                      type: string
                  remainingChecks:
                    type: integer
                    description: Remaining checks today (Pro plan only, absent for Business)
        '400':
          description: Invalid agentKey format, topic too short/long, suspicious input pattern
        '403':
          description: Plan does not include coverage checks (Free)
        '429':
          description: Daily check quota exceeded

  /writer/enrich-kb-topic:
    post:
      tags: [Articles]
      summary: On-demand KB enrichment
      description: |
        Enrich KB for under-covered topics. Searches the web and indexes results to KB.

        Plan access: Free/Starter get 403. Pro uses Serper-based search.
        Business uses GPT WebSearch premium with Serper fallback.

        Poll status with `GET /writer/enrichment-status/{taskId}`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agentKey, topic]
              properties:
                agentKey:
                  type: string
                  description: "Agent key in format niche-COUNTRY (e.g. 'ia-FR')"
                  example: "ia-FR"
                topic:
                  type: string
                  minLength: 10
                  maxLength: 500
                  description: Topic to enrich KB for
                nicheLabel:
                  type: string
                  description: Human-readable niche name for validation (optional)
      responses:
        '200':
          description: Enrichment started or rejected
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: Enrichment started
                    properties:
                      success:
                        type: boolean
                        enum: [true]
                      taskId:
                        type: string
                        description: Task ID to poll with enrichment-status
                      searchType:
                        type: string
                        enum: [serper, gpt_premium]
                      insightsIndexed:
                        type: integer
                      queryTimeMs:
                        type: integer
                      remainingEnrichments:
                        type: integer
                  - type: object
                    description: Topic rejected (not relevant to niche)
                    properties:
                      success:
                        type: boolean
                        enum: [false]
                      rejected:
                        type: object
                        properties:
                          reason:
                            type: string
                          confidence:
                            type: number
                      remainingEnrichments:
                        type: integer
        '400':
          description: Invalid agentKey, topic too short/long, suspicious pattern
        '403':
          description: Plan does not include enrichment (Free/Starter)
        '429':
          description: Daily enrichment quota exceeded

  /writer/enrichment-status/{taskId}:
    get:
      tags: [Articles]
      summary: Poll enrichment task progress
      description: |
        Poll enrichment task progress. Task must belong to authenticated tenant.

        Stage progression:
        - pending/queued: 5% (canGenerate=false)
        - searching: 20% (canGenerate=false)
        - fetching: 40% (canGenerate=false)
        - processing: 60% (canGenerate=false)
        - indexing: 80% (canGenerate=false)
        - enriching: 10-95% real-time (canGenerate=false)
        - ready: 90% (canGenerate=true)
        - completed: 100% (canGenerate=true)
        - failed: 0% (canGenerate=false)
      parameters:
        - name: taskId
          in: path
          required: true
          description: MongoDB ObjectId of the enrichment task
          schema:
            type: string
      responses:
        '200':
          description: Task status
          content:
            application/json:
              schema:
                type: object
                properties:
                  taskId:
                    type: string
                  status:
                    type: string
                    enum: [pending, queued, searching, fetching, processing, indexing, enriching, ready, completed, failed]
                  progress:
                    type: integer
                    description: Progress percentage (0-100)
                  message:
                    type: string
                    description: Human-readable status message
                  canGenerate:
                    type: boolean
                    description: Whether enough insights are available to generate an article
                  insightsAdded:
                    type: integer
                  error:
                    type: string
                    description: Error message (only when status is failed)
                  createdAt:
                    type: string
                    format: date-time
                  startedAt:
                    type: string
                    format: date-time
                  completedAt:
                    type: string
                    format: date-time
        '404':
          description: Task not found or does not belong to tenant

  /writer/suggest-topic:
    post:
      tags: [Articles]
      summary: Suggest topic
      description: Get AI-suggested topics based on KB gaps
      responses:
        '200':
          description: Suggested topics

  /writer/validate-topic:
    post:
      tags: [Articles]
      summary: Validate topic
      description: Validate if topic is suitable for a niche
      responses:
        '200':
          description: Validation result

  /writer/available-lengths:
    get:
      tags: [Articles]
      summary: Get available article lengths
      description: Get available article length options for current plan
      responses:
        '200':
          description: Available length options

  /writer/{id}/publish:
    post:
      tags: [Articles]
      summary: Publish article
      description: Publish article to connected integrations
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Article published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'

  /writer/{id}/export-pdf:
    get:
      tags: [Articles]
      summary: Export article as PDF
      description: Export article as PDF-ready HTML with numbered sources
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: PDF-ready HTML content
          content:
            text/html:
              schema:
                type: string
        '404':
          description: Article not found

  # ==================== PLATFORM ADMIN ====================
  /writer/check-trend-slug:
    get:
      tags: [Platform Admin]
      summary: Check if trend already has an article
      description: |
        3-level duplicate detection: freeTopic exact match, trendSlug match, near-duplicate (4 significant words).
        Call before generate/generate-plan to avoid duplicates.
      parameters:
        - name: slug
          in: query
          schema:
            type: string
          description: Trend slug to check
        - name: topic
          in: query
          schema:
            type: string
          description: Topic text to check (at least one of slug or topic required)
      responses:
        '200':
          description: Duplicate check result
          content:
            application/json:
              schema:
                type: object
                properties:
                  exists:
                    type: boolean
                  article:
                    type: object
                    properties:
                      id:
                        type: string
                      title:
                        type: string
                      slug:
                        type: string
                      status:
                        type: string
                  matchedBy:
                    type: string
                    enum: [freeTopic, trendSlug, nearDuplicate]
        '403':
          description: Platform admin only

  /writer/{id}/auto-image:
    post:
      tags: [Platform Admin]
      summary: Auto-fetch Unsplash image for article
      description: |
        Searches Unsplash for a relevant image based on article title.
        Skips if article already has a featuredImage. Use ?force=true to replace.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: force
          in: query
          schema:
            type: string
            enum: ['true']
          description: Force replace existing image
      responses:
        '200':
          description: Image fetched
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  featuredImage:
                    type: string
                  photographer:
                    type: string
        '403':
          description: Platform admin only

  # ==================== AGENTS ====================
  /agents:
    get:
      tags: [Agents]
      summary: List agents
      description: Get all agents for your account (user agents + system agents for activated niches)
      parameters:
        - name: nicheKey
          in: query
          schema:
            type: string
        - name: enabled
          in: query
          schema:
            type: boolean
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  agents:
                    type: array
                    items:
                      $ref: '#/components/schemas/Agent'
                  total:
                    type: integer

    post:
      tags: [Agents]
      summary: Create agent
      description: Create a new agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                nicheKey:
                  type: string
                country:
                  type: string
                enabled:
                  type: boolean
                  default: true
      responses:
        '200':
          description: Agent created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'

  /agents/{id}:
    get:
      tags: [Agents]
      summary: Get agent by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Agent details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'

    patch:
      tags: [Agents]
      summary: Update agent
      description: Update agent configuration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                enabled:
                  type: boolean
      responses:
        '200':
          description: Agent updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'

    delete:
      tags: [Agents]
      summary: Delete agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Agent deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean

  /agents/{id}/toggle:
    post:
      tags: [Agents]
      summary: Enable/disable agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
      responses:
        '200':
          description: Agent toggled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  agent:
                    $ref: '#/components/schemas/Agent'

  /agents/{id}/execute:
    post:
      tags: [Agents]
      summary: Execute agent
      description: Manually trigger agent execution to generate articles
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Execution started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  executionId:
                    type: string

  /agents/{id}/executions:
    get:
      tags: [Agents]
      summary: Get execution history
      description: Get execution history for an agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Execution history
          content:
            application/json:
              schema:
                type: object
                properties:
                  executions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        status:
                          type: string
                        startedAt:
                          type: string
                          format: date-time
                        completedAt:
                          type: string
                          format: date-time

  /agents/{id}/websearch-premium:
    post:
      tags: [Agents]
      summary: Toggle WebSearch Premium
      description: Toggle WebSearch Premium for specific agent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: WebSearch Premium toggled

  /agents/events:
    get:
      tags: [Agents]
      summary: Agent status events (SSE)
      description: SSE endpoint for real-time agent status updates
      responses:
        '200':
          description: Server-Sent Events stream
          content:
            text/event-stream:
              schema:
                type: string

  # ==================== NICHES ====================
  /niches:
    get:
      tags: [Niches]
      summary: List niches
      description: Get all available niches (preconfigured + custom)
      responses:
        '200':
          description: List of niches
          content:
            application/json:
              schema:
                type: object
                properties:
                  niches:
                    type: array
                    items:
                      $ref: '#/components/schemas/Niche'
                  total:
                    type: integer
                  breakdown:
                    type: object
                    properties:
                      preconfigured:
                        type: integer
                      custom:
                        type: integer
                      activated:
                        type: integer

    post:
      tags: [Niches]
      summary: Create custom niche
      description: Create a custom niche (Pro/Business only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                description:
                  type: string
                icon:
                  type: string
      responses:
        '200':
          description: Niche created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Niche'
        '403':
          description: Custom niches not available for plan

  /niches/list:
    get:
      tags: [Niches]
      summary: Simple niche list
      description: Simple list for dropdowns
      responses:
        '200':
          description: Simple niche list

  /niches/activated:
    get:
      tags: [Niches]
      summary: Get activated niches
      description: Get niches that are activated for your account
      responses:
        '200':
          description: Activated niches
          content:
            application/json:
              schema:
                type: object
                properties:
                  niches:
                    type: array
                    items:
                      $ref: '#/components/schemas/Niche'

  /niches/{id}:
    get:
      tags: [Niches]
      summary: Get niche details
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Niche details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Niche'

    patch:
      tags: [Niches]
      summary: Update niche settings
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                icon:
                  type: string
      responses:
        '200':
          description: Niche updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Niche'

    delete:
      tags: [Niches]
      summary: Delete custom niche
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Niche deleted

  /niches/{id}/activate:
    post:
      tags: [Niches]
      summary: Activate niche
      description: |
        Activate a niche for your account. This creates agents for each country selected.
        Consumes agent quota (1 agent = 1 niche x 1 country).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tones, languages, countries]
              properties:
                tones:
                  type: array
                  items:
                    type: string
                    enum: [professional, casual, expert, informative, persuasive, educational, creative, technical, friendly]
                  minItems: 1
                  description: Content tones to use for this niche
                languages:
                  type: array
                  items:
                    type: string
                    enum: [fr, en, es, de, it, pt, nl, pl, sv, no, da, fi, cs, hu, ro, el, tr, uk, zh, ja, ko, th, vi, id, hi, ar, ru, he]
                  minItems: 1
                  description: Languages for content generation
                countries:
                  type: array
                  items:
                    type: string
                  minItems: 1
                  description: Country codes (ISO 3166-1 alpha-2) to activate (e.g., ["FR", "US"])
                targetAudience:
                  type: string
                  maxLength: 500
                  description: Target audience description (optional)
      responses:
        '200':
          description: Niche activated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  nicheKey:
                    type: string
                  agentId:
                    type: string
                  countries:
                    type: array
                    items:
                      type: string
        '400':
          description: Invalid tones, languages, or countries
        '403':
          description: Agent quota exceeded or plan restriction

  /niches/{id}/deactivate:
    post:
      tags: [Niches]
      summary: Deactivate niche
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Niche deactivated

  /niches/{id}/dashboard:
    get:
      tags: [Niches]
      summary: Get niche dashboard
      description: Get dashboard data for a niche (stats, agents, KB)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Niche dashboard data

  /niches/{id}/settings:
    get:
      tags: [Niches]
      summary: Get niche settings
      description: Get niche-specific settings
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Niche settings

    patch:
      tags: [Niches]
      summary: Update niche settings
      description: Update niche settings (tones, languages, etc.)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Settings updated

  /niches/{id}/article-schedule:
    get:
      tags: [Niches]
      summary: Get article schedule
      description: Get article scheduling configuration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Schedule configuration

    patch:
      tags: [Niches]
      summary: Update article schedule
      description: Update scheduling configuration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Schedule updated

  /niches/{id}/websearch-premium:
    post:
      tags: [Niches]
      summary: Toggle WebSearch Premium for niche
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: WebSearch Premium toggled

  /niches/request:
    post:
      tags: [Niches]
      summary: Request new niche
      description: Request a new niche category
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
      responses:
        '200':
          description: Request submitted

  # ==================== KB COVERAGE ====================
  /kb/{nicheKey}/stats:
    get:
      tags: [KB Coverage]
      summary: Get KB statistics
      description: Get Knowledge Base statistics for a niche or agent
      parameters:
        - name: nicheKey
          in: path
          required: true
          description: Niche key (e.g., "ia-sante") or agent key (e.g., "ia-sante-FR")
          schema:
            type: string
      responses:
        '200':
          description: KB statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  nicheKey:
                    type: string
                  total:
                    type: integer
                    description: Total KB insights
                  webSourced:
                    type: integer
                  aiGenerated:
                    type: integer
                  avgAgeInDays:
                    type: number

  /kb/{nicheKey}/entities:
    get:
      tags: [KB Coverage]
      summary: Get KB entities
      description: Get extracted entities from KB
      parameters:
        - name: nicheKey
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Extracted entities

  /kb/{nicheKey}/graph:
    get:
      tags: [KB Coverage]
      summary: Get entity graph
      description: Get entity relationship graph data
      parameters:
        - name: nicheKey
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Entity graph data

  /governance/{nicheKey}:
    get:
      tags: [KB Coverage]
      summary: Get governance evaluation
      description: Get governance evaluation for niche strategy
      parameters:
        - name: nicheKey
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Governance evaluation

  /seo/topic-coverage:
    get:
      tags: [KB Coverage]
      summary: Get topic coverage analysis
      description: |
        Get SEO topic coverage analysis showing covered/uncovered topics.
        Auto-refreshes stale data based on plan freshness settings.
      parameters:
        - name: agentKey
          in: query
          schema:
            type: string
        - name: nicheKey
          in: query
          schema:
            type: string
        - name: filter
          in: query
          schema:
            type: string
            enum: [all, covered, uncovered]
      responses:
        '200':
          description: Topic coverage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [idle, computing, ready, failed]
                  topics:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        covered:
                          type: boolean
                        heatScore:
                          type: number
                  totalTopics:
                    type: integer
                  coveredCount:
                    type: integer
                  uncoveredCount:
                    type: integer
                  coveragePercent:
                    type: number

    post:
      tags: [KB Coverage]
      summary: Refresh topic coverage
      description: Launch background job to recompute topic coverage
      responses:
        '200':
          description: Computation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [computing]
                  startedAt:
                    type: string
                    format: date-time

  # ==================== INTEGRATIONS ====================
  /integrations:
    get:
      tags: [Integrations]
      summary: List integrations
      description: Get configured integrations (WordPress, Shopify, etc.)
      responses:
        '200':
          description: List of integrations
          content:
            application/json:
              schema:
                type: object
                properties:
                  integrations:
                    type: array
                    items:
                      $ref: '#/components/schemas/Integration'

    post:
      tags: [Integrations]
      summary: Create integration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider, name]
              properties:
                provider:
                  type: string
                  enum: [wordpress, ghost, webflow, hubspot, notion, hashnode, devto, strapi, contentful, linkedin, webhook, zapier, kb-incidents, shopify]
                  description: Integration provider identifier
                name:
                  type: string
                  maxLength: 100
                  description: Display name for this integration
                enabled:
                  type: boolean
                  default: true
                credentials:
                  type: object
                  description: Provider-specific credentials (see /integrations/providers/{provider}/options)
                  default: {}
                settings:
                  type: object
                  description: Provider-specific settings
                  default: {}
      responses:
        '200':
          description: Integration created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  integration:
                    $ref: '#/components/schemas/Integration'
        '400':
          description: Validation error or connection test failed

  /integrations/providers:
    get:
      tags: [Integrations]
      summary: List available providers
      description: Get list of supported integration providers with their configuration requirements
      responses:
        '200':
          description: Available providers
          content:
            application/json:
              schema:
                type: object
                properties:
                  providers:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        icon:
                          type: string
                        features:
                          type: array
                          items:
                            type: string
                        credentialFields:
                          type: array
                          items:
                            type: object

  /integrations/{id}:
    get:
      tags: [Integrations]
      summary: Get integration details
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Integration details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Integration'

    patch:
      tags: [Integrations]
      summary: Update integration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                enabled:
                  type: boolean
                credentials:
                  type: object
                settings:
                  type: object
      responses:
        '200':
          description: Integration updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Integration'

    delete:
      tags: [Integrations]
      summary: Delete integration
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Integration deleted

  /integrations/{id}/test:
    post:
      tags: [Integrations]
      summary: Test integration connection
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Connection test result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string

  /integrations/{id}/publish:
    post:
      tags: [Integrations]
      summary: Publish article via integration
      description: |
        Publishes an article to the configured integration (WordPress, Ghost, etc.).

        You can either:
        - Provide `articleId` to publish an existing article from CoreProse
        - Provide `title` and `content` directly for custom content

        Provider-specific options (category, status, etc.) can be passed in the request body.
        Use GET /integrations/providers/{provider}/options to discover available options.
      parameters:
        - name: id
          in: path
          required: true
          description: Integration ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                articleId:
                  type: string
                  description: CoreProse article ID to publish (optional if title/content provided)
                title:
                  type: string
                  description: Article title (required if articleId not provided)
                content:
                  type: string
                  description: Article content in HTML/Markdown (required if articleId not provided)
                excerpt:
                  type: string
                  description: Short excerpt/description
                slug:
                  type: string
                  description: URL slug (auto-generated if not provided)
                tags:
                  type: array
                  items:
                    type: string
                  description: Article tags
                categories:
                  type: array
                  items:
                    type: string
                  description: Categories (for WordPress, etc.)
                category:
                  type: string
                  description: Single category (for KB Incidents, etc.)
                status:
                  type: string
                  enum: [draft, published, scheduled]
                  description: Publication status
                featuredImage:
                  type: string
                  description: Featured image URL for the article hero banner. If not provided, an auto-generated OG image with the article title and CoreProse branding is used as fallback.
                publishedAt:
                  type: string
                  format: date-time
                  description: Scheduled publication date (for scheduled status)
      responses:
        '200':
          description: Publication result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  provider:
                    type: string
                  externalId:
                    type: string
                    description: ID of the published content on the external platform
                  externalUrl:
                    type: string
                    description: URL of the published content
                  error:
                    type: string
                    description: Error message if success is false
        '400':
          description: Invalid request or missing required fields
        '404':
          description: Integration or article not found

  /integrations/providers/{provider}/options:
    get:
      tags: [Integrations]
      summary: Get provider-specific options
      description: |
        Returns provider-specific options for publishing:
        - Categories (for KB Incidents, WordPress, etc.)
        - Status options
        - Validation constraints (max tags, max length, etc.)

        This endpoint is useful for:
        - API consumers to know valid values before publishing
        - Frontend forms to populate dropdowns
        - Documentation generation
      parameters:
        - name: provider
          in: path
          required: true
          description: Provider identifier
          schema:
            type: string
            enum: [wordpress, ghost, webflow, hubspot, notion, hashnode, devto, strapi, contentful, linkedin, webhook, zapier, kb-incidents, shopify]
      responses:
        '200':
          description: Provider options and constraints
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProviderOptions'
        '404':
          description: Provider not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  statusCode:
                    type: integer
                    example: 404
                  message:
                    type: string
                    example: "Provider \"unknown\" not found. Available: wordpress, ghost, ..."

  # ==================== DOCUMENTS ====================
  /documents:
    get:
      tags: [Documents]
      summary: List private documents
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, completed, failed]
        - name: nicheKey
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
        - name: agentIds
          in: query
          description: Comma-separated agent IDs
          schema:
            type: string
      responses:
        '200':
          description: List of documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  documents:
                    type: array
                    items:
                      $ref: '#/components/schemas/Document'
                  total:
                    type: integer
                  page:
                    type: integer
                  totalPages:
                    type: integer

  /documents/available:
    get:
      tags: [Documents]
      summary: Get available documents
      description: Get available documents for article generation
      responses:
        '200':
          description: Available documents

  /documents/upload:
    post:
      tags: [Documents]
      summary: Upload private document
      description: |
        Upload a document to be indexed and used for RAG in article generation.
        Supported formats: PDF, DOCX, TXT, MD. Max size: 10MB.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                title:
                  type: string
                agentIds:
                  type: string
                  description: JSON array of agent IDs
                tags:
                  type: string
                  description: Comma-separated tags
                category:
                  type: string
      responses:
        '200':
          description: Document uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  originalName:
                    type: string
                  processingStatus:
                    type: string
                    enum: [pending]
        '403':
          description: Document quota exceeded

  /documents/url:
    post:
      tags: [Documents]
      summary: Add document from URL
      description: Add document from URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, nicheKey]
              properties:
                url:
                  type: string
                  format: uri
                nicheKey:
                  type: string
      responses:
        '200':
          description: Document created from URL

  /documents/{id}:
    get:
      tags: [Documents]
      summary: Get document details
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Document details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'

    patch:
      tags: [Documents]
      summary: Update document metadata
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                category:
                  type: string
                tags:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Document updated

    delete:
      tags: [Documents]
      summary: Delete document
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Document deleted

  /documents/search:
    post:
      tags: [Documents]
      summary: Semantic search in KB
      description: Semantic search in Knowledge Base documents
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                nicheKey:
                  type: string
      responses:
        '200':
          description: Search results

  # ==================== TENANT ====================
  /tenant:
    get:
      tags: [Tenant]
      summary: Get tenant details
      description: Get tenant (organization) details
      responses:
        '200':
          description: Tenant details

  /tenant/quotas:
    get:
      tags: [Tenant]
      summary: Get account quotas
      description: Get current usage and limits for your account
      responses:
        '200':
          description: Quota information
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: string
                    enum: [free, starter, pro, business]
                  quotas:
                    type: object
                    properties:
                      articlesPerMonth:
                        type: integer
                      maxAgents:
                        type: integer
                      privateDocsCount:
                        type: integer
                      apiKeys:
                        type: integer
                  usage:
                    type: object
                    properties:
                      articlesThisMonth:
                        type: integer
                      activeAgents:
                        type: integer
                      documentsCount:
                        type: integer
                  remaining:
                    type: object
                    properties:
                      articles:
                        type: integer
                      agents:
                        type: integer
                      documents:
                        type: integer

  /tenant/settings:
    patch:
      tags: [Tenant]
      summary: Update tenant settings
      description: Update tenant settings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Settings updated

  # ==================== TEAM ====================
  /team:
    get:
      tags: [Team]
      summary: List team members
      description: List team members for the current tenant
      responses:
        '200':
          description: Team members list
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      $ref: '#/components/schemas/TeamMember'

  /team/invite:
    post:
      tags: [Team]
      summary: Invite team member
      description: Send an invitation to join the team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email:
                  type: string
                  format: email
                  description: Email address to invite
                role:
                  type: string
                  enum: [editor, admin, viewer]
                  description: Role for the new team member
      responses:
        '200':
          description: Invitation sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
        '400':
          description: Invalid email or role
        '409':
          description: User already a team member

  /team/{id}:
    patch:
      tags: [Team]
      summary: Update team member role
      description: Update the role of a team member
      parameters:
        - name: id
          in: path
          required: true
          description: Team member ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  type: string
                  enum: [editor, admin, viewer]
      responses:
        '200':
          description: Role updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '403':
          description: Cannot modify own role or insufficient permissions
        '404':
          description: Team member not found

    delete:
      tags: [Team]
      summary: Remove team member
      description: Remove a team member
      parameters:
        - name: id
          in: path
          required: true
          description: Team member ID
          schema:
            type: string
      responses:
        '200':
          description: Team member removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '403':
          description: Cannot remove self or insufficient permissions
        '404':
          description: Team member not found

  # ==================== ACCOUNT ====================
  /account:
    get:
      tags: [Account]
      summary: Get account details
      description: Get account details for the current user
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  email:
                    type: string
                  name:
                    type: string
                  avatar:
                    type: string
                  plan:
                    type: string
                  createdAt:
                    type: string
                    format: date-time

  /account/profile:
    patch:
      tags: [Account]
      summary: Update profile
      description: Update profile (name, avatar)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                avatar:
                  type: string
                  format: uri
      responses:
        '200':
          description: Profile updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '400':
          description: Invalid input


  # ==================== REVIEWS ====================
  /reviews:
    get:
      tags: [Reviews]
      summary: List reviews
      description: List reviews (admin sees all, user sees own)
      responses:
        '200':
          description: Reviews list
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviews:
                    type: array
                    items:
                      $ref: '#/components/schemas/Review'

    post:
      tags: [Reviews]
      summary: Submit a review
      description: Submit a review for the platform
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating, title, content, consent]
              properties:
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                  description: Rating from 1 to 5
                title:
                  type: string
                  minLength: 3
                  maxLength: 100
                  description: Review title
                content:
                  type: string
                  minLength: 10
                  maxLength: 2000
                  description: Review content
                consent:
                  type: boolean
                  description: Consent to display review publicly (must be true)
      responses:
        '200':
          description: Review submitted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Review'
        '400':
          description: Invalid input or consent not given
        '409':
          description: User already submitted a review

  /reviews/my-review:
    get:
      tags: [Reviews]
      summary: Get my review
      description: Get current user's review if it exists
      responses:
        '200':
          description: User review
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Review'
        '404':
          description: No review found

  /reviews/{id}:
    patch:
      tags: [Reviews]
      summary: Update review
      description: Update an existing review
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                title:
                  type: string
                  maxLength: 100
                content:
                  type: string
                  maxLength: 2000
      responses:
        '200':
          description: Review updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Review'
        '403':
          description: Cannot edit another user's review
        '404':
          description: Review not found

    delete:
      tags: [Reviews]
      summary: Delete review
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Review deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
        '403':
          description: Cannot delete another user's review
        '404':
          description: Review not found

  /reviews/{id}/helpful:
    post:
      tags: [Reviews]
      summary: Mark review as helpful
      description: Mark a review as helpful (one vote per user per review)
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Review marked as helpful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  helpfulCount:
                    type: integer


  # ==================== TRANSLATION ====================
  /writer/translate-credits:
    get:
      tags: [Translation]
      summary: Get translation credit usage
      description: Returns credit consumption and remaining quota for the current billing period. Optionally previews cost for a specific word count.
      security:
        - BearerAuth: []
      parameters:
        - name: wordCount
          in: query
          required: false
          schema:
            type: integer
          description: Preview credits needed for this word count
      responses:
        '200':
          description: Credit usage and optional preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  used:
                    type: integer
                    description: Credits consumed this billing period
                  limit:
                    type: integer
                    description: Total credits per month for the plan
                  remaining:
                    type: integer
                    description: Credits remaining
                  preview:
                    type: object
                    nullable: true
                    properties:
                      wordCount:
                        type: integer
                      creditsNeeded:
                        type: integer
                      tier:
                        type: string
                        enum: [short, standard, long, document, document-long, document-xl]
                      allowed:
                        type: boolean
              example:
                used: 12
                limit: 150
                remaining: 138
                preview:
                  wordCount: 1200
                  creditsNeeded: 2
                  tier: standard
                  allowed: true

  /writer/{id}/translate:
    post:
      tags: [Translation]
      summary: Translate a published article
      description: |
        Creates a new article in the target language, linked via Article.variants.
        Preserves citations [N], markdown formatting, and sources.

        Credit tiers (by word count):
        | Tier | Words | Credits |
        |---|---|---|
        | short | 0-500 | 1 |
        | standard | 501-1500 | 2 |
        | long | 1501-3000 | 3 |
        | document | 3001-10000 | 5 |
        | document-long | 10001-25000 | 10 |
        | document-xl | 25001+ | 15 |

        Post-translation: GEO optimization, entity extraction, Wikipedia linking in target language. hreflang tags updated.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Source article ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [targetLanguage]
              properties:
                targetLanguage:
                  type: string
                  enum: [en, fr, es, de, it, pt, nl, pl, tr, ar, zh, ja, ko]
                  description: ISO 639-1 target language code
                targetAgentKey:
                  type: string
                  nullable: true
                  description: Target agent (same niche, target country) for KB assignment
                  example: sante-US
                tone:
                  type: string
                  enum: [literal, natural, creative]
                  default: natural
            example:
              targetLanguage: de
              targetAgentKey: sante-DE
              tone: natural
      responses:
        '200':
          description: Translation successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  articleId:
                    type: string
                  slug:
                    type: string
                  sourceLanguage:
                    type: string
                  targetLanguage:
                    type: string
                  wordCount:
                    type: integer
              example:
                success: true
                articleId: "680abc123def456"
                slug: "article-title-de"
                sourceLanguage: fr
                targetLanguage: de
                wordCount: 1200
        '403':
          description: Plan does not support translation
        '404':
          description: Article not found
        '429':
          description: Not enough translation credits

  # ==================== PUBLIC ENDPOINTS ====================
  /public/trend-radar:
    get:
      tags: [Public]
      summary: Get trending topics
      security: []
      description: |
        Trending topics with engagement scoring, freshness decay, and diversification.
        Cache: 5 min TTL per locale+niche+country+offset.
      parameters:
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
            default: fr
        - name: nicheKey
          in: query
          description: Filter by niche
          schema:
            type: string
        - name: country
          in: query
          description: Filter by country code
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Trending topics
          content:
            application/json:
              schema:
                type: object
                properties:
                  locale:
                    type: string
                  hotTopics:
                    type: array
                    items:
                      $ref: '#/components/schemas/TrendTopic'
                  topNiches:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        name:
                          type: string
                        nameEn:
                          type: string
                        icon:
                          type: string
                        score:
                          type: number
                        trendCount:
                          type: integer
                  stats:
                    type: object
                    properties:
                      totalTrends:
                        type: integer
                      totalArticles:
                        type: integer
                      totalNiches:
                        type: integer
                      freeArticlesGenerated:
                        type: integer
                      lastUpdated:
                        type: string
                        format: date-time
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer
                      hasMore:
                        type: boolean

  /public/trends/{slug}:
    get:
      tags: [Public]
      summary: Get single trend detail
      security: []
      description: |
        Single trending topic detail with sources and related trends.
        Cache: 5 min TTL per slug+locale.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
            default: fr
      responses:
        '200':
          description: Trend topic detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  locale:
                    type: string
                  topic:
                    type: object
                    properties:
                      topic:
                        type: string
                      slug:
                        type: string
                      nicheKey:
                        type: string
                      nicheName:
                        type: string
                      country:
                        type: string
                      score:
                        type: number
                      type:
                        type: string
                        enum: [emerging, spiking, stable, declining, cross-niche]
                      detectedAt:
                        type: string
                        format: date-time
                      sources:
                        type: array
                        items:
                          type: object
                          properties:
                            title:
                              type: string
                            url:
                              type: string
                            domain:
                              type: string
                            snippet:
                              type: string
                      evidence:
                        type: object
                        properties:
                          mentionsLast7Days:
                            type: integer
                          mentionsLast30Days:
                            type: integer
                          firstSeen:
                            type: string
                            format: date-time
                          relatedEntities:
                            type: array
                            items:
                              type: string
                      article:
                        type: object
                        properties:
                          slug:
                            type: string
                          title:
                            type: string
                          matchScore:
                            type: number
                  relatedTrends:
                    type: array
                    items:
                      type: object
                      properties:
                        topic:
                          type: string
                        slug:
                          type: string
                        score:
                          type: number
                        type:
                          type: string
                        country:
                          type: string
        '404':
          description: Trend not found

  /public/trend-alerts:
    post:
      tags: [Public]
      summary: Subscribe to trend alerts
      security: []
      description: Subscribe to weekly trend alerts via email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  description: Email (non-disposable)
                nicheKey:
                  type: string
                  description: Niche key to filter alerts (optional)
                locale:
                  type: string
                  enum: [fr, en]
                  default: fr
      responses:
        '200':
          description: Subscribed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean

  /public/trend-radar-click:
    post:
      tags: [Public]
      summary: Record trend engagement
      security: []
      description: |
        Record engagement on a trend topic (used for scoring boost).
        Rate limit: 1 click/topic/IP/hour (silent ignore on duplicate).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [topic]
              properties:
                topic:
                  type: string
                  maxLength: 200
                  description: Trend topic text
                nicheKey:
                  type: string
                country:
                  type: string
                  default: FR
      responses:
        '200':
          description: Click recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /public/kb-articles:
    get:
      tags: [Public]
      summary: List public KB articles
      security: []
      description: List public KB articles with category filtering
      parameters:
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
            default: en
        - name: category
          in: query
          schema:
            type: string
            enum: [hallucinations, rag, drift, chunking, ghost-sources]
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
            maximum: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Public KB articles
          content:
            application/json:
              schema:
                type: object
                properties:
                  locale:
                    type: string
                  articles:
                    type: array
                    items:
                      $ref: '#/components/schemas/KBArticleSummary'
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer
                      hasMore:
                        type: boolean
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        count:
                          type: integer

  /public/kb-articles/{slug}:
    get:
      tags: [Public]
      summary: Get public KB article
      security: []
      description: Single public KB article by slug with sources and related articles
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
            default: en
      responses:
        '200':
          description: KB article detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  article:
                    $ref: '#/components/schemas/KBArticle'
                  relatedArticles:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        title:
                          type: string
                        slug:
                          type: string
                        excerpt:
                          type: string
                  locale:
                    type: string
        '404':
          description: Article not found

  /public/stats:
    get:
      tags: [Public]
      summary: Platform statistics
      security: []
      description: Platform statistics (shown when >= 10 users). Cache 5 min.
      responses:
        '200':
          description: Platform stats
          content:
            application/json:
              schema:
                type: object
                properties:
                  articles:
                    type: integer
                  users:
                    type: integer
                  niches:
                    type: integer
                  shouldShow:
                    type: boolean
                    description: Whether stats should be displayed (true when >= 10 users)

  /public/top-niches:
    get:
      tags: [Public]
      summary: Top niches by score
      security: []
      description: Top niches by governance score
      parameters:
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
            default: en
        - name: limit
          in: query
          schema:
            type: integer
            default: 6
            maximum: 20
      responses:
        '200':
          description: Top niches
          content:
            application/json:
              schema:
                type: object
                properties:
                  country:
                    type: string
                  locale:
                    type: string
                  niches:
                    type: array
                    items:
                      type: object
                      properties:
                        key:
                          type: string
                        name:
                          type: string
                        description:
                          type: string
                        icon:
                          type: string
                        score:
                          type: number
                        interest:
                          type: string

  /public/widget/trends:
    get:
      tags: [Public]
      summary: Trends widget
      security: []
      description: |
        Lightweight endpoint for embeddable widget. CORS enabled.
        Rate limit: 60/IP/min. Cache: 5 min.
      parameters:
        - name: nicheKey
          in: query
          description: Filter by niche
          schema:
            type: string
        - name: country
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 3
            maximum: 10
            default: 5
        - name: locale
          in: query
          schema:
            type: string
            enum: [fr, en]
      responses:
        '200':
          description: Widget trends data
          content:
            application/json:
              schema:
                type: object
                properties:
                  trends:
                    type: array
                    items:
                      type: object
                      properties:
                        topic:
                          type: string
                        score:
                          type: number
                        type:
                          type: string
                          enum: [emerging, spiking, stable, declining, cross-niche]
                        nicheName:
                          type: string
                        country:
                          type: string
                        url:
                          type: string
                          format: uri
                  poweredBy:
                    type: object
                    properties:
                      name:
                        type: string
                      url:
                        type: string
                      logo:
                        type: string
                  generatedAt:
                    type: string
                    format: date-time
        '429':
          description: Rate limit exceeded (60/IP/min)


components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key for authentication (format: cp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)"

  schemas:
    Article:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        content:
          type: string
        excerpt:
          type: string
        status:
          type: string
          enum: [draft, generating, published, scheduled, failed]
        nicheKey:
          type: string
        agentKey:
          type: string
        tone:
          type: string
        language:
          type: string
        wordCount:
          type: integer
        readingTime:
          type: integer
        seo:
          type: object
          properties:
            metaTitle:
              type: string
            metaDescription:
              type: string
        sources:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              url:
                type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        publishedAt:
          type: string
          format: date-time

    ArticlePlan:
      type: object
      properties:
        title:
          type: string
        outline:
          type: array
          items:
            type: object
            properties:
              heading:
                type: string
              subheadings:
                type: array
                items:
                  type: string
        estimatedWordCount:
          type: integer
        keyPoints:
          type: array
          items:
            type: string

    Agent:
      type: object
      properties:
        id:
          type: string
        nicheKey:
          type: string
        country:
          type: string
        name:
          type: string
        description:
          type: string
        enabled:
          type: boolean
        schedule:
          type: object
          properties:
            frequency:
              type: string
              enum: [manual, daily, weekly, monthly]
            timezone:
              type: string
        lastExecutedAt:
          type: string
          format: date-time
        articlesGenerated:
          type: integer

    Niche:
      type: object
      properties:
        id:
          type: string
        key:
          type: string
        name:
          type: string
        icon:
          type: string
        description:
          type: string
        isActivated:
          type: boolean
        countries:
          type: array
          items:
            type: string
        kbStats:
          type: object
          properties:
            totalInsights:
              type: integer
            lastUpdated:
              type: string
              format: date-time

    Integration:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
        name:
          type: string
        enabled:
          type: boolean
        credentials:
          type: array
          items:
            type: string
          description: List of credential keys (values hidden)
        providerInfo:
          type: object
          properties:
            name:
              type: string
            icon:
              type: string
            features:
              type: array
              items:
                type: string
        createdAt:
          type: string
          format: date-time

    Document:
      type: object
      properties:
        id:
          type: string
        originalName:
          type: string
        title:
          type: string
        category:
          type: string
        processingStatus:
          type: string
          enum: [pending, processing, completed, failed]
        chunksCount:
          type: integer
        agentIds:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time

    TrendTopic:
      type: object
      properties:
        topic:
          type: string
        slug:
          type: string
        nicheKey:
          type: string
        nicheName:
          type: string
        nicheNameEn:
          type: string
        nicheIcon:
          type: string
        country:
          type: string
        agentKey:
          type: string
        score:
          type: number
          description: Engagement score
        type:
          type: string
          enum: [emerging, spiking, stable, declining, cross-niche]
        detectedAt:
          type: string
          format: date-time
        snippet:
          type: string
        mentionsLast7Days:
          type: integer
        sourceCount:
          type: integer
        growthPercent:
          type: number
        articleSlug:
          type: string
          description: Slug of linked article (if exists)
        articleTitle:
          type: string
        matchScore:
          type: number

    KBArticleSummary:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        slug:
          type: string
        excerpt:
          type: string
        category:
          type: string
        tags:
          type: array
          items:
            type: string
        wordCount:
          type: integer
        readingTime:
          type: integer
        publishedAt:
          type: string
          format: date-time

    KBArticle:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        slug:
          type: string
        content:
          type: string
          description: Markdown content
        htmlContent:
          type: string
          description: HTML rendered content
        excerpt:
          type: string
        category:
          type: string
        tags:
          type: array
          items:
            type: string
        metaDescription:
          type: string
        wordCount:
          type: integer
        readingTime:
          type: integer
        publishedAt:
          type: string
          format: date-time
        sources:
          type: array
          items:
            type: object
            properties:
              title:
                type: string
              url:
                type: string
              summary:
                type: string
              type:
                type: string
                enum: [kb, private]
        transparency:
          type: object
          properties:
            generationDuration:
              type: integer
              description: Generation duration in ms
            kbQueriesCount:
              type: integer
            confidenceScore:
              type: number
            sourcesCount:
              type: integer
        seo:
          type: object
          properties:
            metaTitle:
              type: string
            metaDescription:
              type: string
            canonicalUrl:
              type: string
        language:
          type: string

    Review:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        userName:
          type: string
        rating:
          type: integer
          minimum: 1
          maximum: 5
        title:
          type: string
        content:
          type: string
        consent:
          type: boolean
        status:
          type: string
          enum: [pending, approved, rejected, featured]
        helpfulCount:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TeamMember:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [owner, admin, editor, viewer]
        status:
          type: string
          enum: [active, invited, disabled]
        joinedAt:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        statusCode:
          type: integer
        message:
          type: string
        data:
          type: object
          description: Optional additional error data

    ProviderOptions:
      type: object
      properties:
        provider:
          type: string
          description: Provider identifier
          example: "kb-incidents"
        name:
          type: string
          description: Human-readable provider name
          example: "KB Incidents"
        description:
          type: string
          description: Provider description
        docsUrl:
          type: string
          format: uri
          description: Documentation URL
        features:
          type: array
          items:
            type: string
            enum: [publish, draft, schedule, update, delete, categories, tags, featuredImage, customFields, series, canonicalUrl, license]
          description: Supported features
        publishOptions:
          type: array
          items:
            $ref: '#/components/schemas/PublishOptionField'
          description: Options available at publish time (categories, status, etc.)
        constraints:
          $ref: '#/components/schemas/IntegrationConstraints'
        optionalSettings:
          type: array
          items:
            $ref: '#/components/schemas/SettingField'
          description: Optional settings configurable on the integration

    PublishOptionField:
      type: object
      properties:
        key:
          type: string
          description: Field key used in publish payload
          example: "category"
        label:
          type: string
          description: Human-readable label
          example: "Category"
        type:
          type: string
          enum: [select, multi-select, text]
          description: Field type
        required:
          type: boolean
          description: Whether this field is required
        options:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              value:
                type: string
          description: Available options for select/multi-select types
        helpText:
          type: string
          description: Help text for the field
      example:
        key: "category"
        label: "Category"
        type: "select"
        required: true
        options:
          - label: "Hallucinations"
            value: "hallucinations"
          - label: "Drift"
            value: "drift"
          - label: "RAG"
            value: "rag"
        helpText: "The incident category for classification"

    IntegrationConstraints:
      type: object
      properties:
        maxTags:
          type: integer
          description: Maximum number of tags allowed
          example: 5
        maxDescriptionLength:
          type: integer
          description: Maximum description/excerpt length in characters
          example: 500
        maxExcerptLength:
          type: integer
          description: Maximum excerpt length in characters

    SettingField:
      type: object
      properties:
        key:
          type: string
        label:
          type: string
        type:
          type: string
          enum: [text, select, boolean, number]
        options:
          type: array
          items:
            type: object
            properties:
              label:
                type: string
              value:
                type: string
        defaultValue:
          oneOf:
            - type: string
            - type: boolean
            - type: number
        helpText:
          type: string
