openapi: 3.1.0
info:
  title: TriviAPI
  version: 1.1.0
  description: |
    Curated trivia API for apps, pub quizzes, websites, and AI agents.

    - **Quiz mode** — questions with shuffled answers; check via `POST /api/answer`
    - **Trivia mode** — standalone facts with `source` and `license`
    - **Fact of the day** — deterministic daily fact by UTC date

    **API keys (optional)** — send `X-Api-Key: tvk_…` or `Authorization: Bearer tvk_…` for higher rate limits.
    Without a key, anonymous per-IP limits apply. Contact the operator for a free key.

    | Tier | Read (req/min) | Answer (req/min) |
    |------|----------------|------------------|
    | Anonymous | 60 | 30 |
    | Free key | 300 | 100 |

    Discovery: [llms.txt](/llms.txt) · [API catalog](/.well-known/api-catalog.json)
  license:
    name: CC BY 4.0
    url: https://creativecommons.org/licenses/by/4.0/
  contact:
    url: https://triviapi.com
servers:
  - url: https://triviapi.com
    description: Production
tags:
  - name: Questions
  - name: Facts
  - name: Discovery
  - name: Answers
paths:
  /api/health:
    get:
      tags: [Discovery]
      summary: Health check
      operationId: getHealth
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  db: { type: string }
                  latency_ms: { type: integer }
                  questions:
                    type: object
                    properties:
                      approved: { type: integer }
                      pending: { type: integer }
  /api/fact-of-the-day:
    get:
      tags: [Facts]
      summary: Fact of the day
      description: Deterministic fact for a UTC date. Same date + filters always return the same fact.
      operationId: getFactOfTheDay
      parameters:
        - name: date
          in: query
          schema: { type: string, format: date, example: "2026-06-29" }
          description: YYYY-MM-DD (default today UTC)
        - name: language
          in: query
          schema: { type: string, default: en }
        - name: category
          in: query
          schema: { type: string }
          description: Category slug
        - name: tag
          in: query
          schema: { type: string }
          description: Tag slug
      responses:
        "200":
          description: Fact
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FactOfDay" }
        "404":
          description: No matching fact
  /api/questions:
    get:
      tags: [Questions]
      summary: Random questions or trivia facts
      operationId: getQuestions
      security:
        - {}
        - ApiKey: []
      parameters:
        - name: amount
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
        - name: mode
          in: query
          schema: { type: string, enum: [quiz, trivia], default: quiz }
        - name: category
          in: query
          schema: { type: string }
          description: Category slug or numeric id
        - name: difficulty
          in: query
          schema: { type: string, enum: [easy, medium, hard] }
        - name: type
          in: query
          schema: { type: string, enum: [multiple, boolean] }
        - name: tags
          in: query
          schema: { type: string }
          description: Comma-separated tag slugs or aliases
        - name: match
          in: query
          schema: { type: string, enum: [all, any], default: all }
        - name: language
          in: query
          schema: { type: string, default: en }
      responses:
        "200":
          description: Question set
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QuestionsResponse" }
        "400":
          description: Invalid parameter
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/answer:
    post:
      tags: [Answers]
      summary: Check an answer
      operationId: postAnswer
      security:
        - {}
        - ApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [questionId, answer]
              properties:
                questionId: { type: integer }
                answer: { type: string }
      responses:
        "200":
          description: Result
          content:
            application/json:
              schema:
                type: object
                properties:
                  correct: { type: boolean }
                  correct_answer: { type: string }
        "404":
          description: Question not found
        "401":
          $ref: "#/components/responses/InvalidApiKey"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/categories:
    get:
      tags: [Discovery]
      summary: List categories
      operationId: getCategories
      parameters:
        - name: language
          in: query
          schema: { type: string, default: en }
      responses:
        "200":
          description: Categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items: { $ref: "#/components/schemas/Category" }
  /api/tags:
    get:
      tags: [Discovery]
      summary: Discover tags
      operationId: getTags
      parameters:
        - name: search
          in: query
          schema: { type: string }
        - name: kind
          in: query
          schema: { type: string }
        - name: language
          in: query
          schema: { type: string, default: en }
        - name: limit
          in: query
          schema: { type: integer, maximum: 500, default: 100 }
      responses:
        "200":
          description: Tags
          content:
            application/json:
              schema:
                type: object
                properties:
                  tags:
                    type: array
                    items: { $ref: "#/components/schemas/Tag" }
  /api/languages:
    get:
      tags: [Discovery]
      summary: Available languages
      operationId: getLanguages
      responses:
        "200":
          description: Languages
          content:
            application/json:
              schema:
                type: object
                properties:
                  languages:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string }
                        questionCount: { type: integer }
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Api-Key
      description: Optional consumer API key (`tvk_…`). Also accepted as `Authorization: Bearer tvk_…`.
  responses:
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
    InvalidApiKey:
      description: Invalid or revoked API key
  schemas:
    FactOfDay:
      type: object
      properties:
        date: { type: string, format: date }
        id: { type: integer }
        fact: { type: string }
        category: { type: string }
        category_slug: { type: string }
        difficulty: { type: string }
        language: { type: string }
        source: { type: string, nullable: true }
        license: { type: string, example: "CC BY 4.0" }
    QuestionsResponse:
      type: object
      properties:
        response_code: { type: integer, description: "0 = success, 1 = no results" }
        mode: { type: string }
        language: { type: string }
        matched_tags: { type: array, items: { type: string } }
        unresolved_tags: { type: array, items: { type: string } }
        results:
          type: array
          items:
            oneOf:
              - { $ref: "#/components/schemas/QuizQuestion" }
              - { $ref: "#/components/schemas/TriviaFact" }
    QuizQuestion:
      type: object
      properties:
        id: { type: integer }
        category: { type: string }
        category_slug: { type: string }
        type: { type: string, enum: [multiple, boolean] }
        difficulty: { type: string }
        language: { type: string }
        question: { type: string }
        all_answers: { type: array, items: { type: string } }
        source: { type: string, nullable: true }
        license: { type: string }
    TriviaFact:
      type: object
      properties:
        id: { type: integer }
        category: { type: string }
        category_slug: { type: string }
        difficulty: { type: string }
        language: { type: string }
        fact: { type: string }
        source: { type: string, nullable: true }
        license: { type: string }
    Category:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        slug: { type: string }
        emoji: { type: string, nullable: true }
        questionCount: { type: integer }
    Tag:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        slug: { type: string }
        kind: { type: string, nullable: true }
        questionCount: { type: integer }