openapi: 3.1.0
info:
  title: US Wine Reference API
  version: "1.0.0"
  summary: US wine label reference data from public-domain TTB and eCFR sources.
  description: |
    Developer-first API over **US wine label reference data**: TTB basic permit holders,
    American Viticultural Areas, TTB-approved grape variety names, and the TTB class/type and
    origin code tables.

    **Free.** Every read path is anonymous and needs no key. A free key only raises the
    per-IP budget; it does not unlock data.

    **Licensing.** Every dataset served is CC0 or a US federal government work. There is no
    share-alike obligation, no non-commercial restriction, and no attribution requirement —
    you may redistribute this data, including commercially.

    **Scope.** This service covers the reference data a wine label *points at*. It does **not**
    serve TTB COLA label records themselves — the Public COLA Registry publishes no bulk feed and
    no API, and this service does not scrape it. Label routes answer `501` saying so, rather
    than returning an empty list that would read as "nothing matched".

    **Two things this API will never claim.** It does not return alcohol by volume as a
    measured value — a COLA record carries no ABV field, and ABV appears only as printed text
    on the label image; what is returned is a *derived regulatory band* from the class/type
    code under 27 CFR 4.21, labelled as derived. And it does not publish applicant contact
    names, phone numbers or street addresses: many TTB permit premises are private homes.
  contact:
    name: Allan Niñal
    url: https://www.allanninal.dev/
  license:
    name: MIT (code). Data is CC0 / US public domain.
    url: https://apis.allanninal.dev/wine/license.html

servers:
  - url: https://apis.allanninal.dev/wine
    description: Production

tags:
  - name: Producers
    description: TTB basic permit holders — wine producers/blenders and alcohol importers.
  - name: Appellations
    description: American Viticultural Areas, with nesting and CFR citations.
  - name: Varieties
    description: Grape variety names approved for type designation on American wine.
  - name: Codes
    description: TTB class/type and origin code tables.
  - name: Labels
    description: COLA label records — out of scope for this service. Documented so the answer is explicit.
  - name: Service
    description: Plans, versioning, usage and health.

paths:
  /v1/producers:
    get:
      tags: [Producers]
      summary: Search permit holders
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "Free text over owner and operating name. Every token must match." }
        - { name: state, in: query, schema: { type: string }, example: CA, description: "Two-letter state code. Repeatable or comma-separated." }
        - { name: kind, in: query, schema: { type: string, enum: [producer, importer] } }
        - { name: county, in: query, schema: { type: string } }
        - { name: permitId, in: query, schema: { type: string }, example: CA-I-4310 }
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/cursor' }
      responses:
        "200": { $ref: '#/components/responses/ProducerList' }
        "400": { $ref: '#/components/responses/InvalidFilter' }
        "429": { $ref: '#/components/responses/RateLimited' }

  /v1/producers/{permitId}:
    get:
      tags: [Producers]
      summary: One permit holder
      parameters:
        - { name: permitId, in: path, required: true, schema: { type: string }, example: CA-I-4310 }
      responses:
        "200":
          description: The permit holder.
          content: { application/json: { schema: { $ref: './api/v1/schemas/producer.schema.json' } } }
        "404":
          description: |
            Not found. A bonded-winery registry number (`BWN-*`) returns this with a `reason`
            explaining that IRC 6103 bars TTB from publishing bonded-cellar lists, so no public
            dataset resolves them.

  /v1/appellations:
    get:
      tags: [Appellations]
      summary: Search American Viticultural Areas
      parameters:
        - { name: q, in: query, schema: { type: string }, example: Napa Valley }
        - { name: state, in: query, schema: { type: string }, example: OR }
        - { name: status, in: query, schema: { type: string, enum: [established, pending, proposed, historic] } }
        - { name: within, in: query, schema: { type: string }, description: "AVAs nested inside this one.", example: Napa Valley }
        - name: includeRevisions
          in: query
          schema: { type: boolean, default: false }
          description: |
            TTB names a pending boundary revision after its parent with a trailing ordinal
            ("Atlas Peak 2"). Those are rulemaking artefacts, not appellations a label can
            claim, so they are hidden unless this is true.
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/cursor' }
      responses:
        "200": { $ref: '#/components/responses/AppellationList' }
        "400": { $ref: '#/components/responses/InvalidFilter' }

  /v1/appellations/{avaId}:
    get:
      tags: [Appellations]
      summary: One appellation
      parameters: [{ name: avaId, in: path, required: true, schema: { type: string }, example: napa-valley }]
      responses:
        "200":
          description: The appellation.
          content: { application/json: { schema: { $ref: './api/v1/schemas/appellation.schema.json' } } }
        "404": { description: Not found. }

  /v1/varieties:
    get:
      tags: [Varieties]
      summary: Search grape variety names
      description: |
        An exact hit on a name, a synonym, or a 27 CFR 4.92 alternative name resolves to the
        canonical variety. Anything else is a substring search.
      parameters:
        - { name: q, in: query, schema: { type: string }, example: napa gamay }
        - { $ref: '#/components/parameters/limit' }
        - { $ref: '#/components/parameters/cursor' }
      responses:
        "200": { $ref: '#/components/responses/VarietyList' }

  /v1/varieties/{varietyId}:
    get:
      tags: [Varieties]
      summary: One grape variety
      parameters: [{ name: varietyId, in: path, required: true, schema: { type: string }, example: merlot }]
      responses:
        "200":
          description: The variety.
          content: { application/json: { schema: { $ref: './api/v1/schemas/grape-variety.schema.json' } } }
        "404": { description: Not found. }

  /v1/class-types/{code}:
    get:
      tags: [Codes]
      summary: Decode a TTB class/type code
      description: |
        **Codes are case-sensitive.** TTB distinguishes `412I` from `412i` by letter case
        alone. A case-insensitive request that matches more than one code returns `409` listing
        the candidates rather than resolving to an arbitrary one.
      parameters: [{ name: code, in: path, required: true, schema: { type: string }, example: "80" }]
      responses:
        "200":
          description: The code, with a derived regulatory alcohol band for wine classes.
          content: { application/json: { schema: { $ref: './api/v1/schemas/class-type.schema.json' } } }
        "409": { description: Ambiguous — the code differs from another only by letter case. }
        "404": { description: Not found. }

  /v1/origin-codes/{code}:
    get:
      tags: [Codes]
      summary: Decode a TTB origin code
      parameters: [{ name: code, in: path, required: true, schema: { type: string }, example: "01" }]
      responses:
        "200":
          description: The code. An origin code is not an AVA.
          content: { application/json: { schema: { $ref: './api/v1/schemas/origin-code.schema.json' } } }
        "404": { description: Not found. }

  /v1/labels:
    get:
      tags: [Labels]
      summary: Search COLA label records (out of scope — returns 501)
      responses:
        "501": { $ref: '#/components/responses/CorpusPending' }

  /v1/labels/{ttbId}:
    get:
      tags: [Labels]
      summary: One COLA record (out of scope — returns 501)
      parameters:
        - { name: ttbId, in: path, required: true, schema: { type: string, pattern: '^\d{14}$' }, example: "26244001000234" }
      responses:
        "400": { description: "Malformed TTB ID. A TTB ID is 14 digits: YYDDD + NNN received code + NNNNNN sequence." }
        "501": { $ref: '#/components/responses/CorpusPending' }

  /v1/plans:
    get:
      tags: [Service]
      summary: Plan catalogue
      description: |
        Machine-readable plans, limits and features, plus which plan answered this request.
        Paid plans are declared but not open; no payment code runs in this service.
      responses: { "200": { description: The catalogue. } }

  /v1/version:
    get:
      tags: [Service]
      summary: Contract and dataset versions
      description: |
        The API contract version, the versioning policy, and the version of every dataset
        currently served. See VERSIONING.md for the decision procedure behind it.
      responses: { "200": { description: Version manifest. } }

  /v1/usage:
    get:
      tags: [Service]
      summary: Your budget and how much of it you have used
      responses: { "200": { description: Usage and limits for the calling identity. } }

  /healthz:
    get:
      tags: [Service]
      summary: Health, dataset versions and coverage
      description: Also states plainly what this API does NOT cover in the current phase.
      responses: { "200": { description: Service health. } }

components:
  parameters:
    limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
    cursor:
      name: cursor
      in: query
      schema: { type: string }
      description: Opaque cursor from a previous response's `nextCursor`.

  schemas:
    Plan:
      type: object
      description: The plan that answered this request, and the next rung up.
      properties:
        id: { type: string, example: free }
        name: { type: string }
        limits: { type: object }
        nextPlan:
          type: object
          properties:
            id: { type: string }
            name: { type: string }
            available: { type: boolean }
            addsFeatures: { type: array, items: { type: string } }
            addsBudget: { type: [object, "null"] }
            signupRequired: { type: boolean }
            reason: { type: [string, "null"] }
            url: { type: [string, "null"] }
    Provenance:
      type: object
      description: Attached to every response, so provenance travels with the data.
      properties:
        datasetVersions: { type: object }
        searchVersion: { type: string }
        licence: { type: string }
        attribution: { type: array, items: { type: string } }
        disclaimer: { type: string }
    Problem:
      type: object
      description: RFC 9457 problem details.
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }

  responses:
    ProducerList:
      description: Matching permit holders.
      content:
        application/json:
          schema:
            allOf:
              - type: object
                properties:
                  total: { type: integer }
                  items: { type: array, items: { $ref: './api/v1/schemas/producer.schema.json' } }
                  nextCursor: { type: [string, "null"] }
                  plan: { $ref: '#/components/schemas/Plan' }
              - $ref: '#/components/schemas/Provenance'
    AppellationList:
      description: Matching appellations.
      content:
        application/json:
          schema:
            allOf:
              - type: object
                properties:
                  total: { type: integer }
                  items: { type: array, items: { $ref: './api/v1/schemas/appellation.schema.json' } }
                  nextCursor: { type: [string, "null"] }
                  revisionsExcluded: { type: boolean }
                  plan: { $ref: '#/components/schemas/Plan' }
              - $ref: '#/components/schemas/Provenance'
    VarietyList:
      description: Matching grape varieties.
      content:
        application/json:
          schema:
            allOf:
              - type: object
                properties:
                  total: { type: integer }
                  items: { type: array, items: { $ref: './api/v1/schemas/grape-variety.schema.json' } }
                  nextCursor: { type: [string, "null"] }
                  plan: { $ref: '#/components/schemas/Plan' }
              - $ref: '#/components/schemas/Provenance'
    InvalidFilter:
      description: The filter was rejected, with reasons. A bad filter is never a silent full scan.
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string }
              details: { type: array, items: { type: string } }
    CorpusPending:
      description: |
        Label records are out of scope for this service. Returned instead of an empty list,
        because an empty list would read as "nothing matched", which is false.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    RateLimited:
      description: |
        Rate limit exceeded. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` are
        published on every response (and mirrored as `X-RateLimit-*`); `Retry-After` on a 429.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { type: object }

security: []
