openapi: 3.1.0
info:
  title: BOC Currency Rate API
  description: |
    Read-only HTTP API over the Bank of China exchange-rate archive.

    Data is published by BOC every ~5 minutes during market hours; this
    API mirrors the [`boc_currency_price_tracker_web`](https://github.com/zning1994/boc_currency_price_tracker_web)
    archive (updated by cron every 30 minutes) and adds caching, CORS, and
    timezone conversion.

    All responses are JSON, all set `Access-Control-Allow-Origin: *`, all
    carry an `ETag` and respect `If-None-Match` (returns `304`).

    Prices are quoted **per 100 units of the foreign currency** in CNY
    (BOC's convention). For example, a USD `spot_buy` of `682.06` means
    100 USD = 682.06 CNY at the spot exchange buying rate.
  version: '1.0.0'
  contact:
    name: GitHub
    url: https://github.com/zning1994/boc_currency_price_tracker_api
  license:
    name: MIT
servers:
  - url: https://api-bocurrencyprice.techina.science
    description: Production
tags:
  - name: rates
    description: Exchange rate queries.
  - name: meta
    description: Reference data.
paths:
  /v1/currencies:
    get:
      tags: [meta]
      summary: List supported currencies
      description: Returns every ISO 4217 code we publish, paired with its Chinese name as it appears on BOC's site.
      operationId: listCurrencies
      responses:
        '200':
          description: OK
          headers:
            ETag:
              schema: { type: string }
            Cache-Control:
              schema: { type: string }
              example: 'public, max-age=86400'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrenciesResponse'
              example:
                data:
                  - { code: AED, name_zh: 阿联酋迪拉姆 }
                  - { code: USD, name_zh: 美元 }
                meta: { count: 40 }
        '304':
          description: ETag match.
  /v1/latest:
    get:
      tags: [rates]
      summary: Latest rate for every currency
      description: |
        Returns the most-recent published row for each of the 40 currencies.
        Walks back up to 7 days per currency to handle weekend / holiday
        gaps. Currencies with no data in the fallback window are omitted
        from `data` rather than returned with nulls.
      operationId: getLatestAll
      parameters:
        - $ref: '#/components/parameters/Tz'
      responses:
        '200':
          description: OK
          headers:
            Cache-Control:
              schema: { type: string }
              example: 'public, max-age=60'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LatestAllResponse'
        '304':
          description: ETag match.
        '400':
          $ref: '#/components/responses/BadRequest'
  /v1/latest/{ccy}:
    get:
      tags: [rates]
      summary: Latest rate for one currency
      operationId: getLatestOne
      parameters:
        - $ref: '#/components/parameters/Ccy'
        - $ref: '#/components/parameters/Tz'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LatestOneResponse'
        '304':
          description: ETag match.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: No data within the 8-day fallback window.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
  /v1/historical/{ccy}:
    get:
      tags: [rates]
      summary: Date-range query
      description: |
        Returns every published row between `from` and `to` (inclusive),
        across however many daily snapshots fall in that range. Range
        capped at **365 days**; longer ranges return `400 range_too_large`.
      operationId: getHistorical
      parameters:
        - $ref: '#/components/parameters/Ccy'
        - in: query
          name: from
          required: true
          schema: { type: string, pattern: '^\d{8}$' }
          description: Inclusive start date, YYYYMMDD (Asia/Shanghai).
          example: '20260420'
        - in: query
          name: to
          required: true
          schema: { type: string, pattern: '^\d{8}$' }
          description: Inclusive end date, YYYYMMDD (Asia/Shanghai).
          example: '20260425'
        - $ref: '#/components/parameters/Tz'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoricalResponse'
        '304':
          description: ETag match.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
components:
  parameters:
    Ccy:
      in: path
      name: ccy
      required: true
      schema: { type: string, example: USD }
      description: ISO 4217 code, case-insensitive. See `/v1/currencies` for the full list.
    Tz:
      in: query
      name: tz
      required: false
      schema:
        type: string
        default: UTC
      description: |
        IANA timezone name (e.g. `Asia/Shanghai`, `UTC`). Affects the
        `published_at` field only; `published_at_utc` is always canonical
        UTC. Default is `UTC`. Invalid tz returns `400 invalid_tz`.
      example: Asia/Shanghai
  schemas:
    CurrencyEntry:
      type: object
      required: [code, name_zh]
      properties:
        code: { type: string, example: USD }
        name_zh: { type: string, example: 美元 }
    Rate:
      type: object
      required: [code, name_zh, as_of_date, spot_buy, cash_buy, spot_sell, cash_sell, conversion, published_at_utc, published_at]
      properties:
        code: { type: string, example: USD }
        name_zh: { type: string, example: 美元 }
        as_of_date: { type: string, pattern: '^\d{8}$', example: '20260425' }
        spot_buy:
          type: number
          nullable: true
          description: '现汇买入价 (spot exchange buying price), per 100 units in CNY.'
          example: 682.06
        cash_buy:
          type: number
          nullable: true
          description: '现钞买入价 (cash buying price).'
          example: 682.06
        spot_sell:
          type: number
          nullable: true
          description: '现汇卖出价 (spot exchange selling price).'
          example: 684.93
        cash_sell:
          type: number
          nullable: true
          description: '现钞卖出价 (cash selling price).'
          example: 684.93
        conversion:
          type: number
          nullable: true
          description: '中行折算价 (BOC conversion price).'
          example: 686.74
        published_at_utc:
          type: string
          format: date-time
          description: Canonical UTC ISO 8601.
          example: '2026-04-24T17:48:00Z'
        published_at:
          type: string
          format: date-time
          description: ISO 8601 in the tz requested via `?tz=`.
          example: '2026-04-25T01:48:00+08:00'
    CurrenciesResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/CurrencyEntry' }
        meta:
          type: object
          properties:
            count: { type: integer, example: 40 }
    LatestAllResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Rate' }
        meta:
          type: object
          properties:
            tz: { type: string, example: UTC }
            count: { type: integer, example: 40 }
    LatestOneResponse:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: '#/components/schemas/Rate' }
        meta:
          type: object
          properties:
            tz: { type: string, example: UTC }
    HistoricalResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Rate' }
        meta:
          type: object
          properties:
            code: { type: string, example: USD }
            from: { type: string, example: '20260420' }
            to: { type: string, example: '20260425' }
            tz: { type: string, example: UTC }
            count: { type: integer, example: 104 }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_tz
                - unknown_currency
                - missing_params
                - invalid_date
                - invalid_range
                - range_too_large
                - no_recent_data
                - not_found
                - method_not_allowed
              example: invalid_tz
            message:
              type: string
              example: 'invalid tz: Mars/Phobos'
  responses:
    BadRequest:
      description: Validation failure.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Unknown currency or route.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
