openapi: 3.1.0
info:
  title: Grape Variety & Appellation Reference API
  version: "1.0.0"
  summary: Global grape varieties from Wikidata (CC0) joined to the US 27 CFR part 4 labelling thresholds.
  description: |
    2,782 grape varieties with berry colour, species, country of origin, recorded parentage and
    VIVC cross-references, normalised from Wikidata with **every raw value preserved** — plus the
    US thresholds that govern what a bottle may be called.

    **What this API will not do, stated up front.** It will not give you permitted varieties or
    blending percentages for an AOC or DOCG. That data is not obtainable: no Wikidata property
    expresses composition or percentage, and the EU's eAmbrosia register publishes a single HTML
    distribution under a licence carrying an attribution obligation, over per-appellation PDFs in
    24 languages. The reasoning is served at `/v1/sources`.

    **The upstream is crowd-sourced and contains errors.** Two are published at `/v1/quality` by
    entity id. Field coverage ranges from 78% to 18%. `confidence` measures how well described an
    entity is, never whether it is correct.

    **Where upstream contradicts itself, so does the answer.** 146 varieties carry berry colours
    that map to different categories; those return `colour: "unknown"` with both raw values and an
    explanation rather than a coin flip.

    **An ambiguous name is a 409 with candidates**, never an arbitrary pick.

    Not an official source. Not affiliated with TTB, the Office of the Federal Register, the
    Wikimedia Foundation or VIVC.
  license:
    name: MIT (server) — data is CC0 (Wikidata) and public domain (eCFR)
    url: https://apis.allanninal.dev/grapes/license.html
  contact:
    name: Allan Niñal
    url: https://www.linkedin.com/in/allanninal/
servers:
  - url: https://apis.allanninal.dev/grapes
    description: Production
tags:
  - name: Varieties
    description: Grape varieties and their recorded attributes.
  - name: Claims
    description: Wikidata co-occurrence claims. Never permitted-variety lists.
  - name: Rules
    description: US composition thresholds from 27 CFR part 4.
  - name: Quality
    description: What this service knows to be wrong in its upstream.
  - name: Service
    description: Health, version, plans, usage, provenance and history.

paths:
  /healthz:
    get:
      tags: [Service]
      summary: Liveness, dataset versions and declared coverage
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } }

  /v1/varieties:
    get:
      tags: [Varieties]
      summary: Search grape varieties
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "Free text over the name. EVERY token must match." }
        - { name: colour, in: query, schema: { type: string, enum: [white, red, rose, grey, unknown] }, description: "Normalised category. 'unknown' includes both absent and contradictory upstream values — use colourConflict to tell them apart." }
        - { name: species, in: query, schema: { type: string } }
        - { name: origin, in: query, schema: { type: string } }
        - { name: hasVivc, in: query, schema: { type: boolean } }
        - { name: hasParents, in: query, schema: { type: boolean } }
        - { name: colourConflict, in: query, schema: { type: boolean }, description: "Surface exactly the varieties whose upstream colours disagree." }
        - { name: isHybrid, in: query, schema: { type: boolean }, description: "Hybrid grapes — anything Wikidata classes under 'hybrid grape' (Seibel, Seyve-Villard, Rogers' Hybrids and the rest). 330 of 2,782, and the bulk of what US labelling law calls an American variety." }
        - { name: minConfidence, in: query, schema: { type: number, minimum: 0, maximum: 1 } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/VarietyList" }
        "400": { $ref: "#/components/responses/BadFilter" }

  /v1/varieties/{id}/parents:
    get:
      tags: [Varieties]
      summary: Recorded parentage
      description: Registered BEFORE `/v1/varieties/{id}` so "parents" is not read as an identifier. Only ~18% of varieties have any recorded parentage; an unresolvable parent is returned unresolved rather than dropped.
      parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object } } } }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Ambiguous" }

  /v1/varieties/{id}:
    get:
      tags: [Varieties]
      summary: One variety, by slug, name or Wikidata entity id
      parameters: [{ name: id, in: path, required: true, schema: { type: string }, example: "Q213332" }]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Variety" } } } }
        "400": { $ref: "#/components/responses/Problem" }
        "404": { $ref: "#/components/responses/Problem" }
        "409": { $ref: "#/components/responses/Ambiguous" }

  /v1/relationships:
    get:
      tags: [Claims]
      summary: Co-occurrence claims — NOT permitted-variety lists
      description: |
        Wikidata P186 links. Each records that an editor associated a grape with a wine entity. They
        carry no percentages, 98.5% of them are Italian, and demonstrable errors exist — see
        `/v1/quality`. Every item carries a `caution` field stating this.
      parameters:
        - { name: grape, in: query, schema: { type: string }, description: "Wikidata entity id of the grape." }
        - { name: wine, in: query, schema: { type: string } }
        - { name: country, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object } } } }
        "400": { $ref: "#/components/responses/BadFilter" }

  /v1/composition-rules:
    get:
      tags: [Rules]
      summary: US labelling thresholds from 27 CFR part 4
      description: The 75% varietal rule (4.23), the 85% appellation rule (4.25) and the vintage rules (4.27), quoted verbatim with citations. United States only — these are not EU blending rules.
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } }

  /v1/quality:
    get:
      tags: [Quality]
      summary: What this service knows to be wrong upstream
      description: A floor on the error rate, never a ceiling. Names offending entities by id, lists colour conflicts, and publishes the confidence formula.
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } }

  /v1/sources:
    get:
      tags: [Service]
      summary: Every upstream — and everything evaluated and rejected, with reasons
      responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } }

  /v1/changes:
    get:
      tags: [Service]
      summary: What changed since a date
      parameters:
        - { name: dataset, in: query, schema: { type: string, enum: [varieties, relationships, rules], default: varieties } }
        - { name: since, in: query, required: true, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object } } } }
        "400": { $ref: "#/components/responses/Problem" }
        "402": { description: Beyond this plan's history window., content: { application/problem+json: { schema: { type: object } } } }
        "422": { description: Before this service began retaining history. Wikidata publishes no dated snapshot, so no earlier state exists anywhere., content: { application/problem+json: { schema: { type: object } } } }

  /v1/history/coverage:
    get:
      tags: [Service]
      summary: How far back the archive actually reaches
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object } } } }
        "503": { $ref: "#/components/responses/Problem" }

  /v1/plans:
    get: { tags: [Service], summary: The plan catalogue, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }
  /v1/version:
    get: { tags: [Service], summary: Contract and dataset versions, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }
  /v1/usage:
    get: { tags: [Service], summary: Your budget and what you have used today, responses: { "200": { description: OK, content: { application/json: { schema: { type: object } } } } } }

components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: Optional. A key raises your budget; it never unlocks data.
  responses:
    Problem:
      description: RFC 9457 problem document.
      content: { application/problem+json: { schema: { type: object } } }
    Ambiguous:
      description: |
        The name matches more than one variety. The API refuses rather than choosing, and returns
        every candidate with its colour, origins and VIVC id so a caller can disambiguate.
      content: { application/problem+json: { schema: { type: object } } }
    BadFilter:
      description: A filter was invalid. The reason is returned; the query is never silently run unfiltered.
      content: { application/json: { schema: { type: object, properties: { error: { type: string }, details: { type: array, items: { type: string } } } } } }
    VarietyList:
      description: A page of varieties, with provenance, plan and the colour vocabulary.
      content:
        application/json:
          schema:
            type: object
            properties:
              total: { type: integer }
              items: { type: array, items: { $ref: "#/components/schemas/Variety" } }
              nextCursor: { type: [string, "null"] }
              colourVocabulary: { type: array, items: { type: string } }
              datasetVersions: { type: object }
              licence: { type: string }
              attribution: { type: array, items: { type: string } }
  schemas:
    Variety:
      $ref: https://apis.allanninal.dev/grapes/api/v1/schemas/variety.schema.json
    Claim:
      $ref: https://apis.allanninal.dev/grapes/api/v1/schemas/claim.schema.json
    Rule:
      $ref: https://apis.allanninal.dev/grapes/api/v1/schemas/rule.schema.json
security:
  - {}
  - bearerKey: []
