openapi: 3.1.0
info:
  title: Trawlspace API
  version: 0.1.0-preview
  summary: Crawl, extract, enrich, and track any site through one API.
  description: 'The API-first crawler for SEO and AEO. Trawlspace crawls a site, extracts every on-page
    and AEO signal as structured data, runs LLM enrichment across the crawl, tracks change over time,
    and delivers clean tables into your warehouse or tools.


    Conventions: authenticate with a bearer API key; every mutating call accepts an Idempotency-Key; lists
    are cursor-paginated; errors share one shape with a request_id and a doc_url.'
  contact:
    name: Trawlspace API
    url: https://docs.trawlspace.com
servers:
- url: https://api.trawlspace.com/v1
  description: Production
- url: https://api.trawlspace.com/v1
  description: Sandbox (use an sk_test_ key)
security:
- bearerAuth: []
tags:
- name: Sites
  description: Monitored properties. A site owns crawl config, gated-page auth, the warehouse connection,
    and the schedule.
- name: Configuration
  description: Versioned config objects on a site, snapshotted into every crawl for reproducibility.
- name: Crawls
  description: 'The primary async resource: one run against a site. (Legacy docs also called this a "job";
    `crawls` is canonical.) Named selectors: any crawl path accepts `latest`, `previous`, or `production`
    (a promoted baseline) in place of an id, resolved within the key project scope, so `/crawls/latest/pages`
    and `/crawls/latest/diff?against=production` work without tracking ids.'
- name: Pages
  description: Per-URL results, a superset of a Screaming Frog export. Includes the synchronous single-URL
    audit.
- name: Issues
  description: Normalized, typed, severity-rated findings, queryable as first-class objects.
- name: Enrichment
  description: The LLM work product at five altitudes. Drafted output your team takes or leaves.
- name: Link graph
  description: Inlinks, redirects, broken links, orphans, and anchors, queryable and warehouse-ready.
- name: Diffs
  description: The structured field diff plus an AI-digested change narrative and audit log.
- name: Perception and brand voice
  description: Site-side perception legibility. Does each page convey your intended positioning in an
    AI-legible way.
- name: AI readiness
  description: The fixable, site-side part of AEO. Can the AI crawlers reach the page, and is the content
    answer-ready. Not citation tracking.
- name: Usage and billing
  description: Two-meter metered usage (crawl credits and enrichment credits) with caps and plan mapping.
- name: Account
  description: API keys and team management.
paths:
  /sites:
    post:
      tags:
      - Sites
      summary: Create a site
      operationId: createSite
      x-maturity: GA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteCreate'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
        '400':
          $ref: '#/components/responses/Error'
      x-priority: P0
    get:
      tags:
      - Sites
      summary: List sites
      operationId: listSites
      x-maturity: GA
      parameters:
      - $ref: '#/components/parameters/limit'
      - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Page_'
                - type: object
                  properties:
                    data:
                      type: array
                      items:
                        $ref: '#/components/schemas/Site'
      x-priority: P0
  /sites/{id}:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Sites
      summary: Retrieve a site
      operationId: getSite
      x-maturity: GA
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
        '404':
          $ref: '#/components/responses/Error'
      x-priority: P0
    patch:
      tags:
      - Sites
      summary: Update a site
      operationId: updateSite
      x-maturity: GA
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteCreate'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Site'
      x-priority: P0
    delete:
      tags:
      - Sites
      summary: Delete a site and its crawl history (irreversible)
      operationId: deleteSite
      x-maturity: GA
      parameters:
      - name: confirm
        in: query
        required: true
        schema:
          type: boolean
        description: Must be true.
      responses:
        '204':
          description: Deleted
      x-priority: P0
  /sites/{id}/verify:
    parameters:
    - $ref: '#/components/parameters/id'
    post:
      tags:
      - Sites
      summary: Start or check domain-ownership verification
      operationId: verifySite
      x-maturity: GA
      responses:
        '200':
          description: Verification status
          content:
            application/json:
              schema:
                type: object
                properties:
                  method:
                    type: string
                    enum:
                    - dns
                    - meta_tag
                  verified:
                    type: boolean
                  token:
                    type: string
      x-priority: P0
  /sites/{id}/config:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Configuration
      summary: Set crawl scope, render policy, rate limits, and auth
      description: Importable from a Screaming Frog profile. Returns a version snapshotted into every
        crawl.
      operationId: putConfig
      x-maturity: GA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SiteConfig'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SiteConfig'
      x-priority: P0
  /sites/{id}/extractors:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Configuration
      summary: Custom extraction fields (XPath / CSS / regex, or plain-English semantic)
      description: 'The upgrade over Screaming Frog: describe a field in plain English ("pull the price
        and availability") and it keeps working when the DOM changes. Returned as first-class columns.'
      operationId: putExtractors
      x-maturity: GA
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                extractors:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                      type:
                        type: string
                        enum:
                        - xpath
                        - css
                        - regex
                        - semantic
                      expression:
                        type: string
                        description: The selector, or a plain-English description for a semantic field.
      responses:
        '200':
          description: OK
      x-priority: P0
  /sites/{id}/rules:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Configuration
      summary: Custom issue rules, severities, mutes, and pass/fail assertions
      description: These assertions also power the CI/CD gate.
      operationId: putRules
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P0
  /sites/{id}/categorization:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Configuration
      summary: Page-type and segment definitions
      description: By URL pattern, template fingerprint, extracted field, or a combining rule (e.g. `/products/*
        AND has(price) -> PDP`). Every score, alert, and rollup is filterable by these.
      operationId: putCategorization
      x-maturity: GA
      responses:
        '200':
          description: OK
      x-priority: P0
  /sites/{id}/enrichment:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Configuration
      summary: Which altitudes and passes run, prompt/rubric overrides, house style
      operationId: putEnrichmentConfig
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P0
  /sites/{id}/brand-voice:
    parameters:
    - $ref: '#/components/parameters/id'
    put:
      tags:
      - Perception and brand voice
      summary: Define the voice/positioning rubric, or infer it from exemplar_urls
      operationId: putBrandVoice
      x-maturity: Beta
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rubric:
                  type: object
                  description: Explicit voice dimensions and do/don't lexicon.
                exemplar_urls:
                  type: array
                  items:
                    type: string
                    format: uri
                  description: If provided, Trawlspace infers the rubric in one LLM pass over these best
                    pages.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BrandVoiceRubric'
      x-priority: P1
  /sites/{id}/crawls:
    parameters:
    - $ref: '#/components/parameters/id'
    post:
      tags:
      - Crawls
      summary: Start a crawl
      description: '`url_list` runs list-mode (sitemap / GSC export / paste) instead of a frontier crawl.
        Returns 202 with the crawl in `queued`. Idempotent with an `Idempotency-Key`.'
      operationId: createCrawl
      x-maturity: GA
      parameters:
      - name: Idempotency-Key
        in: header
        schema:
          type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                config_overrides:
                  type: object
                url_list:
                  type: array
                  items:
                    type: string
                    format: uri
                enrich:
                  type: array
                  items:
                    type: string
                  description: Altitudes to run inline (e.g. page).
      responses:
        '202':
          description: Accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Crawl'
      x-priority: P0
  /crawls:
    post:
      tags:
      - Crawls
      summary: Start a crawl (top-level; site in body)
      description: Ergonomic alias for POST /sites/{site_id}/crawls, matching the one-call story.
      operationId: createCrawlTopLevel
      x-maturity: GA
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                site_id:
                  type: string
                url:
                  type: string
                  format: uri
                  description: If site_id is omitted, resolve or create a site from this URL.
                config_overrides:
                  type: object
      responses:
        '202':
          description: Accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Crawl'
      x-priority: P0
    get:
      tags:
      - Crawls
      summary: List crawls
      operationId: listCrawls
      x-maturity: GA
      parameters:
      - name: site_id
        in: query
        schema:
          type: string
      - name: status
        in: query
        schema:
          $ref: '#/components/schemas/CrawlStatus'
      - $ref: '#/components/parameters/limit'
      - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: OK
      x-priority: P0
  /crawls/{id}:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Crawls
      summary: Retrieve a crawl (status, stats, config snapshot)
      operationId: getCrawl
      x-maturity: GA
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Crawl'
      x-priority: P0
  /crawls/{id}/cancel:
    parameters:
    - $ref: '#/components/parameters/id'
    post:
      tags:
      - Crawls
      summary: Cancel a running crawl (completed pages remain queryable)
      operationId: cancelCrawl
      x-maturity: GA
      responses:
        '200':
          description: OK
      x-priority: P0
  /crawls/{id}/tag:
    parameters:
    - $ref: '#/components/parameters/id'
    post:
      tags:
      - Crawls
      summary: Promote a crawl to a named baseline (e.g. production)
      description: Lets CI diff a preview crawl against `production` without tracking ids.
      operationId: tagCrawl
      x-maturity: GA
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tag:
                  type: string
                  enum:
                  - production
                  - baseline
      responses:
        '200':
          description: OK
      x-priority: P0
  /crawls/{id}/pages:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Pages
      summary: List pages in a crawl
      operationId: listPages
      x-maturity: GA
      parameters:
      - name: segment
        in: query
        schema:
          type: string
      - name: status_code
        in: query
        schema:
          type: integer
      - name: indexable
        in: query
        schema:
          type: boolean
      - name: select
        in: query
        schema:
          type: string
        description: Comma-separated fields.
      - $ref: '#/components/parameters/limit'
      - $ref: '#/components/parameters/cursor'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Page_'
                - type: object
                  properties:
                    data:
                      type: array
                      items:
                        $ref: '#/components/schemas/PageResult'
      x-priority: P0
  /page:
    get:
      tags:
      - Pages
      summary: Synchronous single-URL audit (no crawl, no setup)
      description: Point at any live URL and get on-page facts, enriched fields, and ranked issues in
        one call. The fastest aha.
      operationId: auditPage
      x-maturity: GA
      parameters:
      - name: url
        in: query
        required: true
        schema:
          type: string
          format: uri
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PageResult'
      x-priority: P0
  /crawls/{id}/issues:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Issues
      summary: List issues in a crawl
      operationId: listIssues
      x-maturity: GA
      parameters:
      - name: severity
        in: query
        schema:
          type: string
          enum:
          - critical
          - warning
          - notice
      - name: type
        in: query
        schema:
          type: string
      - name: segment
        in: query
        schema:
          type: string
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Issue'
      x-priority: P0
  /crawls/{id}/enrich:
    parameters:
    - $ref: '#/components/parameters/id'
    post:
      tags:
      - Enrichment
      summary: Run or re-run enrichment over a crawl or page subset
      description: Only changed pages re-enrich on monitored sites.
      operationId: enrichCrawl
      x-maturity: Beta
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                altitudes:
                  type: array
                  items:
                    type: string
                    enum:
                    - page
                    - interconnection
                    - change
                    - site
                    - competitive
                pages:
                  type: array
                  items:
                    type: string
      responses:
        '202':
          description: Accepted
      x-priority: P1
  /crawls/{id}/summary:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Enrichment
      summary: Site-altitude rollup (what the site is about, gaps, health, top fixes)
      operationId: getSummary
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/links:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Link graph
      summary: 'Every internal edge: source, target, anchor, rel, status'
      operationId: getLinks
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/redirects:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Link graph
      summary: Redirect chains and loops
      operationId: getRedirects
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/broken-links:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Link graph
      summary: 4xx/5xx targets and every page linking to them
      operationId: getBrokenLinks
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/orphans:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Link graph
      summary: Pages with no internal inlinks
      operationId: getOrphans
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/anchors:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Link graph
      summary: Anchor-text distribution and a PageRank-like internal score
      operationId: getAnchors
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/diff:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Diffs
      summary: Structured diff plus AI-digested change narrative and audit log
      operationId: getDiff
      x-maturity: Beta
      parameters:
      - name: against
        in: query
        required: true
        schema:
          type: string
        description: 'The prior crawl to compare against: an id or a named selector (previous, production,
          latest).'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Diff'
      x-priority: P1
  /crawls/{id}/brand-voice:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Perception and brand voice
      summary: Voice rollup (site score, per-segment breakdown, pages trending off-voice)
      operationId: getBrandVoiceRollup
      x-maturity: Beta
      responses:
        '200':
          description: OK
      x-priority: P1
  /crawls/{id}/perception:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - Perception and brand voice
      summary: Perception-alignment score at site and segment level
      description: How well each segment conveys the intended positioning, of which brand voice is one
        dimension.
      operationId: getPerception
      x-maturity: Beta
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Perception'
      x-priority: P1
  /crawls/{id}/ai-readiness:
    parameters:
    - $ref: '#/components/parameters/id'
    get:
      tags:
      - AI readiness
      summary: Per-bot crawlability and answer-readiness, with drafted fixes
      description: Per bot (GPTBot, OAI-SearchBot, ClaudeBot, Claude-SearchBot, PerplexityBot). Not citation
        tracking.
      operationId: getAiReadiness
      x-maturity: GA
      responses:
        '200':
          description: OK
      x-priority: P1
  /usage:
    get:
      tags:
      - Usage and billing
      summary: Metered usage with per-crawl breakdown and caps
      description: 'Two meters, never blended. Crawl credits (~1 per page crawled + audited, ~$1.50/1,000,
        ~$2.25/1,000 JS-heavy) and enrichment credits (~$0.10 per enriched page). Plan mapping is by the
        binding constraint, not price alone: Free is volume-gated (~1,000 pages/mo, no enrichment) and
        multi-client scale gates to Agency.'
      operationId: getUsage
      x-maturity: GA
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usage'
      x-priority: P0
  /usage/caps:
    put:
      tags:
      - Usage and billing
      summary: Set hard usage caps and an alert threshold
      description: .
      operationId: putUsageCaps
      x-maturity: GA
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                crawl_credit_cap:
                  type: integer
                enrichment_credit_cap:
                  type: integer
                alert_at_percent:
                  type: integer
                  description: Alert when usage reaches this percent of the cap.
      responses:
        '200':
          description: OK
      x-priority: P0
  /api-keys:
    get:
      tags:
      - Account
      summary: List API keys
      operationId: listApiKeys
      x-maturity: GA
      responses:
        '200':
          description: OK
      x-priority: P0
    post:
      tags:
      - Account
      summary: Create or rotate an API key (account-wide or project-scoped)
      operationId: createApiKey
      x-maturity: GA
      responses:
        '201':
          description: Created
      x-priority: P0
  /members:
    get:
      tags:
      - Account
      summary: List team members
      operationId: listMembers
      x-maturity: GA
      responses:
        '200':
          description: OK
      x-priority: P0
    post:
      tags:
      - Account
      summary: Invite a member (agency workspaces, per-client access)
      operationId: createMember
      x-maturity: GA
      responses:
        '201':
          description: Created
      x-priority: P0
  /api-keys/{id}:
    parameters:
    - $ref: '#/components/parameters/id'
    delete:
      tags:
      - Account
      summary: Revoke an API key
      operationId: revokeApiKey
      x-maturity: GA
      responses:
        '204':
          description: Deleted
      x-priority: P0
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Bearer API key. Prefixed `sk_live_` / `sk_test_`; can be project-scoped. Never send
        in a query string.
  parameters:
    id:
      name: id
      in: path
      required: true
      schema:
        type: string
      description: Resource id.
    limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 50
        maximum: 200
      description: Page size.
    cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: Opaque pagination cursor.
  responses:
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Page_:
      type: object
      description: Cursor pagination envelope.
      properties:
        data:
          type: array
          items: {}
        next_cursor:
          type: string
          nullable: true
        has_more:
          type: boolean
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum:
              - invalid_request
              - authentication
              - permission
              - not_found
              - rate_limit
              - conflict
              - server
            code:
              type: string
            message:
              type: string
            request_id:
              type: string
            doc_url:
              type: string
              format: uri
    SiteCreate:
      type: object
      required:
      - url
      properties:
        url:
          type: string
          format: uri
        name:
          type: string
        crawl_scope:
          $ref: '#/components/schemas/CrawlScope'
        auth:
          type: object
          description: Credentials for gated or staging crawls.
    CrawlScope:
      type: object
      properties:
        include:
          type: array
          items:
            type: string
        exclude:
          type: array
          items:
            type: string
        subdomains:
          type: boolean
    SiteConfig:
      type: object
      properties:
        crawl_scope:
          $ref: '#/components/schemas/CrawlScope'
        render_policy:
          type: string
          enum:
          - auto
          - js
          - browser
        rate_limit_rps:
          type: number
        version:
          type: integer
          readOnly: true
    Site:
      type: object
      properties:
        id:
          type: string
          examples:
          - site_9f2a
        url:
          type: string
          format: uri
        name:
          type: string
        verified:
          type: boolean
        crawl_scope:
          $ref: '#/components/schemas/CrawlScope'
        render_policy:
          type: string
          enum:
          - auto
          - js
          - browser
        warehouse_connection_id:
          type: string
          nullable: true
        schedule_id:
          type: string
          nullable: true
        last_crawl_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    CrawlStatus:
      type: string
      enum:
      - queued
      - running
      - completed
      - failed
      - canceled
    Crawl:
      type: object
      properties:
        id:
          type: string
          examples:
          - crl_a1b2c3
        site_id:
          type: string
        status:
          $ref: '#/components/schemas/CrawlStatus'
        trigger:
          type: string
          enum:
          - manual
          - scheduled
          - ci
          - api
        config_version:
          type: integer
        stats:
          type: object
          properties:
            pages_crawled:
              type: integer
            pages_billable:
              type: integer
              description: Lower than pages_crawled on re-crawls (conditional GET billed at ~35%).
            issues:
              type: integer
            failed_pages:
              type: integer
            duration_s:
              type: integer
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
    PageResult:
      type: object
      properties:
        url:
          type: string
          format: uri
        status:
          type: integer
        title:
          type: string
        title_len:
          type: integer
        meta_description_len:
          type: integer
        canonical:
          type: string
          nullable: true
        canonical_status:
          type: string
          enum:
          - self
          - other
          - missing
        indexable:
          type: boolean
        indexability_reason:
          type: string
          nullable: true
        robots:
          type: string
        hreflang:
          type: array
          items:
            type: string
        redirect_chain:
          type: array
          items:
            type: string
        h1:
          type: array
          items:
            type: string
        h2:
          type: array
          items:
            type: string
        structured_data:
          type: array
          items:
            type: string
        internal_inlinks:
          type: integer
        cwv:
          type: object
          properties:
            lcp_ms:
              type: integer
            cls:
              type: number
            inp_ms:
              type: integer
        segment:
          type: string
        rendered:
          type: object
          description: The JS-rendered value of each element (present when the render policy renders).
        render_divergence:
          type: boolean
          description: True when rendered content differs from raw HTML, so content that only appears
            after render is caught.
        enrichment:
          $ref: '#/components/schemas/Enrichment'
    Issue:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          examples:
          - cdn_blocks_perplexitybot
        severity:
          type: string
          enum:
          - critical
          - warning
          - notice
        page_url:
          type: string
          format: uri
        detail:
          type: string
        fix_available:
          type: boolean
        first_seen_crawl:
          type: string
        count:
          type: integer
    Enrichment:
      type: object
      description: Drafted output your team takes or leaves. Never auto-applied.
      properties:
        schema:
          type: object
          description: Generated, valid JSON-LD.
        title_rewrite:
          type: string
        meta_rewrite:
          type: string
        faq:
          type: array
          items:
            type: object
            properties:
              q:
                type: string
              a:
                type: string
        entities:
          type: array
          items:
            type: string
        answer_readiness:
          type: number
        perception:
          type: object
          properties:
            alignment:
              type: number
            gaps:
              type: array
              items:
                type: string
        brand_voice:
          type: object
          properties:
            score:
              type: integer
            drift_snippets:
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                  why:
                    type: string
    Diff:
      type: object
      properties:
        against:
          type: string
        narrative:
          type: string
          description: AI-digested "what changed and why it matters."
        audit_log:
          type: array
          items:
            type: object
        regressions:
          type: array
          items:
            $ref: '#/components/schemas/Issue'
    BrandVoiceRubric:
      type: object
      properties:
        dimensions:
          type: object
        lexicon:
          type: object
          properties:
            do:
              type: array
              items:
                type: string
            dont:
              type: array
              items:
                type: string
        inferred_from:
          type: array
          items:
            type: string
            format: uri
    Perception:
      type: object
      properties:
        site_alignment:
          type: number
          description: '0 to 1: how well the site conveys the intended positioning.'
        by_segment:
          type: object
          additionalProperties:
            type: number
        gaps:
          type: array
          items:
            type: string
    Visibility:
      type: object
      properties:
        by_category:
          type: array
          items:
            type: object
            properties:
              category:
                type: string
              mention_rate:
                type: number
              share_of_voice:
                type: number
              avg_position:
                type: number
        engine_coverage:
          type: object
          description: Per-engine coverage so gaps are never silent.
          additionalProperties:
            type: string
            enum:
            - passive
            - scheduled_prompt
            - unavailable
    Export:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum:
          - queued
          - running
          - completed
          - failed
        format:
          type: string
        url:
          type: string
          format: uri
          nullable: true
          description: Signed download URL when completed.
    Usage:
      type: object
      properties:
        period:
          type: string
        crawl_credits:
          type: object
          properties:
            used:
              type: integer
            cap:
              type: integer
        enrichment_credits:
          type: object
          properties:
            used:
              type: integer
            cap:
              type: integer
        estimated_cost_usd:
          type: number
        plan:
          type: string
          enum:
          - free
          - pro
          - agency
          - enterprise
    Integration:
      type: object
      properties:
        id:
          type: string
        provider:
          type: string
          enum:
          - gsc
          - ga4
          - psi
        status:
          type: string
          enum:
          - connected
          - pending
          - error
        connected_at:
          type: string
          format: date-time
    Workspace:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        site_count:
          type: integer
        created_at:
          type: string
          format: date-time
    HistoryPoint:
      type: object
      properties:
        date:
          type: string
          format: date
        metric:
          type: string
        value:
          type: number
