openapi: 3.1.0
info:
  title: Seeklyn API
  version: '1'
  description: >
    Deterministic SEO / AEO scoring API. Headline and content analysis
    endpoints are free and keyless (rate-limited per IP). Metered endpoints
    (cited, history, share, badge) require an API key.
  contact:
    url: https://seeklyn.com/contact

servers:
  - url: https://api.seeklyn.com
    description: Production

security: []   # no global auth — per-operation where needed

components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: >
        API key issued from the account settings page (prefix `sk_…`).
        Alternatively pass as the `X-Seeklyn-Key` request header.
    headerKey:
      type: apiKey
      in: header
      name: X-Seeklyn-Key

  schemas:
    # ── Headline ──────────────────────────────────────────────────────────────

    HeadlineContext:
      type: object
      properties:
        keyword:
          type: string
        channel:
          type: string
        audience:
          type: string

    HeadlineScores:
      type: object
      properties:
        ctr:
          type: integer
          description: Click-through-rate sub-score (0-100)
        aeo:
          type: integer
          description: Answer-engine-optimisation sub-score (0-100)

    Signal:
      type: object
      properties:
        key:
          type: string
        label:
          type: string
        impact:
          type: string
          enum: [high, medium, bonus]
        present:
          type: boolean
        detail:
          type: string
        tip:
          type: string

    AEOReadiness:
      type: object
      properties:
        signalsPresent:
          type: integer
        signalsTotal:
          type: integer
        grade:
          type: string
        percentile:
          type: integer
        headline:
          type: string
        signals:
          type: array
          items:
            $ref: '#/components/schemas/Signal'

    DimensionMeter:
      type: object
      properties:
        value:
          type: integer
        ideal:
          type: integer
        min:
          type: integer
        max:
          type: integer
        score:
          type: integer
        position:
          type: number
          format: float
        message:
          type: string

    HeadlineType:
      type: object
      properties:
        type:
          type: string
        label:
          type: string
        confidence:
          type: number
          format: float
        message:
          type: string

    ReadingGrade:
      type: object
      properties:
        grade:
          type: integer
        min:
          type: integer
        max:
          type: integer
        position:
          type: number
          format: float
        message:
          type: string

    Sentiment:
      type: object
      properties:
        label:
          type: string
        strength:
          type: integer
        message:
          type: string
        positiveWords:
          type: array
          items:
            type: string
        negativeWords:
          type: array
          items:
            type: string

    Clarity:
      type: object
      properties:
        clear:
          type: boolean
        message:
          type: string

    Skimmability:
      type: object
      properties:
        pass:
          type: boolean
        message:
          type: string
        tokens:
          type: array
          items:
            type: string
        topicStart:
          type: integer
        topicEnd:
          type: integer

    Preview:
      type: object
      properties:
        domain:
          type: string
        slug:
          type: string
        title:
          type: string
        snippet:
          type: string

    RewriteRisk:
      type: object
      properties:
        level:
          type: string
          enum: [low, medium, high]
        message:
          type: string
        saferVariant:
          type: string
        h1Mismatch:
          type: boolean
        h1Provided:
          type: boolean

    SubScores:
      type: object
      properties:
        wordBalance:
          type: integer
        headlineType:
          type: integer
        length:
          type: integer
        sentiment:
          type: integer
        readability:
          type: integer
        aeoExtractability:
          type: integer

    WordBalance:
      type: object
      description: Per-word POS/category breakdown and four vocabulary buckets
      properties:
        words:
          type: array
          items:
            type: object
            properties:
              text:
                type: string
              category:
                type: string
              pos:
                type: string
        common:
          type: object
        uncommon:
          type: object
        emotional:
          type: object
        power:
          type: object

    Readability:
      type: object
      properties:
        fkGrade:
          type: number
          format: float
        fleschEase:
          type: number
          format: float
        syllables:
          type: integer

    PositionCheck:
      type: object
      properties:
        firstThree:
          type: array
          items:
            type: string
        lastThree:
          type: array
          items:
            type: string
        keywordPlaced:
          type: boolean

    Suggestion:
      type: object
      properties:
        dimension:
          type: string
        severity:
          type: string
          enum: [low, medium, high]
        message:
          type: string

    HeadlineResult:
      type: object
      description: Full response for headline analysis (POST or GET)
      properties:
        title:
          type: string
        score:
          type: integer
          description: Composite headline score (0-100)
        band:
          type: string
          description: Score band label (e.g. "Strong", "Good", "Fair", "Weak")
        scores:
          $ref: '#/components/schemas/HeadlineScores'
        aeoReadiness:
          $ref: '#/components/schemas/AEOReadiness'
        length:
          type: object
          properties:
            chars:
              type: integer
            words:
              type: integer
        wordCount:
          $ref: '#/components/schemas/DimensionMeter'
        characterCount:
          $ref: '#/components/schemas/DimensionMeter'
        headlineType:
          $ref: '#/components/schemas/HeadlineType'
        readingGrade:
          $ref: '#/components/schemas/ReadingGrade'
        sentiment:
          $ref: '#/components/schemas/Sentiment'
        clarity:
          $ref: '#/components/schemas/Clarity'
        skimmability:
          $ref: '#/components/schemas/Skimmability'
        wordBalance:
          $ref: '#/components/schemas/WordBalance'
        preview:
          $ref: '#/components/schemas/Preview'
        rewriteRisk:
          $ref: '#/components/schemas/RewriteRisk'
        subScores:
          $ref: '#/components/schemas/SubScores'
        readability:
          $ref: '#/components/schemas/Readability'
        positionCheck:
          $ref: '#/components/schemas/PositionCheck'
        suggestions:
          type: array
          items:
            $ref: '#/components/schemas/Suggestion'
        ai:
          nullable: true
          description: Reserved for deep-mode LLM result; null in the current release
          type: object

    # ── Content / passage ─────────────────────────────────────────────────────

    ContentFunnel:
      type: object
      properties:
        activation:
          nullable: true
          type: integer
          description: Null until activation-preflight feature ships
        retrieval:
          type: integer
        citation:
          type: integer
        overall:
          type: integer

    ContentResult:
      type: object
      description: Full response for passage / website citability analysis
      properties:
        funnel:
          $ref: '#/components/schemas/ContentFunnel'
        retrievalSim:
          type: object
          description: BM25 retrieval simulation over the document's own chunks
          properties:
            query:
              type: string
            topK:
              type: array
              items:
                type: object
                properties:
                  chunkIndex:
                    type: integer
                  bm25:
                    type: number
                    format: float
                  rank:
                    type: integer
            orphanChunks:
              type: array
              items:
                type: integer
        coverage:
          type: object
          description: Query fan-out coverage (sub-question coverage)
          properties:
            query:
              type: string
            topic:
              type: string
            covered:
              type: integer
            weak:
              type: integer
            total:
              type: integer
            score:
              type: integer
            subQuestions:
              type: array
              items:
                type: object
        chunks:
          type: array
          description: Per-chunk (100-300 word passage) analysis
          items:
            type: object
            properties:
              index:
                type: integer
              heading:
                nullable: true
                type: string
              text:
                type: string
              wordCount:
                type: integer
              idealBand:
                type: boolean
              score:
                type: integer
              bm25Rank:
                type: integer
              selfContained:
                type: boolean
              flags:
                type: array
                items:
                  type: object
              sentences:
                type: array
                items:
                  type: object
              fix:
                nullable: true
                type: object
              evidenceGaps:
                type: array
                items:
                  type: object
              llmRewrite:
                nullable: true
                type: object
        rubricVersion:
          type: string
          example: content-v1
        source:
          type: object
          properties:
            title:
              type: string
            content:
              type: string
            fromUrl:
              type: boolean
        landing:
          nullable: true
          type: object
          description: Website-type audit result; null for article type
        ai:
          nullable: true
          type: object
          description: LLM judge result (deep mode, paid); null in instant mode

paths:
  /api/health:
    get:
      operationId: health
      summary: Health check
      tags: [Utility]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  service:
                    type: string
                    example: seeklyn

  /api/v1/analyze:
    get:
      operationId: analyzeHeadlineGet
      summary: Analyze a headline (keyless GET)
      description: >
        Score a headline for Google CTR and AEO citability. No signup or key
        required — pass the title as a query parameter. Rate-limited per IP.
      tags: [Headline]
      parameters:
        - name: title
          in: query
          required: true
          schema:
            type: string
          description: The headline text to score
        - name: keyword
          in: query
          required: false
          schema:
            type: string
          description: Optional target keyword for keyword-placement checks
      responses:
        '200':
          description: Headline analysis result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeadlineResult'
        '400':
          description: Missing or empty title
        '429':
          description: Rate limit exceeded

    post:
      operationId: analyzeHeadlinePost
      summary: Analyze a headline (POST)
      description: >
        Score a headline for Google CTR and AEO citability. Accepts an optional
        H1, mode, and context. With an API key the result is persisted to your
        history and attributed to your plan.
      tags: [Headline]
      security:
        - {}
        - bearerKey: []
        - headerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title:
                  type: string
                h1:
                  type: string
                  description: >
                    On-page H1. When provided, powers the title↔H1 mismatch
                    signal in rewriteRisk.
                mode:
                  type: string
                  enum: [instant]
                  default: instant
                context:
                  $ref: '#/components/schemas/HeadlineContext'
            example:
              title: "10 SEO Mistakes That Kill Your Google Rankings"
              context:
                keyword: "SEO mistakes"
      responses:
        '200':
          description: Headline analysis result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeadlineResult'
        '400':
          description: Missing or empty title
        '429':
          description: Rate limit exceeded

  /api/v1/analyze-content:
    post:
      operationId: analyzeContent
      summary: Analyze passage or website citability
      description: >
        Run the Citation Funnel on a passage of text (`type: article`, default)
        or a URL (`type: website`). Returns BM25 retrieval simulation,
        per-chunk extractability lint, query fan-out coverage, and an optional
        deterministic fix. Without an API key the request is rate-limited and
        LLM-rewrite is disabled.
      tags: [Content]
      security:
        - {}
        - bearerKey: []
        - headerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                  description: Article title (used in scoring; required for article type)
                content:
                  type: string
                  description: Article body text (mutually exclusive with url)
                url:
                  type: string
                  format: uri
                  description: URL to fetch and analyze (mutually exclusive with content)
                targetQuery:
                  type: string
                  description: The search query this content should rank for
                type:
                  type: string
                  enum: [article, website]
                  default: article
                mode:
                  type: string
                  enum: [instant]
                  default: instant
                context:
                  type: object
                  properties:
                    keyword:
                      type: string
            examples:
              article:
                summary: Analyze article text
                value:
                  title: "Best practices for AEO"
                  content: "Answer Engine Optimization (AEO) is…"
                  targetQuery: "what is answer engine optimization"
              url:
                summary: Analyze a website URL
                value:
                  url: "https://example.com/blog/post"
                  type: website
      responses:
        '200':
          description: Content analysis result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContentResult'
        '400':
          description: Bad request (missing title+content or url)
        '429':
          description: Rate limit exceeded

  /api/v1/account/keys:
    post:
      operationId: createApiKey
      summary: Create an API key
      tags: [Keys]
      security:
        - bearerKey: []
        - headerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '200':
          description: The new key (plaintext shown once)
          content:
            application/json:
              schema:
                type: object
                description: Key metadata including the one-time plaintext key field
    get:
      operationId: listApiKeys
      summary: List API keys (metadata only)
      tags: [Keys]
      security:
        - bearerKey: []
        - headerKey: []
      responses:
        '200':
          description: Array of key metadata objects

  /api/v1/account/keys/{id}:
    delete:
      operationId: revokeApiKey
      summary: Revoke an API key
      tags: [Keys]
      security:
        - bearerKey: []
        - headerKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  revoked:
                    type: boolean

  /api/v1/cited:
    post:
      operationId: citedCheck
      summary: Check if a URL or domain is cited by AI engines (metered)
      tags: [Cited]
      security:
        - bearerKey: []
        - headerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                url:
                  type: string
                  format: uri
                domain:
                  type: string
      responses:
        '200':
          description: Citation check result per engine
        '429':
          description: Quota exceeded

  /api/v1/cited/check:
    get:
      operationId: citedCheckGet
      summary: Check citation (GET form, metered)
      tags: [Cited]
      security:
        - bearerKey: []
        - headerKey: []
      parameters:
        - name: query
          in: query
          required: true
          schema:
            type: string
        - name: url
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Citation check result per engine

  /api/v1/cited/history:
    get:
      operationId: citedHistory
      summary: Retrieve citation ledger (trend feed)
      tags: [Cited]
      security:
        - bearerKey: []
        - headerKey: []
      parameters:
        - name: domain
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Historical citation observations

  /api/v1/badge.svg:
    get:
      operationId: badge
      summary: Embeddable SVG score badge
      tags: [Utility]
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
            format: uri
      responses:
        '200':
          description: SVG badge
          content:
            image/svg+xml:
              schema:
                type: string

  /api/v1/share:
    post:
      operationId: createShare
      summary: Create a shareable report URL
      tags: [Utility]
      security:
        - {}
        - bearerKey: []
        - headerKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                kind:
                  type: string
                title:
                  type: string
                score:
                  type: integer
                band:
                  type: string
                payload:
                  type: object
      responses:
        '200':
          description: Share URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri

  /mcp:
    post:
      operationId: mcpEndpoint
      summary: Remote MCP endpoint (streamable HTTP)
      description: >
        Streamable-HTTP MCP endpoint. Paste `https://api.seeklyn.com/mcp` into
        Claude.ai custom connectors, Cursor, or any MCP-aware tool. No API key
        required. Tools: `analyze_passage` and `audit_url`.
      tags: [MCP]
      responses:
        '200':
          description: MCP JSON-RPC response
