openapi: 3.1.0
info:
  title: RankRush API
  version: 0.1.0
  description: 'Programmatic access to RankRush AI-visibility, audit, keyword, blog, and Buzz capabilities.
    All ops are POST to a Supabase edge function under /functions/v1. Authenticate with an `rr_live_*`
    API key as a Bearer token (Settings → API Keys), plus the project anon key as the `apikey` header
    (the client transport sets this). Enforcement (tiers, limits) is 100% server-side; clients are thin
    wrappers.


    > **Integration status:** the MCP server (`@rankrush.ai/mcp`) and the CLI (`@rankrush.ai/cli`) are
    the live integration paths today. Direct REST at the base URL above is rolling out - this document
    is the machine-readable contract those clients are generated from.'
  license:
    name: MIT
    identifier: MIT
servers:
- url: https://api.rankrush.ai/v1
  description: RankRush REST API
security:
- RankRushApiKey: []
  SupabaseAnonKey: []
tags:
- name: visibility
  description: AI visibility + keywords
- name: audit
  description: Web audit + scan queue
- name: blog
  description: Blog content engine (Publish module)
- name: buzz
  description: Reddit/community engagement (Buzz module)
- name: profiles
  description: Business profiles
- name: admin
  description: Admin-role-gated operations
- name: jwt-only
  description: Not yet API-key reachable (jwt-only in the backend)
paths:
  /check-ai-visibility:
    post:
      operationId: checkAiVisibility
      summary: Run an AI-visibility analysis for a set of keywords
      tags:
      - visibility
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckAiVisibilityRequest'
      responses:
        '200':
          description: Analysis result (fresh or from cache)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckAiVisibilityResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /generate-visibility-keywords:
    post:
      operationId: generateVisibilityKeywords
      summary: Generate/collect visibility keywords for a profile (growth + GSC)
      tags:
      - visibility
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileIdCamelRequest'
      responses:
        '200':
          description: Keyword set with source breakdown
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateVisibilityKeywordsResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Per-profile or per-tier keyword cap reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CapError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /audit-enqueue:
    post:
      operationId: auditEnqueue
      summary: Enqueue a web audit run (bulk or single-node)
      tags:
      - audit
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuditEnqueueRequest'
      responses:
        '200':
          description: Audit run queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEnqueueResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /enqueue-scan:
    post:
      operationId: enqueueScan
      summary: Enqueue a scan (ai_visibility | web_audit | node_check)
      tags:
      - audit
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnqueueScanRequest'
      responses:
        '200':
          description: Scan queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnqueueScanResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /generate-blog-topics:
    post:
      operationId: generateBlogTopics
      summary: Enqueue blog-topic generation for a profile
      tags:
      - blog
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateBlogTopicsRequest'
      responses:
        '200':
          description: Topic-generation job queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobQueuedResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /generate-blog-post:
    post:
      operationId: generateBlogPost
      summary: Atomically claim a topic for blog-post generation
      description: 'Returns HTTP 200 with `{ok:false, code}` for expected business failures (topic_wrong_status,
        connection_missing, tier_too_low, monthly_cap_reached, topic_already_claimed).

        '
      tags:
      - blog
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateBlogPostRequest'
      responses:
        '200':
          description: Claim result (ok:true) or expected business failure (ok:false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateBlogPostResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /generate-content-strategy:
    post:
      operationId: generateContentStrategy
      summary: Generate a content strategy for a profile
      tags:
      - blog
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileIdCamelRequest'
      responses:
        '200':
          description: Strategy text
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateContentStrategyResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /save-cms-connection:
    post:
      operationId: saveCmsConnection
      summary: Save + verify a WordPress CMS connection
      description: 'Returns HTTP 200 with `{ok:false, code:"connection_failed"}` when the WordPress credential
        probe fails (nothing persisted).

        '
      tags:
      - blog
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SaveCmsConnectionRequest'
      responses:
        '200':
          description: Verified + saved (ok:true) or connection failure (ok:false)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SaveCmsConnectionResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /buzz-brand-research:
    post:
      operationId: buzzBrandResearch
      summary: Run Buzz brand research (async by default, sync with wait:true)
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzWaitRequest'
      responses:
        '200':
          description: Started (async) or full research result (wait:true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzBrandResearchResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /buzz-generate-drafts:
    post:
      operationId: buzzGenerateDrafts
      summary: Generate Buzz reply/post drafts (bulk or single engagement)
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzGenerateDraftsRequest'
      responses:
        '200':
          description: Draft(s) created (bulk counts or single draft)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzGenerateDraftsResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /buzz-opportunity-search:
    post:
      operationId: buzzOpportunitySearch
      summary: Search Buzz engagement opportunities (async default, sync wait:true)
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzWaitRequest'
      responses:
        '200':
          description: Started (async) or opportunity counts (wait:true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzOpportunitySearchResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /buzz-visibility-score:
    post:
      operationId: buzzVisibilityScore
      summary: Compute the Buzz visibility score for a profile
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileIdSnakeRequest'
      responses:
        '200':
          description: Buzz visibility score + engagement metrics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzVisibilityScoreResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /profiles-list:
    post:
      operationId: profilesList
      summary: List the account's business profiles (or fetch one by id)
      tags:
      - profiles
      x-rr-auth-level: api-key
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfilesListRequest'
      responses:
        '200':
          description: 'Array of profiles (empty if none). When `id` is supplied, the array holds just
            that profile, or is empty if it isn''t the caller''s.

            '
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Profile'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /keywords-list:
    post:
      operationId: keywordsList
      summary: List a profile's tracked AEO keywords
      tags:
      - visibility
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/KeywordsListRequest'
      responses:
        '200':
          description: The profile's tracked keywords (empty list if none)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeywordsListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /profile-update:
    post:
      operationId: profileUpdate
      summary: Update a profile's website URL and/or industry
      tags:
      - profiles
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileUpdateRequest'
      responses:
        '200':
          description: The updated profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Profile'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /buzz-engagements-list:
    post:
      operationId: buzzEngagementsList
      summary: List Buzz (Reddit) engagements for a profile or the whole account
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzEngagementsListRequest'
      responses:
        '200':
          description: Matching engagements (with optional per-subreddit totals)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzEngagementsListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /buzz-draft-approve:
    post:
      operationId: buzzDraftApprove
      summary: Approve (or schedule) a Buzz draft post
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzDraftApproveRequest'
      responses:
        '200':
          description: The updated engagement
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzEngagement'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 'Draft not approvable — body carries `reason: invalid_status` (only draft/approved/scheduled
            rows may be approved or re-scheduled; posted/dismissed/new rows are rejected) or `reason:
            empty_draft` (no draft_content to publish).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /buzz-research-list:
    post:
      operationId: buzzResearchList
      summary: List a profile's high-relevance Buzz research mentions
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuzzResearchListRequest'
      responses:
        '200':
          description: Brand mentions (reactive research rows; drafts and opportunity rows are excluded)
            at or above minRelevance, best-first. Unscored (null relevance) mentions are always included.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzResearchListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /buzz-profile-get:
    post:
      operationId: buzzProfileGet
      summary: Get a profile's Buzz research status
      tags:
      - buzz
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileIdCamelRequest'
      responses:
        '200':
          description: The profile's Buzz research status (nulls if not set up)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuzzProfileGetResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /audit-run-get:
    post:
      operationId: auditRunGet
      summary: Fetch an audit run (with optional per-node progress summary)
      tags:
      - audit
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuditRunGetRequest'
      responses:
        '200':
          description: The audit run, plus node summary when includeNodes is true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditRunGetResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /blog-topics-list:
    post:
      operationId: blogTopicsList
      summary: List a profile's planned blog topics
      tags:
      - blog
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BlogTopicsListRequest'
      responses:
        '200':
          description: The profile's blog topics (empty list if none)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlogTopicsListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /nodes-list:
    post:
      operationId: nodesList
      summary: List a profile's node-progress rows (optionally filtered by status)
      tags:
      - audit
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NodesListRequest'
      responses:
        '200':
          description: The profile's node-progress rows (empty list if none)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NodesListResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
  /node-complete:
    post:
      operationId: nodeComplete
      summary: Mark a node complete (manual lifecycle override)
      tags:
      - audit
      x-rr-auth-level: api-key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NodeCompleteRequest'
      responses:
        '200':
          description: The updated node, with its prior status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NodeCompleteResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    RankRushApiKey:
      type: http
      scheme: bearer
      description: An `rr_live_*` API key sent as a Bearer token.
    SupabaseAnonKey:
      type: apiKey
      in: header
      name: apikey
      description: The project anon/publishable key (set by the client transport).
  responses:
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Profile ownership or admin-role check failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Referenced resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ValidationError:
      description: Invalid request body
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorBody'
    RateLimited:
      description: Monthly tier limit or daily cap exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
  schemas:
    Error:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
        code:
          type: string
      required:
      - error
    ValidationErrorBody:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
          example: Invalid input
        details:
          type: object
          additionalProperties: true
      required:
      - error
    RateLimitError:
      type: object
      description: '`code` is `limit_exceeded` (monthly tier limit) or `daily_cap_exceeded` / `daily_llm_gen_cap_exceeded`
        (daily breakers).

        '
      properties:
        success:
          type: boolean
          const: false
        error:
          type: string
        code:
          type: string
        tier:
          type: string
        upgrade_to:
          type: string
        limit:
          type: integer
        used:
          type: integer
        cap:
          type: integer
      required:
      - error
      - code
    CapError:
      type: object
      description: Per-profile (profile_keyword_limit) or per-tier (keyword_limit_exceeded) cap.
      properties:
        success:
          type: boolean
          const: false
        code:
          type: string
        tier:
          type: string
        upgrade_to:
          type: string
        limit:
          type: integer
        used:
          type: integer
      required:
      - code
    ProfileIdCamelRequest:
      type: object
      properties:
        profileId:
          type: string
      required:
      - profileId
    ProfileIdSnakeRequest:
      type: object
      properties:
        profile_id:
          type: string
      required:
      - profile_id
    BuzzWaitRequest:
      type: object
      properties:
        profile_id:
          type: string
        wait:
          type: boolean
          default: false
          description: true runs inline and returns the full result; default runs in background.
      required:
      - profile_id
    OkResponse:
      type: object
      properties:
        success:
          type: boolean
          const: true
      required:
      - success
    Profile:
      type: object
      properties:
        id:
          type: string
        business_name:
          type:
          - string
          - 'null'
        website_url:
          type:
          - string
          - 'null'
        industry:
          type:
          - string
          - 'null'
          enum:
          - local_service
          - b2b_saas
          - ecommerce
          - null
        created_at:
          type: string
          format: date-time
    ProfilesListRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Fetch just this profile (must belong to the caller). Omit to list all.
    ProfileUpdateRequest:
      type: object
      description: At least one of website_url or industry must be provided.
      properties:
        profileId:
          type: string
          format: uuid
        website_url:
          type: string
          minLength: 1
        industry:
          type: string
          enum:
          - local_service
          - b2b_saas
          - ecommerce
      required:
      - profileId
    KeywordsListRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 200
      required:
      - profileId
    KeywordsListResponse:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        keywords:
          type: array
          items:
            type: string
        count:
          type: integer
      required:
      - profile_id
      - keywords
      - count
    CheckAiVisibilityRequest:
      type: object
      properties:
        keywords:
          type: array
          items:
            type: string
            maxLength: 500
          minItems: 1
          maxItems: 200
        businessName:
          type: string
          minLength: 1
          maxLength: 200
        businessUrl:
          type: string
          format: uri
        profileId:
          type: string
          format: uuid
        force_refresh:
          type: boolean
          default: false
        llm:
          type: string
          enum:
          - chatgpt
          - claude
          - perplexity
          - gemini
          description: Single-LLM fan-out mode (skips rate-limit/cache/write) when present.
        run_id:
          type: string
          format: uuid
      required:
      - keywords
      - businessName
    CheckAiVisibilityResponse:
      type: object
      properties:
        success:
          type: boolean
        fromCache:
          type: boolean
        runId:
          type: string
        summary:
          type: string
        score:
          type: number
          minimum: 0
          maximum: 100
        analyzedAt:
          type: string
          format: date-time
        written:
          type: integer
        attempted:
          type: integer
        dropped:
          type: array
          items:
            type: object
            properties:
              keyword:
                type: string
              llm:
                type: string
              reason:
                type: string
      required:
      - success
      - runId
    GenerateVisibilityKeywordsResponse:
      type: object
      properties:
        success:
          type: boolean
        keywords:
          type: array
          items:
            type: object
            properties:
              keyword:
                type: string
              source:
                type: string
                enum:
                - growth
                - gsc
        totalCount:
          type: integer
        sources:
          type: object
          properties:
            growth:
              type: integer
            gsc:
              type: integer
        gscConnected:
          type: boolean
        gscLastSynced:
          type:
          - string
          - 'null'
      required:
      - success
      - keywords
    AuditEnqueueRequest:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        mode:
          type: string
          enum:
          - bulk
          - single
          default: bulk
        node_filter:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 80
          maxItems: 120
          description: Required when mode=single.
      required:
      - profile_id
    AuditEnqueueResponse:
      type: object
      properties:
        auditRunId:
          type: string
        status:
          type: string
          const: queued
        enqueuedAt:
          type: string
          format: date-time
        mode:
          type: string
          enum:
          - bulk
          - single
      required:
      - auditRunId
      - status
    AuditRunGetRequest:
      type: object
      properties:
        runId:
          type: string
          format: uuid
        includeNodes:
          type: boolean
          default: false
          description: When true, also returns the per-node progress summary.
      required:
      - runId
    AuditRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        profile_id:
          type: string
          format: uuid
        status:
          type: string
        mode:
          type: string
        website_url:
          type: string
        industry:
          type: string
        overall_score:
          type: integer
        technical_score:
          type: integer
        content_score:
          type: integer
        authority_score:
          type: integer
        audit_results:
          description: v1-compatible check array ([{passed, ...}]) or {} when unpopulated (jsonb).
        error_message:
          type:
          - string
          - 'null'
        enqueued_at:
          type: string
          format: date-time
        started_at:
          type:
          - string
          - 'null'
          format: date-time
        completed_at:
          type:
          - string
          - 'null'
          format: date-time
        audited_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
      required:
      - id
      - status
    NodeProgressSummary:
      type: object
      properties:
        node_id:
          type: string
        status:
          type: string
        diagnosis:
          type:
          - string
          - 'null'
      required:
      - node_id
      - status
    AuditRunGetResponse:
      type: object
      properties:
        run:
          $ref: '#/components/schemas/AuditRun'
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/NodeProgressSummary'
        node_counts:
          type: object
          additionalProperties:
            type: integer
      required:
      - run
    EnqueueScanRequest:
      type: object
      properties:
        scan_type:
          type: string
          enum:
          - ai_visibility
          - web_audit
          - node_check
        platform:
          type: string
          enum:
          - chatgpt
          - claude
          - perplexity
          - gemini
        profile_id:
          type: string
          format: uuid
        website_url:
          type: string
          maxLength: 500
        payload:
          type: object
          additionalProperties: true
          default: {}
        priority:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
      required:
      - scan_type
      - profile_id
    EnqueueScanResponse:
      type: object
      properties:
        status:
          type: string
          const: queued
        queue_id:
          type: string
        position:
          type: integer
        estimatedWait:
          type: string
      required:
      - status
      - queue_id
    NodesListRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - locked
          - available
          - in_progress
          - complete
          - pass
          - fail
          - manual
          - skipped
          description: Optional filter on node_progress.status.
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 500
      required:
      - profileId
    NodeProgress:
      type: object
      properties:
        node_id:
          type: string
        status:
          type: string
        diagnosis:
          type:
          - string
          - 'null'
        recommended_fixes:
          description: Suggestion list or object (jsonb); shape varies by node. Null when unset.
        last_audit_run_id:
          type:
          - string
          - 'null'
          format: uuid
        updated_at:
          type: string
          format: date-time
      required:
      - node_id
      - status
    NodesListResponse:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/NodeProgress'
        count:
          type: integer
      required:
      - profile_id
      - nodes
      - count
    NodeCompleteRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        nodeId:
          type: string
          minLength: 1
      required:
      - profileId
      - nodeId
    NodeCompleteResponse:
      type: object
      properties:
        node_id:
          type: string
        prior_status:
          type:
          - string
          - 'null'
        status:
          type: string
        updated_at:
          type: string
          format: date-time
        last_audit_run_id:
          type:
          - string
          - 'null'
          format: uuid
      required:
      - node_id
      - status
    BlogTopicsListRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 200
      required:
      - profileId
    BlogTopic:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        status:
          type: string
        generation_stage:
          type:
          - string
          - 'null'
        scheduled_date:
          type:
          - string
          - 'null'
          format: date
      required:
      - id
      - title
    BlogTopicsListResponse:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        topics:
          type: array
          items:
            $ref: '#/components/schemas/BlogTopic'
        count:
          type: integer
      required:
      - profile_id
      - topics
      - count
    GenerateBlogTopicsRequest:
      type: object
      properties:
        profileId:
          type: string
        days:
          type: integer
          default: 30
          minimum: 1
          maximum: 180
        velocity:
          type: integer
          minimum: 1
          maximum: 5
      required:
      - profileId
    JobQueuedResponse:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        jobId:
          type: string
        status:
          type: string
          const: queued
      required:
      - ok
      - jobId
    GenerateBlogPostRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        topicId:
          type: string
          format: uuid
        dryRun:
          type: boolean
      required:
      - profileId
      - topicId
    GenerateBlogPostResponse:
      type: object
      description: 'ok:true → claim succeeded. ok:false + code → expected business failure (topic_wrong_status
        | connection_missing | tier_too_low | monthly_cap_reached | topic_already_claimed). dryRun returns
        a preview.

        '
      properties:
        ok:
          type: boolean
        claimed:
          type: boolean
        topicId:
          type: string
        code:
          type: string
        message:
          type: string
        error:
          type: string
      required:
      - ok
    GenerateContentStrategyResponse:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        strategy:
          type: string
      required:
      - ok
      - strategy
    SaveCmsConnectionRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        site_url:
          type: string
          format: uri
        username:
          type: string
          minLength: 1
          maxLength: 200
        app_password:
          type: string
          minLength: 1
          maxLength: 400
      required:
      - profileId
      - site_url
      - username
      - app_password
    SaveCmsConnectionResponse:
      type: object
      description: ok:true → verified+saved; ok:false + code:connection_failed → probe failed.
      properties:
        ok:
          type: boolean
        last_verified_at:
          type: string
          format: date-time
        error:
          type: string
        code:
          type: string
        message:
          type: string
        wp_error_code:
          type: string
      required:
      - ok
    BuzzEngagementsListRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
          description: Scope to one profile (ownership-checked). Omit to list across the account.
        status:
          type: string
          description: Filter by engagement status (e.g. draft, approved, scheduled, posted, failed).
        since:
          type: string
          description: Time window — a relative span ("7d", "24h") or an ISO 8601 timestamp; filters created_at.
        groupBy:
          type: string
          description: When "subreddit", include a per-subreddit total in the response.
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 200
    BuzzEngagement:
      type: object
      properties:
        id:
          type: string
          format: uuid
        profile_id:
          type: string
          format: uuid
        status:
          type: string
        post_type:
          type:
          - string
          - 'null'
        subreddit:
          type:
          - string
          - 'null'
        draft_content:
          type:
          - string
          - 'null'
        scheduled_at:
          type:
          - string
          - 'null'
          format: date-time
        posted_at:
          type:
          - string
          - 'null'
          format: date-time
        retry_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
      - id
      - profile_id
      - status
    BuzzEngagementsListResponse:
      type: object
      properties:
        engagements:
          type: array
          items:
            $ref: '#/components/schemas/BuzzEngagement'
        total:
          type: integer
        by_subreddit:
          type: object
          additionalProperties:
            type: integer
      required:
      - engagements
      - total
    BuzzDraftApproveRequest:
      type: object
      properties:
        draftId:
          type: string
          format: uuid
        scheduleAt:
          type: string
          format: date-time
          description: ISO 8601. When present the draft is scheduled; otherwise it's approved.
      required:
      - draftId
    BuzzResearchListRequest:
      type: object
      properties:
        profileId:
          type: string
          format: uuid
        minRelevance:
          type: number
          minimum: 0
          maximum: 1
          default: 0.7
          description: Only return mentions with relevance_score at or above this. Mentions with a null
            relevance_score (unclassified) are always included regardless of the threshold.
        limit:
          type: integer
          minimum: 1
          maximum: 500
          default: 200
      required:
      - profileId
    BuzzMention:
      type: object
      properties:
        id:
          type: string
          format: uuid
        post_url:
          type:
          - string
          - 'null'
        post_title:
          type:
          - string
          - 'null'
        subreddit:
          type:
          - string
          - 'null'
        mention_type:
          type:
          - string
          - 'null'
        sentiment:
          type:
          - string
          - 'null'
        relevance_score:
          type:
          - number
          - 'null'
        created_at:
          type: string
          format: date-time
      required:
      - id
    BuzzResearchListResponse:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        mentions:
          type: array
          items:
            $ref: '#/components/schemas/BuzzMention'
        count:
          type: integer
      required:
      - profile_id
      - mentions
      - count
    BuzzProfileGetResponse:
      type: object
      properties:
        profile_id:
          type: string
          format: uuid
        research_status:
          type:
          - string
          - 'null'
        last_research_at:
          type:
          - string
          - 'null'
          format: date-time
      required:
      - profile_id
    BuzzBrandResearchResponse:
      type: object
      description: Async default `{success,started}`; sync (wait:true) returns the full result.
      properties:
        success:
          type: boolean
        started:
          type: boolean
        posts_processed:
          type: integer
        subreddits_found:
          type: array
          items:
            type: string
        research_summary:
          type: object
          properties:
            total_mentions:
              type: integer
            sentiment_breakdown:
              type: object
              properties:
                positive:
                  type: integer
                neutral:
                  type: integer
                negative:
                  type: integer
            top_subreddits:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                  count:
                    type: integer
            keyword_matches:
              type: integer
      required:
      - success
    BuzzGenerateDraftsRequest:
      type: object
      properties:
        profile_id:
          type: string
        engagement_id:
          type: string
          description: Single-engagement regeneration path.
        post_type:
          type: string
          enum:
          - comment
          - post
          description: Single path only.
      required:
      - profile_id
    BuzzGenerateDraftsResponse:
      type: object
      description: Single-engagement returns draft_content; bulk returns counts.
      properties:
        success:
          type: boolean
        draft_content:
          type: string
        post_type:
          type: string
          enum:
          - comment
          - post
        skipped:
          type: boolean
        reason:
          type: string
        drafts_created:
          type: integer
        comment_drafts:
          type: integer
        post_drafts:
          type: integer
        drafts_skipped:
          type: integer
      required:
      - success
    BuzzOpportunitySearchResponse:
      type: object
      description: Async default `{success,started}`; sync (wait:true) returns counts.
      properties:
        success:
          type: boolean
        started:
          type: boolean
        opportunities_found:
          type: integer
        posts_processed:
          type: integer
        message:
          type: string
      required:
      - success
    BuzzVisibilityScoreResponse:
      type: object
      properties:
        score:
          type: number
          minimum: 0
          maximum: 100
        total_engagements:
          type: integer
        mentions:
          type: integer
        opportunities:
          type: integer
        posted:
          type: integer
        drafted:
          type: integer
        sentiment:
          type: object
          properties:
            positive:
              type: integer
            negative:
              type: integer
            neutral:
              type: integer
        top_subreddits:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              total:
                type: integer
              posted:
                type: integer
              avg_upvotes:
                type: number
        opportunity_rate:
          type: number
        engagement_metrics:
          type: object
          properties:
            total_tracked:
              type: integer
            total_upvotes:
              type: integer
            avg_upvotes:
              type: number
            total_comments:
              type: integer
            avg_comments:
              type: number
        by_opportunity_type:
          type: object
          additionalProperties:
            type: integer
      required:
      - score
    DeleteAuditRunRequest:
      type: object
      properties:
        runId:
          type: string
      required:
      - runId
    DeleteAuditRunResponse:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        deleted:
          type: object
          properties:
            audit_run:
              type: string
            website_url:
              type: string
            node_rows:
              type: integer
      required:
      - ok
    CreateLeadAccountRequest:
      type: object
      properties:
        email:
          type: string
          format: email
        business_name:
          type: string
        website_url:
          type: string
        industry:
          type: string
          enum:
          - local_service
          - ecommerce
          - b2b_saas
        location:
          type: string
        redirect_to:
          type: string
          default: https://app.rankrush.ai
      required:
      - email
      - business_name
      - website_url
      - industry
    CreateLeadAccountResponse:
      type: object
      properties:
        success:
          type: boolean
          const: true
        userId:
          type: string
        profileId:
          type: string
        runId:
          type: string
        confirmUrl:
          type: string
        scan:
          type: object
          properties:
            keywords:
              type: integer
            written:
              type: integer
            error:
              type:
              - string
              - 'null'
            summary:
              type: string
      required:
      - success
      - userId
      - profileId
    SuggestCompetitorsRequest:
      type: object
      properties:
        websiteUrl:
          type: string
          format: uri
          maxLength: 500
        industry:
          type:
          - string
          - 'null'
          enum:
          - local_service
          - b2b_saas
          - ecommerce
          - null
        businessName:
          type: string
          maxLength: 200
        specialty:
          type: string
          maxLength: 500
        businessDescription:
          type: string
          maxLength: 500
        primaryCountry:
          type: string
          maxLength: 100
        additionalMarkets:
          type: array
          items:
            type: string
          maxItems: 5
        businessLanguage:
          type: string
          maxLength: 10
      required:
      - websiteUrl
    SuggestCompetitorsResponse:
      type: object
      properties:
        success:
          type: boolean
        category:
          type: object
          properties:
            primary:
              type: string
            subcategory:
              type: string
            geography:
              type: string
            pricePoint:
              type: string
            businessModel:
              type: string
        competitors:
          type: array
          maxItems: 10
          items:
            type: object
            properties:
              domain:
                type: string
              reason:
                type: string
              confidence:
                type: string
                enum:
                - high
                - medium
                - low
        message:
          type: string
      required:
      - success
    GenerateNodeContentRequest:
      type: object
      properties:
        nodeId:
          type: string
          minLength: 1
          maxLength: 100
        nodeName:
          type: string
          maxLength: 200
        taskId:
          type: string
          minLength: 1
          maxLength: 100
        taskName:
          type: string
          maxLength: 200
        promptTemplate:
          type: string
          minLength: 1
          maxLength: 10000
        businessName:
          type: string
          minLength: 1
          maxLength: 200
        industry:
          type: string
          enum:
          - local_service
          - b2b_saas
          - ecommerce
        location:
          type: string
          maxLength: 200
        specialty:
          type: string
          maxLength: 200
        businessDescription:
          type: string
          maxLength: 500
        websiteUrl:
          type: string
          maxLength: 500
        ecommercePlatform:
          type:
          - string
          - 'null'
          enum:
          - woocommerce
          - shopify
          - magento
          - custom
          - null
      required:
      - nodeId
      - taskId
      - promptTemplate
      - businessName
      - industry
    GenerateNodeContentResponse:
      type: object
      properties:
        success:
          type: boolean
        content:
          description: JSON-parsed object if parseable, else raw string.
          oneOf:
          - type: object
            additionalProperties: true
          - type: string
        taskId:
          type: string
        nodeId:
          type: string
        generatedAt:
          type: string
          format: date-time
      required:
      - success
      - content
    ExtractOfferingsRequest:
      type: object
      properties:
        profileId:
          type: string
        force:
          type: boolean
          description: Re-extract even if confirmed offerings exist.
      required:
      - profileId
    ExtractOfferingsResponse:
      type: object
      properties:
        success:
          type: boolean
        jobId:
          type: string
        profileId:
          type: string
        status:
          type: string
          const: processing
        skipped:
          type: boolean
        reason:
          type: string
      required:
      - success
    GscSelectPropertyRequest:
      type: object
      properties:
        profileId:
          type: string
        propertyUrl:
          type: string
      required:
      - profileId
      - propertyUrl
    GscFetchQueriesResponse:
      type: object
      description: success:false (200) when GSC not connected or token expired.
      properties:
        success:
          type: boolean
        queryCount:
          type: integer
        queries:
          type: array
          maxItems: 20
          items:
            type: object
            properties:
              query:
                type: string
              clicks:
                type: integer
              impressions:
                type: integer
              ctr:
                type: number
              position:
                type: number
        error:
          type: string
        tokenExpired:
          type: boolean
      required:
      - success
    BuzzExtractProfileResponse:
      type: object
      properties:
        success:
          type: boolean
        extracted:
          type: object
          properties:
            brand_voice:
              type: string
            persona_description:
              type: string
            target_subreddits:
              type: array
              items:
                type: string
            auto_extracted_keywords:
              type: array
              items:
                type: string
            reddit_keywords:
              type: array
              items:
                type: string
            competitor_brands:
              type: array
              items:
                type: string
      required:
      - success
      - extracted
    VerifyBlogPostsRequest:
      type: object
      description: Single-topic mode when topicId present (needs profileId); empty {} = cron sweep (service-role
        only).
      properties:
        profileId:
          type: string
        topicId:
          type: string
    VerifyBlogPostsResponse:
      type: object
      properties:
        checked:
          type: integer
        published:
          type: integer
        trashed:
          type: integer
        reverted:
          type: integer
        errors:
          type: integer
        message:
          type: string
      required:
      - checked
