# Customer-facing product contract (Scalar /docs.html via web/public/openapi.yaml).
# Full implementer surface (billing, webhooks, ops probes): openapi.internal.yaml
openapi: 3.1.0
info:
  title: Grid Headroom API
  version: 1.0.0
  summary: UK DNO substation capacity and headroom (indicative, not a PoC offer)
  description: |
    Grid Headroom aggregates UK Distribution Network Operator (DNO) substation
    capacity and headroom into a single REST API. Query the nearest primary
    substation and published demand, generation, and fault-level headroom for a
    WGS84 point. Values are indicative planning data, not a formal Point of
    Connection offer.

    **Base URL:** `https://api.gridheadroom.com/v1`

    **Authentication:** `Authorization: Bearer <api_key>`. Keys use prefixes
    `gh_test_` (test) and `gh_live_` (live). Obtain a key via Checkout on
    [gridheadroom.com](https://gridheadroom.com); manage billing from the
    Customer Portal linked after signup.

    **Terms:** Self-serve use is AS-IS / best effort.

    Capacity measures are unit-bearing (`MW` / `MVA` / `kA`) with an explicit
    published vs not-published state. Clients should ignore unknown response
    fields added within this major version.
  contact:
    name: Grid Headroom
    url: https://gridheadroom.com
  license:
    name: Proprietary API contract; underlying facts remain under each source licence
    url: https://gridheadroom.com

servers:
  - url: https://api.gridheadroom.com/v1
    description: Production (versioned product API)

tags:
  - name: substations
    description: Spatial substation and headroom queries

paths:
  /substation/nearest:
    get:
      tags: [substations]
      operationId: getNearestSubstation
      summary: Nearest primary substation(s) and published headroom
      description: |
        Nearest-neighbour query for the DNO licence area that contains the point.
        Results are from that DNO only.

        Capacity measures use a three-state contract: a measured zero is
        `availability: PUBLISHED` with `value: 0`; not published is
        `availability: NOT_PUBLISHED` with `value` and `unit` null (never `0.0`
        as a stand-in for missing data).

        Records whose source licence cannot be resolved are not served
        (`403` / `LICENCE_UNRESOLVED`).
      security:
        - ApiKeyAuth: []
      parameters:
        - name: latitude
          in: query
          required: true
          description: Query latitude (WGS84 / EPSG:4326). Must fall within the UK bounding box.
          schema:
            type: number
            format: double
            minimum: 49.8
            maximum: 60.9
            example: 51.2301
        - name: longitude
          in: query
          required: true
          description: Query longitude (WGS84 / EPSG:4326).
          schema:
            type: number
            format: double
            minimum: -8.6
            maximum: 1.8
            example: -2.3197
        - name: limit
          in: query
          required: false
          description: |
            Maximum substations to return (nearest first). Default 1.
            Plan tiers may apply lower effective caps.
          schema:
            type: integer
            minimum: 1
            maximum: 25
            default: 1
      responses:
        "200":
          description: |
            Success. `data` may be empty if no in-scope substation is found
            (still HTTP 200; absence of a neighbour is not an error).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NearestSubstationResponse"
              examples:
                publishedAndNotPublished:
                  summary: Mixed published / not-published measures
                  value:
                    status: success
                    meta:
                      query_coordinates:
                        latitude: 51.2301
                        longitude: -2.3197
                      result_count: 1
                      execution_time_ms: 3.8
                    data:
                      - substation_id: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
                        dno_code: NGED
                        substation_ref: NGED-SOM-5412
                        name: Frome North Primary
                        voltage_kv: 33.0
                        location:
                          latitude: 51.2345
                          longitude: -2.315
                          distance_meters: 582.4
                        headroom:
                          generation:
                            availability: PUBLISHED
                            value: 4.5
                            unit: MVA
                            not_published_reason: null
                          demand:
                            availability: PUBLISHED
                            value: 1.2
                            unit: MVA
                            not_published_reason: null
                          fault_level:
                            availability: NOT_PUBLISHED
                            value: null
                            unit: null
                            not_published_reason: NOT_PUBLISHED_BY_DNO
                          calculation_method: FIRM_DERIVED
                          status: AVAILABLE
                          data_freshness: "2026-07-20"
                          source_dataset: ltds-capacity-heatmap
                          source_last_refresh: "2026-05-01"
                          confidence: HIGH
                    provenance_and_attribution:
                      data_source_attribution: "Supported by NGED Open Data"
                      license_url: "https://connecteddata.nationalgrid.co.uk/"
                      disclaimer: "Informational metric only. Does not constitute a formal Point of Connection offer."
                measuredZero:
                  summary: Known measured zero (not the same as not published)
                  value:
                    status: success
                    meta:
                      query_coordinates:
                        latitude: 51.5
                        longitude: -0.12
                      result_count: 1
                      execution_time_ms: 4.1
                    data:
                      - substation_id: 11111111-1111-1111-1111-111111111111
                        dno_code: UKPN
                        substation_ref: UKPN-EX-0001
                        name: Example Primary
                        voltage_kv: 11.0
                        location:
                          latitude: 51.501
                          longitude: -0.121
                          distance_meters: 120.0
                        headroom:
                          generation:
                            availability: PUBLISHED
                            value: 0.0
                            unit: MW
                            not_published_reason: null
                          demand:
                            availability: NOT_PUBLISHED
                            value: null
                            unit: null
                            not_published_reason: NOT_PUBLISHED_BY_DNO
                          fault_level:
                            availability: NOT_PUBLISHED
                            value: null
                            unit: null
                            not_published_reason: SENTINEL_IN_SOURCE
                          calculation_method: FIRM_DERIVED
                          status: CONSTRAINED
                          data_freshness: "2026-07-28"
                          source_dataset: example-ukpn-headroom-dataset
                          source_last_refresh: "2026-07-27"
                          confidence: MEDIUM
                    provenance_and_attribution:
                      data_source_attribution: "Attribution derived from the source licence text (no shared OGL default)."
                      license_url: "https://ukpowernetworks.opendatasoft.com/"
                      disclaimer: "Informational metric only. Does not constitute a formal Point of Connection offer."
        "400":
          description: Invalid coordinates or query parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                status: error
                error:
                  code: INVALID_QUERY
                  message: "latitude must be within the UK bounding box (49.8-60.9)."
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                status: error
                error:
                  code: UNAUTHORIZED
                  message: "Valid API key required."
        "403":
          description: |
            Request understood but refused. Includes licence unresolved: the API
            does not serve a capacity record whose source licence is unresolved.
            Same envelope for other policy refusals.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                licenceUnresolved:
                  summary: Source licence unresolved; refuse to serve
                  value:
                    status: error
                    error:
                      code: LICENCE_UNRESOLVED
                      message: "Capacity data for this source cannot be served until its licence is resolved."
                calculationMethodUnresolved:
                  summary: calculation_method unresolved; refuse to serve
                  value:
                    status: error
                    error:
                      code: CALCULATION_METHOD_UNRESOLVED
                      message: "Capacity data cannot be served with an unresolved calculation_method."
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Unexpected server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: gh_test_ / gh_live_
      description: |
        API key minted after Checkout. Prefix `gh_test_` in test mode,
        `gh_live_` in live. Send as `Authorization: Bearer <key>`.

  schemas:
    NearestSubstationResponse:
      type: object
      additionalProperties: false
      required:
        - status
        - meta
        - data
        - provenance_and_attribution
      properties:
        status:
          type: string
          const: success
        meta:
          $ref: "#/components/schemas/QueryMeta"
        data:
          type: array
          items:
            $ref: "#/components/schemas/SubstationNearestItem"
        provenance_and_attribution:
          $ref: "#/components/schemas/ProvenanceAndAttribution"

    QueryMeta:
      type: object
      additionalProperties: false
      required:
        - query_coordinates
        - result_count
        - execution_time_ms
      properties:
        query_coordinates:
          $ref: "#/components/schemas/Coordinates"
        result_count:
          type: integer
          minimum: 0
        execution_time_ms:
          type: number
          format: double
          minimum: 0
          description: Query latency in milliseconds (not a claim about data currency).

    Coordinates:
      type: object
      additionalProperties: false
      required: [latitude, longitude]
      properties:
        latitude:
          type: number
          format: double
        longitude:
          type: number
          format: double

    SubstationNearestItem:
      type: object
      additionalProperties: false
      required:
        - substation_id
        - dno_code
        - substation_ref
        - name
        - voltage_kv
        - location
        - headroom
      properties:
        substation_id:
          type: string
          format: uuid
        dno_code:
          $ref: "#/components/schemas/DnoCode"
        substation_ref:
          type: string
          description: DNO-local reference string (no universal cross-DNO UUID exists).
        name:
          type: string
        voltage_kv:
          type: number
          format: double
          description: |
            Voltage in kV. One of the known UK set: 0.4, 6.6, 11, 33, 66, 132, 275, 400.
          enum: [0.4, 6.6, 11.0, 33.0, 66.0, 132.0, 275.0, 400.0]
        location:
          $ref: "#/components/schemas/SubstationLocation"
        headroom:
          $ref: "#/components/schemas/HeadroomBlock"

    SubstationLocation:
      type: object
      additionalProperties: false
      required: [latitude, longitude, distance_meters]
      properties:
        latitude:
          type: number
          format: double
        longitude:
          type: number
          format: double
        distance_meters:
          type: number
          format: double
          minimum: 0
          description: Distance from query point in metres.

    HeadroomBlock:
      type: object
      additionalProperties: false
      required:
        - generation
        - demand
        - fault_level
        - calculation_method
        - status
        - data_freshness
        - source_dataset
        - source_last_refresh
        - confidence
      properties:
        generation:
          $ref: "#/components/schemas/CapacityMeasure"
        demand:
          $ref: "#/components/schemas/CapacityMeasure"
        fault_level:
          $ref: "#/components/schemas/CapacityMeasure"
        calculation_method:
          $ref: "#/components/schemas/CalculationMethod"
        status:
          $ref: "#/components/schemas/CapacityStatus"
        data_freshness:
          type: string
          format: date
          description: |
            Our ingestion snapshot date. Not the DNO's publication date; see
            `source_last_refresh`.
        source_dataset:
          type: string
          minLength: 1
          description: |
            Specific feed or dataset identifier (for example a DNO layer id or
            LTDS edition), not merely the DNO code.
        source_last_refresh:
          type: string
          format: date
          nullable: true
          description: |
            The DNO's own last-refresh / publication date for the figure.
            Null only when the source does not publish a refresh date.
            Never substitute `data_freshness` here.
        confidence:
          $ref: "#/components/schemas/Confidence"

    CapacityMeasure:
      type: object
      additionalProperties: false
      required: [availability, value, unit]
      description: |
        Unit-bearing three-state measure. `value` and `unit` are both non-null
        exactly when `availability` is `PUBLISHED`. No field may be omitted
        (`null` must serialise).
      properties:
        availability:
          $ref: "#/components/schemas/Availability"
        value:
          type: number
          format: double
          nullable: true
          description: Present only when `availability` is `PUBLISHED` (including measured 0.0).
        unit:
          allOf:
            - $ref: "#/components/schemas/CapacityUnit"
          nullable: true
          description: Present only when `availability` is `PUBLISHED`. Never inferred from the field name.
        not_published_reason:
          allOf:
            - $ref: "#/components/schemas/NotPublishedReason"
          nullable: true
          description: |
            Optional when `availability` is `NOT_PUBLISHED`.
            Null when published. Serialise explicitly.

    Availability:
      type: string
      enum: [PUBLISHED, NOT_PUBLISHED]
      description: |
        Two-valued enum; three customer-visible states via the cross product with
        `value`: real value, measured zero, or not published.

    CapacityUnit:
      type: string
      enum: [MW, MVA, kA]
      description: |
        Exact unit the DNO published. No silent conversion.

    NotPublishedReason:
      type: string
      enum:
        - NOT_PUBLISHED_BY_DNO
        - SENTINEL_IN_SOURCE
        - UNIT_UNRESOLVED
        - QUARANTINED
      description: |
        Why the measure is not published. Optional on the wire; recommended when
        known. Clients must ignore unknown future values.

    CalculationMethod:
      type: string
      enum: [FIRM_DERIVED, REALTIME_ANM, ESTIMATED]
      description: |
        How the headroom figure was derived. Records with an unresolved method
        are refused at read (`CALCULATION_METHOD_UNRESOLVED`). Clients must
        ignore unknown future values if the enum widens additively.

    CapacityStatus:
      type: string
      enum: [AVAILABLE, CONSTRAINED, CLOSED, REPAIRED_UNVERIFIED]
      description: |
        Headroom or site status. `REPAIRED_UNVERIFIED` marks repaired mappings
        not yet proven stable. Clients must ignore unknown future values.

    Confidence:
      type: string
      enum: [HIGH, MEDIUM, LOW, REPAIRED_UNVERIFIED]
      description: |
        Confidence indicator reflecting calculation method, source quality, and
        repair state. Clients must ignore unknown values.

    DnoCode:
      type: string
      enum: [NGED, UKPN, SSEN, SPEN, NPg, ENW]
      description: Canonical short group codes.

    ProvenanceAndAttribution:
      type: object
      additionalProperties: false
      required:
        - data_source_attribution
        - license_url
        - disclaimer
      properties:
        data_source_attribution:
          type: string
          minLength: 1
          description: |
            Per-source attribution derived from that source's own licence text
            (no shared default string). May be multi-part. When NESO data
            contributed, includes exactly:
            `Supported by National Energy SO Open Data`.
        license_url:
          type: string
          format: uri
          description: URL of the operative source licence (or portal licence page).
        disclaimer:
          type: string
          const: "Informational metric only. Does not constitute a formal Point of Connection offer."
          description: Liability shield; this is not a Point of Connection offer.

    ErrorResponse:
      type: object
      additionalProperties: false
      required: [status, error]
      properties:
        status:
          type: string
          const: error
        error:
          $ref: "#/components/schemas/ErrorBody"

    ErrorBody:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        code:
          type: string
          description: |
            Machine-readable code. Reserved values include:
            `INVALID_QUERY`, `UNAUTHORIZED`, `LICENCE_UNRESOLVED`,
            `CALCULATION_METHOD_UNRESOLVED`, `RATE_LIMITED`, `INTERNAL_ERROR`.
          examples:
            - LICENCE_UNRESOLVED
        message:
          type: string
