openapi: 3.1.0
info:
  title: Bollard API
  version: 0.1.0
  description: |
    Official customs exchange rates — the rates governments legally require for customs
    filings — scraped from official portals, normalized, and served with a paper trail.
    Every response carries its own evidence: authority, effective window, publication date,
    source URL, and a freshness flag.

    **Freshness semantics**: `current` means the data is within the authority's publication
    cadence plus a grace allowance. `stale` means a new publication is overdue — the last
    official rate is still served (it may well still be correct), but your compliance policy
    decides. Bollard never silently serves aged data.
servers:
  - url: https://bollard-api-dev.workers.dev
    description: dev (URL placeholder until deploy)
security:
  - bearerKey: []
paths:
  /v1/rates:
    get:
      summary: The official rate for one country/currency on a date
      parameters:
        - { name: country, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{2}$" }, example: GB, description: "ISO 3166-1 alpha-2 customs jurisdiction (GB, EU)" }
        - { name: currency, in: query, required: true, schema: { type: string, pattern: "^[A-Za-z]{3}$" }, example: EUR, description: ISO 4217 currency code }
        - { name: date, in: query, required: false, schema: { type: string, format: date }, description: "Defaults to today (UTC). The effective window containing this date is served — windows are inclusive on both ends, as the authorities publish them." }
      responses:
        "200":
          description: The applicable official rate with full provenance
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/rates/latest:
    get:
      summary: All currently applicable rates for a country
      description: The rate a filer must use today per currency — never a future-dated early publication.
      parameters:
        - { name: country, in: query, required: true, schema: { type: string }, example: GB }
        - { name: currency, in: query, required: false, schema: { type: string }, description: Optional single-currency filter }
      responses:
        "200":
          description: Applicable rates as of today
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LatestRates" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/countries:
    get:
      summary: Coverage — jurisdictions, authorities, currency counts, freshness
      responses:
        "200":
          description: Covered jurisdictions
          content:
            application/json:
              schema:
                type: object
                properties:
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        country: { type: string }
                        authority: { type: string }
                        portal_url: { type: string }
                        cadence_hours: { type: integer }
                        currencies: { type: integer }
                        freshness: { $ref: "#/components/schemas/Freshness" }
  /v1/freshness:
    get:
      summary: Per-country data age vs the authority's publication schedule
      responses:
        "200":
          description: The freshness ledger
          content:
            application/json:
              schema:
                type: object
                properties:
                  checked_at: { type: string, format: date-time }
                  freshness:
                    type: array
                    items:
                      type: object
                      properties:
                        country: { type: string }
                        authority: { type: string }
                        status: { $ref: "#/components/schemas/Freshness" }
                        age_fraction: { type: number, description: "data age / (cadence + grace); >= 1.0 flips status to stale" }
                        latest_published_at: { type: [string, "null"], format: date }
                        latest_effective_from: { type: [string, "null"], format: date }
                        cadence_hours: { type: integer }
                        grace_hours: { type: integer }
components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: "API key: Authorization: Bearer blrd_live_…"
  schemas:
    Freshness:
      type: string
      enum: [current, stale, unknown]
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: rate_not_found }
            message: { type: string }
    Rate:
      type: object
      properties:
        country: { type: string, example: GB }
        currency: { type: string, example: EUR }
        rate: { type: string, example: "1.1564", description: Decimal as a string — never binary floating point }
        authority: { type: string, example: UK HMRC }
        effective_from: { type: string, format: date, example: "2026-07-01" }
        effective_to: { type: string, format: date, example: "2026-07-31" }
        published_at: { type: string, format: date, example: "2026-06-18" }
        source_url: { type: string, example: "https://www.trade-tariff.service.gov.uk/exchange_rates/view/2026-7" }
        freshness: { $ref: "#/components/schemas/Freshness" }
    LatestRates:
      type: object
      properties:
        country: { type: string }
        authority: { type: string }
        as_of: { type: string, format: date }
        freshness: { $ref: "#/components/schemas/Freshness" }
        rates:
          type: array
          items:
            type: object
            properties:
              currency: { type: string }
              rate: { type: string }
              effective_from: { type: string, format: date }
              effective_to: { type: string, format: date }
              published_at: { type: string, format: date }
              source_url: { type: string }
  responses:
    BadRequest:
      description: Invalid parameters
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, invalid, or revoked API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No official rate covers the request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
