openapi: 3.1.0
info:
  title: zipzipdoc Public Content API
  version: "1.0.0"
  summary: Read-only structured access to zipzipdoc marketing and reference content.
  description: |
    Public, unauthenticated JSON API for AI agents and integrators.
    Lets agents fetch structured page bundles (title, description, BLUF,
    FAQs, primary entity, related entities) and run keyword search across
    every public page on zipzipdoc.com. Pair with /llms.txt and
    /llms-full.txt for orientation, and with /sitemap.xml for the full
    URL list across 29 locales.
  contact:
    name: zipzipdoc
    url: https://zipzipdoc.com
  license:
    name: Public content
    url: https://zipzipdoc.com/terms-of-service
servers:
  - url: https://zipzipdoc.com
    description: Production
paths:
  /api/content:
    get:
      operationId: getContent
      summary: Fetch structured content for a page path.
      description: |
        Returns the structured content bundle for a given page path. Mirrors
        the data the SSR renderer uses, so agents get the same source of
        truth as humans.
      parameters:
        - name: path
          in: query
          required: true
          description: Page path beginning with "/" (e.g. "/pricing", "/glossary/eidas").
          schema: { type: string, example: "/pricing" }
        - name: locale
          in: query
          required: false
          description: BCP-47 locale code. Defaults to "en".
          schema: { type: string, default: en, example: en }
      responses:
        "200":
          description: Page found.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContentBundle" }
              examples:
                pricing:
                  value:
                    metadata:
                      pageId: pricing
                      path: /pricing
                      locale: en
                      title: Pricing | zipzipdoc
                      description: One flat $29/month plan, unlimited users, unlimited documents.
                      bluf: One flat $29/month plan, unlimited users, unlimited documents, AI included.
                      lastModified: "2026-04-20"
        "404":
          description: Path does not match any known page.
  /api/search:
    get:
      operationId: searchContent
      summary: Keyword search across public pages.
      parameters:
        - name: q
          in: query
          required: true
          description: Search query (1-200 characters).
          schema: { type: string, minLength: 1, maxLength: 200, example: eIDAS }
        - name: locale
          in: query
          required: false
          schema: { type: string, default: en }
        - name: limit
          in: query
          required: false
          schema: { type: integer, default: 10, minimum: 1, maximum: 50 }
      responses:
        "200":
          description: Ranked search results.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SearchResponse" }
  /api/rag:
    get:
      operationId: ragQuery
      summary: Semantic retrieval for RAG.
      description: |
        Returns the most semantically relevant content chunks for a natural-language question.
        Backed by OpenAI text-embedding-3-small with in-process cosine similarity over a chunked
        index of every public page (chunks ≈ 300 tokens, 50-token overlap). Falls back to
        BM25 retrieval when no embedding key is configured. Designed for LLM agents doing RAG.
      parameters:
        - name: q
          in: query
          required: true
          description: Natural-language question (1-500 characters).
          schema: { type: string, minLength: 1, maxLength: 500, example: "How much does zipzipdoc cost?" }
        - name: locale
          in: query
          required: false
          schema: { type: string, default: en }
        - name: limit
          in: query
          required: false
          schema: { type: integer, default: 5, minimum: 1, maximum: 20 }
      responses:
        "200":
          description: Ranked content chunks ready to drop into an LLM context window.
          content:
            application/json:
              schema:
                type: object
                required: [query, locale, mode, total, elapsedMs, results]
                properties:
                  query: { type: string }
                  locale: { type: string }
                  mode: { type: string, enum: [semantic, bm25] }
                  total: { type: integer }
                  elapsedMs: { type: integer }
                  results:
                    type: array
                    items:
                      type: object
                      required: [chunk, snippet, sourceUrl, sourceTitle, pageId, anchor, score]
                      properties:
                        chunk: { type: string, description: "Full chunk text (~300 tokens), ready to drop into an LLM context window." }
                        snippet: { type: string, description: "Shorter highlighted excerpt with HTML <mark> wrapping the matching query terms." }
                        sourceUrl: { type: string }
                        sourceTitle: { type: string }
                        pageId: { type: string }
                        anchor: { type: string, description: "Stable citation-friendly anchor ID, e.g. rag-chunk-pricing-0. Append as #anchor to sourceUrl to deep-link." }
                        score: { type: number }
              examples:
                pricing:
                  value:
                    query: "How much does zipzipdoc cost?"
                    locale: en
                    mode: semantic
                    total: 1
                    elapsedMs: 42
                    results:
                      - chunk: "One flat $29/month plan, unlimited users, unlimited documents..."
                        snippet: "One flat $29/<mark>month</mark> plan, unlimited users…"
                        sourceUrl: /pricing
                        sourceTitle: Pricing | zipzipdoc
                        pageId: pricing
                        anchor: rag-chunk-pricing-0
                        score: 0.84
  /api/datasets:
    get:
      operationId: listDatasets
      summary: List published research datasets.
      description: Returns a JSON list of every research dataset with download URLs and last-updated dates. Rate-limited to 60 req/min/IP.
      responses:
        "200":
          description: Dataset index.
          content:
            application/json: {}
        "429": { description: Rate limit exceeded. }
  /api/datasets/{file}:
    get:
      operationId: getDataset
      summary: Download a single research dataset as JSON or CSV.
      description: |
        Path parameter `file` must be `{slug}.json` or `{slug}.csv`. Datasets
        are open data under CC BY 4.0. Responses include ETag and Last-Modified
        headers; supports If-None-Match and If-Modified-Since for 304 responses.
        Rate-limited to 60 req/min/IP.
      parameters:
        - name: file
          in: path
          required: true
          description: '"{slug}.json" or "{slug}.csv"'
          schema: { type: string, example: esignature-speed-report-2026.json }
      responses:
        "200":
          description: Dataset payload.
          content:
            application/json: {}
            text/csv: {}
        "304": { description: Not modified. }
        "404": { description: Dataset not found. }
        "429": { description: Rate limit exceeded. }
  /mcp:
    get:
      operationId: getMcpDiscovery
      summary: MCP server discovery document.
      description: Returns a JSON document describing the Model Context Protocol HTTP endpoint, supported protocol version, and example client configuration.
      responses:
        "200":
          description: Discovery document.
          content:
            application/json: {}
    post:
      operationId: mcpJsonRpc
      summary: Model Context Protocol JSON-RPC 2.0 endpoint (Streamable HTTP transport, spec 2025-06-18).
      description: |
        Accepts JSON-RPC 2.0 requests/notifications. Implements MCP methods
        `initialize`, `ping`, `tools/list`, `tools/call`,
        `resources/list`, `resources/read`, `resources/templates/list`.
        Tools exposed: search_zipzipdoc, query_zipzipdoc, get_page,
        compare_competitors, list_pricing_tiers.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200": { description: JSON-RPC response. }
        "202": { description: Notification accepted. }
  /llms.txt:
    get:
      operationId: getLlmsTxt
      summary: Short llms.txt orientation file.
      responses:
        "200":
          description: Plain-text orientation file.
          content:
            text/plain: {}
  /llms-full.txt:
    get:
      operationId: getLlmsFullTxt
      summary: Full English content corpus as plain text.
      responses:
        "200":
          description: Plain-text concatenated content corpus.
          content:
            text/plain: {}
components:
  schemas:
    ContentBundle:
      type: object
      required: [metadata, page]
      properties:
        metadata:
          type: object
          required: [pageId, path, locale, title, description, lastModified]
          properties:
            pageId: { type: string }
            path: { type: string }
            locale: { type: string }
            title: { type: string }
            description: { type: string }
            bluf: { type: string, nullable: true }
            lastModified: { type: string, format: date }
            breadcrumbs:
              type: array
              items:
                type: object
                properties:
                  label: { type: string }
                  path: { type: string }
        page:
          type: object
          additionalProperties: true
        primary:
          type: object
          nullable: true
          additionalProperties: true
        related:
          type: array
          items:
            type: object
            additionalProperties: true
        faqs:
          type: object
          nullable: true
          properties:
            id: { type: string }
            items:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  question: { type: string }
                  answer: { type: string }
        data:
          type: object
          additionalProperties: true
    SearchResponse:
      type: object
      required: [query, results]
      properties:
        query: { type: string }
        locale: { type: string }
        total: { type: integer }
        results:
          type: array
          items:
            type: object
            required: [path, title, snippet, score]
            properties:
              path: { type: string }
              title: { type: string }
              snippet: { type: string }
              score: { type: number }
              pageId: { type: string }
