openapi: 3.1.0
info:
  title: GraySky Free Weather API
  version: "1.0.0"
  summary: The free, keyless JSON weather API on graysky.net.
  description: |
    One endpoint. No key, no sign-up, no account. Send a coordinate and the
    service sends a JSON weather forecast. Use it for non-commercial work
    only, and give credit to GraySky with a link to graysky.net.

    All timestamps are RFC 3339 with an explicit offset. All units are
    suffixed in the field names (`temperatureC`, `precipMm`, `windSpeedMs`).
    The wire is always SI; `units=imperial` adds companion fields and
    removes nothing.

    The service moves each position to a 0.05 degree cell (about 5.5 km
    across) and serves every caller in that cell from one cache entry. The
    human-readable reference is https://graysky.net/dev.

    Versioning: `/free/v1/` is additive-only. A breaking change ships under
    a new prefix, never as a silent edit to an existing field.
  contact:
    name: GraySky
    url: https://graysky.net/dev
    email: api@graycloud.app
  license:
    name: Proprietary
    identifier: LicenseRef-GrayCloud-Proprietary

servers:
  - url: https://graysky.net
    description: Production

tags:
  - name: forecast
    description: Current, minutely, hourly, and daily blocks.

paths:
  /free/v1/forecast/{location}:
    get:
      tags: [forecast]
      operationId: getFreeForecast
      summary: Forecast at a coordinate. No auth.
      description: |
        Returns a forecast for the given latitude/longitude. `dataSets=`
        opts into blocks; request only what you render.

        The rules: non-commercial use only, credit GraySky, poll each
        coordinate at most once every 10 minutes, and send fewer than 10
        requests each minute. The service has a shared hourly budget; when
        the budget is empty the service answers 503 with `Retry-After`.
        Full rules: https://graysky.net/dev#rules
      parameters:
        - $ref: "#/components/parameters/Location"
        - $ref: "#/components/parameters/DataSets"
        - $ref: "#/components/parameters/Units"
        - $ref: "#/components/parameters/Lang"
      responses:
        "200":
          description: Forecast for the snapped cell.
          headers:
            X-Gc-Surface:
              description: Always `free-v1` on this endpoint.
              schema: { type: string }
            X-Gc-Grid:
              description: The 0.05 degree cell that answered.
              schema: { type: string }
            X-Gc-Budget:
              description: How much of this hour's shared budget is used, in steps of 10 percent.
              schema: { type: string }
            X-Attribution:
              description: The credit line. Show it or link to graysky.net.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ForecastResponse" }
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "502":
          $ref: "#/components/responses/UpstreamError"
        "503":
          $ref: "#/components/responses/BudgetExhausted"
        "504":
          $ref: "#/components/responses/UpstreamTimeout"

components:
  parameters:
    Location:
      name: location
      in: path
      required: true
      description: |
        Comma-separated `lat,lon` in decimal degrees. The service snaps the
        pair to a 0.05 degree cell before lookup; the `X-Gc-Grid` header
        shows the cell that answered.
      schema:
        type: string
        pattern: '^-?\d{1,3}(\.\d+)?,-?\d{1,3}(\.\d+)?$'
        example: "42.35,-71.05"
    DataSets:
      name: dataSets
      in: query
      required: false
      description: |
        Comma-separated set of blocks to include. Default: `current,daily`.
        The members are `current`, `minutely`, `hourly`, and `daily`. An
        unknown member gets HTTP 400 with the problem type
        `https://graysky.net/dev#bad-parameter`. `minutely` has radar
        coverage over the United States and Europe; outside that area its
        `data[]` is empty and `coverage` says so.
      schema:
        type: string
        default: "current,daily"
        example: "current,hourly,daily"
    Units:
      name: units
      in: query
      required: false
      description: |
        Display preference. SI fields (`temperatureC`, `windSpeedMs`,
        `precipMm`, ...) are ALWAYS on the wire. When `units=imperial`,
        each block entry also carries imperial companion fields
        (`temperatureF`, `windSpeedMph`, `precipIn`, `precipRateInHr`,
        `visibilityMi`, `pressureInHg`, `windGustMph`, plus the `*F`
        variants for `feelsLike`, `dewPoint`, and daily min/max). A
        companion appears only when the SI source field is present;
        a `null` SI value makes a `null` imperial value.
        `meta.units` echoes the chosen mode.
      schema:
        type: string
        enum: [si, imperial]
        default: si
    Lang:
      name: lang
      in: query
      required: false
      description: |
        The API answers in English only. A value other than `en` gets
        HTTP 400 with the problem type
        `https://graysky.net/dev#unsupported-language`.
      schema:
        type: string
        enum: [en]
        default: en

  responses:
    BadRequest:
      description: |
        Malformed request. The problem `type` URI ends with
        `#bad-coordinate`, `#bad-parameter`, `#paid-only` (the request
        asked for a block the free API does not serve), or
        `#unsupported-language`. See https://graysky.net/dev#errors
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    NotFound:
      description: |
        GraySky has no data for this point now (`#no-coverage`). The
        answer holds for 6 hours; do not retry the point sooner.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    BudgetExhausted:
      description: |
        The shared budget for this hour is empty (`#budget-exhausted`), or
        the free service is off (`#service-disabled`). Wait for the
        `Retry-After` seconds.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    UpstreamError:
      description: The data source sent an error (`#upstream-error`). Retry with backoff.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    UpstreamTimeout:
      description: The data source did not answer in time (`#upstream-timeout`). Retry with backoff.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }

  schemas:
    ForecastResponse:
      type: object
      required: [location, meta]
      properties:
        location:   { $ref: "#/components/schemas/Location" }
        meta:       { $ref: "#/components/schemas/Meta" }
        current:    { $ref: "#/components/schemas/CurrentBlock" }
        hourly:     { $ref: "#/components/schemas/HourlyBlock" }
        daily:      { $ref: "#/components/schemas/DailyBlock" }
        minutely:   { $ref: "#/components/schemas/MinutelyBlock" }

    Location:
      type: object
      required: [latitude, longitude, timezone, utcOffsetSeconds]
      properties:
        latitude:         { type: number, format: double, example: 40.7128 }
        longitude:        { type: number, format: double, example: -74.0060 }
        timezone:         { type: string, example: "America/New_York" }
        utcOffsetSeconds: { type: integer, example: -14400 }

    Meta:
      type: object
      required: [units, generatedAt, expiresAt, modelSources]
      properties:
        units:
          type: string
          enum: [si, imperial]
          description: |
            Echoes the request's `units` query param. The wire format is
            always SI regardless of value - `imperial` here is a hint to
            display layers that the caller wants imperial conversion.
        generatedAt:
          type: string
          format: date-time
          example: "2026-05-13T05:38:42Z"
        expiresAt:
          type: string
          format: date-time
          description: Hint for client-side caches; matches `Cache-Control` s-maxage.
          example: "2026-05-13T05:48:42Z"
        modelSources:
          type: array
          items: { type: string }
          description: Internal model identifiers contributing to this response.
          example: ["nbm", "hrrr", "ecmwf"]
        attribution:
          type: string
          description: Optional credit string.
          example: "Powered by GraySky"

    # ----------------------------------------------------------------------
    # Blocks
    # ----------------------------------------------------------------------
    CurrentBlock:
      type: object
      required: [validAt, conditionCode, conditionLabel]
      properties:
        validAt: { type: string, format: date-time }
        conditionCode:   { $ref: "#/components/schemas/ConditionCode" }
        conditionLabel:  { type: string, example: "Mostly Cloudy" }
        temperatureC:    { type: number, format: double }
        feelsLikeC:      { type: number, format: double }
        dewPointC:       { type: number, format: double }
        heatIndexC:      { type: number, format: double, nullable: true, description: "NWS Rothfusz heat index, °C. Null when temperatureC < 26.7 (regression undefined below 80 °F)." }
        humidityPercent: { type: number, minimum: 0, maximum: 100 }
        cloudCoverPercent: { type: number, minimum: 0, maximum: 100 }
        visibilityKm:    { type: number, format: double, minimum: 0, nullable: true }
        uvIndex:         { type: integer, minimum: 0, nullable: true }
        uvCategory:      { allOf: [{ $ref: "#/components/schemas/UVCategory" }], nullable: true }
        uvSource:        { allOf: [{ $ref: "#/components/schemas/UVSource" }], nullable: true }
        pressureMb:      { type: number, format: double }
        precipRateMmHr:
          type: number
          format: double
          minimum: 0
          description: |
            Instantaneous precipitation rate at "now" in mm/hr. Blended with
            the MRMS radar nowcast (lead-5min frame) when available - see
            `precipSource`. The companion `hourly[0].precipChancePercent`
            gives the in-progress hour's probability for that bucket;
            `precipRateMmHr > 0` here is a direct observation/derivation.
        precipType: { $ref: "#/components/schemas/PrecipType" }
        precipSource:
          type: string
          enum: [model, mrms]
          description: |
            How `precipRateMmHr` was derived. `mrms` means the radar nowcast
            overrode the model rate (ground truth wins); `model` means the
            primary source's native precip channel was used. Clients can
            surface this as a freshness/confidence badge.
        windSpeedMs:     { type: number, format: double, minimum: 0 }
        windGustMs:      { type: number, format: double, minimum: 0, nullable: true }
        windDirectionDeg:      { type: number, format: double, minimum: 0, maximum: 360 }
        windDirectionCardinal: { $ref: "#/components/schemas/Cardinal" }
        # Imperial-suffixed companions (only present when ?units=imperial).
        # `null` SI inputs produce `null` imperial values.
        temperatureF:    { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        feelsLikeF:      { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        dewPointF:       { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        heatIndexF:      { type: number, format: double, nullable: true, description: "Present when units=imperial and heatIndexC is non-null." }
        visibilityMi:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        pressureInHg:    { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        precipRateInHr:  { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windSpeedMph:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windGustMph:     { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        summary:
          type: string
          description: |
            One-sentence natural-language summary of the next few hours,
            anchored to the time of day ("Clear and cooling through the
            afternoon."). Rule-based, deterministic, no LLM. Empty string
            when the hourly window is too thin to draw on.
          example: "Clear and cooling through the afternoon."
        summaryShort:
          type: string
          description: |
            Terse variant of `summary` for glance UIs (widget, watch).
            "Condition" or "Condition, trend" - no trailing period.
          example: "Clear, cooling"

    HourlyBlock:
      type: object
      required: [data]
      description: |
        Hourly forecast block. `data[0]` is the **in-progress hour** - the
        bucket whose `validAt <= request-time < validAt + 1h`. Matches
        Dark Sky / Apple WeatherKit / OpenWeather / NWS conventions. The
        `current` block represents the now-instant and is radar-blended
        against the latest MRMS observation; treat `current` and
        `hourly[0]` as complementary views of the same hour rather than
        reconcileable forms of the same fact. Clients picking a "current
        hour" symbol/label should look for the bucket containing now and
        fall back to `current` when none exists (e.g. stale cached
        snapshot).
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/HourlyEntry" }
        elapsed:
          type: array
          description: >
            Today's already-elapsed local hours, from local midnight up to (but
            not including) the in-progress hour that `data[0]` begins at. Same
            entry shape as `data`; never overlaps it. May be empty or absent when
            the rolling snapshot store has no coverage for the cell (outside
            CONUS, or before warm-up). Clients render `elapsed` + `data` as a
            continuous full-day strip in the Today drilldown.
          items: { $ref: "#/components/schemas/HourlyEntry" }
        summary:
          type: string
          description: Optional rule-based text summary of the hourly block.

    HourlyEntry:
      type: object
      required: [validAt, conditionCode]
      description: |
        Per-hour forecast row. `validAt` is the start of the hour
        (top-of-hour) in UTC; the row covers `[validAt, validAt + 1h)`.
        When `conditionCode` is `limited`, the numeric fields are `null`
        and clients should render "unavailable"; the `validAt` timestamp
        is still trustworthy.
      properties:
        validAt:               { type: string, format: date-time }
        conditionCode:         { $ref: "#/components/schemas/ConditionCode" }
        conditionLabel:        { type: string }
        temperatureC:          { type: number, format: double, nullable: true }
        feelsLikeC:            { type: number, format: double, nullable: true }
        dewPointC:             { type: number, format: double, nullable: true }
        heatIndexC:            { type: number, format: double, nullable: true, description: "NWS heat index, °C. Null when temperatureC < 26.7." }
        humidityPercent:       { type: number, minimum: 0, maximum: 100, nullable: true }
        cloudCoverPercent:     { type: number, minimum: 0, maximum: 100, nullable: true }
        visibilityKm:          { type: number, format: double, minimum: 0, nullable: true }
        uvIndex:               { type: integer, minimum: 0, nullable: true }
        uvCategory:            { allOf: [{ $ref: "#/components/schemas/UVCategory" }], nullable: true }
        uvSource:              { allOf: [{ $ref: "#/components/schemas/UVSource" }], nullable: true }
        precipChancePercent:   { type: number, minimum: 0, maximum: 100, nullable: true }
        precipMm:              { type: number, format: double, minimum: 0, nullable: true }
        precipRateMmHr:        { type: number, format: double, minimum: 0, nullable: true }
        precipType:            { $ref: "#/components/schemas/PrecipType" }
        windSpeedMs:           { type: number, format: double, minimum: 0, nullable: true }
        windGustMs:            { type: number, format: double, minimum: 0, nullable: true }
        windDirectionDeg:      { type: number, format: double, minimum: 0, maximum: 360, nullable: true }
        windDirectionCardinal: { $ref: "#/components/schemas/Cardinal" }
        # Imperial-suffixed companions (only present when ?units=imperial).
        temperatureF:    { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        feelsLikeF:      { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        dewPointF:       { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        heatIndexF:      { type: number, format: double, nullable: true, description: "Present when units=imperial and heatIndexC is non-null." }
        visibilityMi:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        precipIn:        { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        precipRateInHr:  { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windSpeedMph:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windGustMph:     { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }

    DailyBlock:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/DailyEntry" }
        summary:
          type: string

    DailyEntry:
      type: object
      required: [validAt, conditionCode, temperatureMaxC, temperatureMinC]
      properties:
        validAt:              { type: string, format: date-time }
        conditionCode:        { $ref: "#/components/schemas/ConditionCode" }
        conditionLabel:       { type: string }
        temperatureMaxC:      { type: number, format: double }
        temperatureMinC:      { type: number, format: double }
        feelsLikeMaxC:        { type: number, format: double }
        feelsLikeMinC:        { type: number, format: double }
        dewPointMaxC:         { type: number, format: double, nullable: true, description: "Day's max hourly dew point, °C. Null when no hourly entries had dew data." }
        dewPointMinC:         { type: number, format: double, nullable: true, description: "Day's min hourly dew point, °C. Null when no hourly entries had dew data." }
        humidityPercent:      { type: number, minimum: 0, maximum: 100 }
        cloudCoverPercent:    { type: number, minimum: 0, maximum: 100 }
        precipChancePercent:  { type: number, minimum: 0, maximum: 100 }
        precipMm:             { type: number, format: double, minimum: 0 }
        precipType:           { $ref: "#/components/schemas/PrecipType" }
        uvIndex:              { type: integer, minimum: 0 }
        uvCategory:           { $ref: "#/components/schemas/UVCategory" }
        uvIndexPeakTime:      { type: string, format: date-time, nullable: true, description: "RFC 3339 timestamp of the hourly entry that produced the day's max UV. Null when the day has no UV data on any hour." }
        uvSource:             { allOf: [{ $ref: "#/components/schemas/UVSource" }], nullable: true }
        visibilityKm:         { type: number, format: double, minimum: 0, nullable: true }
        windSpeedMs:          { type: number, format: double, minimum: 0 }
        windGustMs:           { type: number, format: double, minimum: 0, nullable: true }
        windDirectionDeg:     { type: number, format: double, minimum: 0, maximum: 360 }
        windDirectionCardinal: { $ref: "#/components/schemas/Cardinal" }
        sunriseAt:            { type: string, format: date-time, nullable: true }
        sunsetAt:             { type: string, format: date-time, nullable: true }
        solarNoonAt:          { type: string, format: date-time, nullable: true, description: "Sun at its highest point on this calendar date." }
        civilDawnAt:          { type: string, format: date-time, nullable: true, description: "Sun 6° below horizon - bright enough to discern outdoor activity without artificial light." }
        civilDuskAt:          { type: string, format: date-time, nullable: true }
        nauticalDawnAt:       { type: string, format: date-time, nullable: true, description: "Sun 12° below horizon - horizon visible at sea." }
        nauticalDuskAt:       { type: string, format: date-time, nullable: true }
        astronomicalDawnAt:   { type: string, format: date-time, nullable: true, description: "Sun 18° below horizon - sky fully dark for astronomy. Null at high latitudes where the sun never reaches this depression." }
        astronomicalDuskAt:   { type: string, format: date-time, nullable: true }
        moonriseAt:           { type: string, format: date-time, nullable: true }
        moonsetAt:            { type: string, format: date-time, nullable: true }
        moonPhase:            { type: number, format: double, minimum: 0, maximum: 1, nullable: true }
        narrative:            { type: string, description: "Optional rule-based day narrative." }
        # Imperial-suffixed companions (only present when ?units=imperial).
        temperatureMaxF: { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        temperatureMinF: { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        feelsLikeMaxF:   { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        feelsLikeMinF:   { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        dewPointMaxF:    { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        dewPointMinF:    { type: number, format: double, nullable: true, description: "Present when units=imperial." }
        precipIn:        { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        visibilityMi:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windSpeedMph:    { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        windGustMph:     { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }

    MinutelyBlock:
      type: object
      required: [data]
      properties:
        data:
          type: array
          minItems: 0
          items: { $ref: "#/components/schemas/MinutelyEntry" }
        summary:
          type: string
          description: |
            Long-form next-hour nowcast caption ("Rain starting in 5 minutes.
            Stopping in 20 minutes."). Generated from PySTEPS optical-flow
            output, rule-based decision tree over a 60-min trajectory.
            Deterministic, no LLM. When the trajectory cannot be loaded
            (transient pipeline gap), `data` is empty and the summary surfaces
            "Minute-by-minute precipitation is temporarily unavailable." -
            distinct from the out-of-CONUS / out-of-grid case which says
            "not available for this location."
          example: "Rain starting in 5 minutes. Stopping in 20 minutes."
        summaryShort:
          type: string
          description: |
            Terse variant of `summary` for glance UIs (widget, watch).
            No trailing period.
          example: "Rain in 5 minutes"
        precipChanceSource:
          type: string
          enum: [heuristic, calibrated]
          description: |
            Provenance of every `precipChancePercent` in `data`.
            `calibrated`: neighborhood wet fraction mapped through the
            nightly isotonic reliability fit - a 70 means it has rained on
            ~70% of comparable forecasts. `heuristic`: legacy rate ladder
            (0/20/70/95), NOT a probability; served when calibration curves
            are missing, stale, or the point sits in a radar-gap patch.
          example: "calibrated"
        coverage:
          type: string
          enum: [ok, outside, unavailable]
          description: |
            Machine-readable provenance for an empty `data` array -
            previously only distinguishable by string-matching the summary,
            which breaks on any non-English locale. `outside`: the point has
            no nowcast producer region (or falls outside the producer grid)
            - permanent for this location. `unavailable`: transient
            (trajectory load failure or an interior radar gap) - worth
            retrying. `ok`: `data` carries a real per-minute series.
          example: "ok"
        anchorAgeMinutes:
          type: number
          nullable: true
          description: |
            Wall-clock age (minutes) of the radar cycle the series is
            anchored to - ~3-7 for CONUS, ~15-22 for Europe. Null when
            unknown (no trajectory, or a malformed cycle timestamp);
            clients should treat null as "possibly stale", not fresh.
          example: 4.2

    MinutelyEntry:
      type: object
      required: [validAt, precipRateMmHr]
      properties:
        validAt:             { type: string, format: date-time }
        precipRateMmHr:      { type: number, format: double, minimum: 0 }
        precipRateInHr:      { type: number, format: double, minimum: 0, nullable: true, description: "Present when units=imperial." }
        precipChancePercent: { type: number, minimum: 0, maximum: 100 }

    ConditionCode:
      type: string
      description: |
        Schema-enforced enum, mapped 1:1 from legacy Dark Sky `icon` strings.
        New conditions are additive - clients should fall back to `cloudy` for
        any unknown value.

        `limited` marks a degraded-mode entry: the validAt timestamp is valid
        but the numeric fields are `null` because every model source ran out
        of coverage for that lead. Clients should render "data unavailable"
        for these rows rather than a zero-filled fake value.
      enum:
        - clear-day
        - clear-night
        - mostly-clear-day
        - mostly-clear-night
        - partly-cloudy-day
        - partly-cloudy-night
        - mostly-cloudy-day
        - mostly-cloudy-night
        - cloudy
        - rain
        - sleet
        - snow
        - wind
        - fog
        - hail
        - thunderstorm
        - scattered-thunderstorms
        - isolated-thunderstorms
        - tornado
        - limited

    PrecipType:
      type: string
      enum: [none, rain, snow, sleet, hail, freezing-rain, mix]
      default: none

    Cardinal:
      type: string
      enum: [N, NNE, NE, ENE, E, ESE, SE, SSE, S, SSW, SW, WSW, W, WNW, NW, NNW]

    UVCategory:
      type: string
      enum: [low, moderate, high, veryHigh, extreme]
      description: "WHO global UV-exposure category. Thresholds: <3 low, 3–5 moderate, 6–7 high, 8–10 very high, 11+ extreme."

    UVSource:
      type: string
      enum: [gfs_uv, dswrf_derived]
      description: |
        Which signal produced the uvIndex value.
        `gfs_uv` = GFS DUVB surface UV-B flux converted via the McKenzie 2004 factor.
        `dswrf_derived` = broadband downward-shortwave fallback (used when GFS DUVB
        is unavailable for the cell).

    # ----------------------------------------------------------------------
    # RFC 9457 Problem Details
    # ----------------------------------------------------------------------
    Problem:
      type: object
      required: [type, title, status]
      properties:
        type:     { type: string, format: uri, default: "about:blank" }
        title:    { type: string }
        status:   { type: integer }
        detail:   { type: string }
        instance: { type: string, format: uri }
        traceId:  { type: string, description: "X-Ray / correlation id for support tickets." }
