openapi: 3.1.0
info:
  title: DnD Scribe Summary API
  version: 1.0.0
  description: >-
    Read-only API for consuming published DnD Scribe session summaries.
    API keys are scoped to one campaign and currently expose only the summaries:read scope.
servers:
  - url: https://dnd.faysk.dev
    description: Production
security:
  - bearerApiKey: []
tags:
  - name: Health
  - name: Summaries
paths:
  /api/v1/health:
    get:
      tags: [Health]
      summary: Check API availability
      security: []
      responses:
        '200':
          description: API is available.
          content:
            application/json:
              schema:
                type: object
                required: [status, apiVersion, service, documentation]
                properties:
                  status: { type: string, const: ok }
                  apiVersion: { type: string, const: v1 }
                  service: { type: string }
                  documentation: { type: string, format: uri }
  /api/v1/summaries:
    get:
      tags: [Summaries]
      summary: List published session summaries
      description: >-
        Returns summary metadata and the short summary only. Use the detail endpoint to fetch
        summaryMarkdown. Results are ordered by updatedAt descending, then id descending.
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - in: query
          name: cursor
          description: Opaque cursor returned by pagination.nextCursor.
          schema: { type: string }
        - in: query
          name: updatedAfter
          description: Return summaries updated after this ISO 8601 instant.
          schema: { type: string, format: date-time }
        - in: query
          name: from
          description: Minimum session date, inclusive.
          schema: { type: string, format: date }
        - in: query
          name: to
          description: Maximum session date, inclusive.
          schema: { type: string, format: date }
        - in: query
          name: arc
          description: Exact case-insensitive arc name.
          schema: { type: string, maxLength: 160 }
      responses:
        '200':
          description: Paginated summary list.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SummaryListResponse'
        '304': { description: The representation has not changed since If-None-Match. }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/v1/summaries/{id}:
    get:
      tags: [Summaries]
      summary: Get one published session summary
      description: >-
        Returns JSON by default. Send Accept: text/markdown to receive the raw published Markdown.
      parameters:
        - in: path
          name: id
          required: true
          description: Stable DnD Scribe source session ID.
          schema: { type: string, minLength: 1, maxLength: 180 }
      responses:
        '200':
          description: Published summary.
          headers:
            ETag: { $ref: '#/components/headers/ETag' }
            Last-Modified:
              schema: { type: string }
            X-RateLimit-Limit: { $ref: '#/components/headers/RateLimit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/RateRemaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/RateReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SummaryDetailResponse'
            text/markdown:
              schema:
                type: string
        '304': { description: The representation has not changed since If-None-Match. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: dnd_live_...
      description: DnD Scribe API key. Store it server-side and send it in the Authorization header.
  headers:
    ETag:
      schema: { type: string }
      description: Representation validator for conditional requests.
    RateLimit:
      schema: { type: integer, example: 300 }
    RateRemaining:
      schema: { type: integer, example: 297 }
    RateReset:
      schema: { type: integer, example: 1786914000 }
      description: Unix timestamp when the current one-minute window resets.
  responses:
    BadRequest:
      description: Invalid query parameter.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: API key is missing, invalid, expired, or revoked.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Forbidden:
      description: API key does not have the required scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    NotFound:
      description: Published summary was not found in the API key campaign.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    RateLimited:
      description: The per-key request limit was exceeded.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
  schemas:
    SummaryListItem:
      type: object
      required: [id, title, summary, updatedAt, webUrl]
      properties:
        id: { type: string, description: Stable source session ID. }
        title: { type: string }
        sessionDate: { type: [string, 'null'], format: date }
        arc: { type: [string, 'null'] }
        summary: { type: string, description: Short card description. }
        updatedAt: { type: string, format: date-time }
        webUrl: { type: string, format: uri }
    SummaryDetail:
      allOf:
        - $ref: '#/components/schemas/SummaryListItem'
        - type: object
          required: [summaryMarkdown, coverImageUrl, heroImageUrl]
          properties:
            summaryMarkdown: { type: string, description: Full published Markdown summary. }
            coverImageUrl: { type: string }
            heroImageUrl: { type: string }
    SummaryListResponse:
      type: object
      required: [object, apiVersion, data, pagination]
      properties:
        object: { type: string, const: list }
        apiVersion: { type: string, const: v1 }
        data:
          type: array
          items: { $ref: '#/components/schemas/SummaryListItem' }
        pagination:
          type: object
          required: [limit, hasMore, nextCursor]
          properties:
            limit: { type: integer }
            hasMore: { type: boolean }
            nextCursor: { type: [string, 'null'] }
    SummaryDetailResponse:
      type: object
      required: [object, apiVersion, data]
      properties:
        object: { type: string, const: session_summary }
        apiVersion: { type: string, const: v1 }
        data: { $ref: '#/components/schemas/SummaryDetail' }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string, example: invalid_api_key }
            message: { type: string }
