openapi: 3.1.0

info:
  title: Globe Parts Trade API
  version: 1.0.0
  summary: Live pricing, stock and ordering of automotive glass for your trade account.
  description: |
    The Globe Parts Trade API allows you to price, check and order automotive glass and
    fitting materials without leaving your own software. Send us a vehicle, get back the
    glass we can supply for it — each with your account's net price and our stock on hand
    — and place the order in the same session.

    Use it if you run your own job-management or point-of-sale product and hold a Globe
    Parts trade account — or build software for people who do.

    ## Authentication

    Send your key in the **`X-Globe-Api-Key`** header on every request, over TLS.

    Your key resolves to exactly one trade account, and that binding decides everything —
    the prices you see, the catalogue you see, the branch you're served from. There is no
    account parameter on any request, and you can't address another account's data.

    Keys are rotatable, and we'll run two keys on your account during a rotation window so
    you can cut over without downtime.

    **`401` and `403` are terminal — never retry them.** The credential or its entitlement
    is wrong and will stay wrong until someone changes it. See *Errors and retries*.

    Contact us on **1800 764 994** for feedback.

  contact:
    name: Globe Parts — Integrations
    email: dev@globeparts.com.au
    url: https://www.globeparts.com.au

servers:
  - url: https://api.globeparts.com.au/trade/v1
    description: Production
  - url: https://api-sandbox.globeparts.com.au/trade/v1
    description: |
      Sandbox — the full contract against our live catalogue: real parts, your real
      prices, live stock. Orders are accepted but never fulfilled or billed — nothing
      you place here reaches a branch. Your sandbox key is issued alongside your
      production key at onboarding.

tags:
  - name: Fitment
    description: Send us a vehicle, get back the glass we can supply for it.
  - name: Availability
    description: Get your net price and our stock on hand.
  - name: Delivery
    description: When our delivery runs reach your store.
  - name: Orders
    description: Place an order and poll its status.
  - name: Catalogue
    description: Get product detail, and the fitting materials that go with a glass part.
  - name: Service
    description: Health and monitoring.
  - name: Errors and retries
    description: |
      Every error comes back as **RFC 9457 Problem Details**, served as
      `application/problem+json`:

      ```json
      {
        "type": "https://api.globeparts.com.au/problems/price-changed",
        "title": "Price changed since your quote",
        "status": 409,
        "detail": "Resolved total 148.20 exceeds expectedTotalExGst 140.00 by more than the 0.02 tolerance."
      }
      ```

      **Branch on `type`, never on `title`.** Titles are human-readable and we may reword
      them; `type` values are stable identifiers.

      Two classes:

      | Status | Class | What to do |
      |---|---|---|
      | `400` `401` `403` `404` `409` `422` | **Terminal** | Don't retry. Fix the request, the credential, or ask the operator. Retrying returns the same answer |
      | `429` | **Retryable** | Wait for `Retry-After`, then retry. Honouring it rather than retrying immediately is a condition of access |
      | `500` `503` | **Retryable** | Back off exponentially with jitter. For `POST /orders`, retry with the **same `Idempotency-Key`** — never a fresh one |

      Every response carries `X-Request-Id`. Quote it when you contact us about a specific
      call.

      ### Problem types — the full list

      Every `type` this API returns. A new problem type may be added **without** a
      version bump. Branch on the types you know; for one you don't, fall back to the
      HTTP status class.

      | `type` (…/problems/) | Status | Class |
      |---|---|---|
      | `validation-failed` | 400 | Terminal |
      | `batch-too-large` | 400 | Terminal |
      | `unauthorized` | 401 | Terminal |
      | `account-on-hold` | 403 | Terminal |
      | `not-found` | 404 | Terminal |
      | `idempotency-key-reused` | 409 | Terminal |
      | `idempotency-key-in-flight` | 409 | Retryable after a short delay |
      | `price-changed` | 409 | Terminal — re-check, then resubmit |
      | `vehicle-unresolved` | 422 | Terminal |
      | `ambiguous-part-number` | 422 | Terminal — choose a candidate SKU |
      | `unknown-sku` | 422 | Terminal |
      | `not-purchasable` | 422 | Terminal |
      | `fulfilment-unavailable` | 422 | Terminal — resubmit as `pickup` |
      | `credit-limit-exceeded` | 422 | Terminal *for this order* — a smaller order, or the same one after a payment lands, may succeed |
      | `rate-limited` | 429 | Retryable after `Retry-After` |
      | `internal-error` | 500 | Retryable with backoff |
      | `unavailable` | 503 | Retryable after `Retry-After` |

      **`account-on-hold`** covers two cases — the account is not in good standing, or
      ordering is switched off on the account. We don't say which. `detail` carries a
      line your operator can act on; the remedy is a phone call to Globe Parts
      accounts.
      **A part outside the catalogue your account can see** is never a `403`: it is
      `404` on `GET /products/{sku}`, a `notFound` entry with reason `not_purchasable`
      on `POST /availability`, and `422` `not-purchasable` on `POST /orders`.
      **`credit-limit-exceeded`** applies to *this order*, not the credential — a
      smaller order, or the same one after a payment lands, may succeed. We return no
      limit, balance or headroom figure.
  - name: Rate limits
    description: |
      Two limits apply together: a **per-key request rate**, which protects the service, and
      a **per-account daily budget**, shared across every key on your account.

      | Limit | Scope | |
      |---|---|---|
      | `POST /fitment/glass` | per key | 60 / minute |
      | `POST /availability` | per key | 60 / minute |
      | `POST /orders` | per key | 30 / minute |
      | `GET` endpoints | per key | 120 / minute |
      | `POST /fitment/glass` | **per account** | 300 / day |
      | `POST /availability` | **per account** | 1,800 SKU-lines / day |
      | `POST /orders` | **per account** | 150 / day |
      | `GET` endpoints | **per account** | 12,000 / day |

      We meter `POST /availability` in **SKU-lines, not requests** — a request carrying 40
      SKUs draws 40.

      Budgets reset at 00:00 `Australia/Perth`. Per-minute limits run on a sliding window
      with a one-minute burst allowance. We return a `RateLimit` header on every
      authenticated `2xx` response and on every `429`, reflecting whichever limit is
      nearest exhaustion.

      `GET /health` is unauthenticated and rate-limited by source address.

      If your workload approaches a limit, contact us to raise it.
  - name: Compatibility
    description: |
      ### What we can change without warning you

      We ship additive changes without a version bump: **new endpoints, new optional request
      fields, and new response properties**. **Ignore any response property you don't
      recognise** rather than failing on it (the [tolerant
      reader](https://martinfowler.com/bliki/TolerantReader.html) pattern).

      We never send a placeholder to mean "unknown" — no sentinel code, no empty string,
      no zero standing in for missing.

      ### What we can't

      Everything else is a **breaking change**, and a breaking change ships as a **new major
      version at a new base path**. Your integration keeps running against the old one until
      you choose to move.

      Breaking, explicitly:

      - Removing anything, or changing what it means.
      - Adding a required request field, or tightening validation on an existing one.
      - Changing a default.
      - **Adding a value to any enumeration we return.**

      **Every value set we return is closed and enumerated in full in this document.**
      When our vocabulary grows, that is a new major version, and we will tell you
      before it ships. The one exception is `problem.type` — see *Problem types* above.
  - name: Cross-references
    description: |
      Our parts carry the equivalent numbers we hold for them — Eurocode, **NAGS AU**, OEM,
      and other alternates. `GET /products/{sku}?supplier=eurocode` (or `nagsau`, `oem`,
      `obriens`) reads the path as *your* number and returns *our* part.

      We give you these **to quote, order and fulfil your own work**. Coverage varies by
      part: where we don't hold an identifier we return `null` or an empty array, never an
      error.
  - name: Terms
    description: |
      Access is proprietary and governed by the Globe Parts Trade API Terms of Use we agree
      with you.

security:
  - ApiKeyAuth: []

paths:

  # ---------------------------------------------------------------- Fitment --

  /fitment/glass:
    post:
      tags: [Fitment]
      operationId: findGlassForVehicle
      summary: /fitment/glass
      description: |
        Send us a vehicle and we return every glass part we carry for it, each with
        your net price and our stock on hand at the serving branch.

        **We don't look up registrations or VINs on your behalf.** You resolve the vehicle
        with your own vehicle-data provider and send us the vehicle object.

        This endpoint accepts two request forms:

        - `infoagent` — your InfoAgent IVDS report envelope, passed through as-is. We
          normalise it.
        - `attributes` — an explicit make / model / year / series / badge / body object. Use
          this if you are on a different provider, or entering the vehicle by hand.

        Both forms resolve to the same normalised vehicle. We echo it back to you as
        `vehicleResolved` so you can see exactly what we matched.

        **Light vehicles only** — passenger cars and light commercials. Heavy vehicles
        (trucks, buses, plant) are not served through this API; a heavy vehicle is
        rejected with `422`.

        We order results by glass position — the order the codes are listed in the table
        below — then by SKU. That order is stable and carries no judgement: it is not a
        ranking, and the first match is not the best one.

        **How sure we are about each part is on the part**, as its `confidence` field
        (`confirmed`, `catalogue` or `inferred`). Show that indicator against the part.
        Do not infer it from position in the list.

        ## What the position codes mean

        Every `glassPosition` you can send or receive, with our name for it:

        | Code | Name | Code | Name |
        |---|---|---|---|
        | `F` | Windscreen | `RCL` | Rear cargo LH |
        | `FVL` | Front vent LH | `RCR` | Rear cargo RH |
        | `FVR` | Front vent RH | `RCL2` | Rear cargo LH 2 |
        | `FQL` | Front quarter LH | `RCR2` | Rear cargo RH 2 |
        | `FQR` | Front quarter RH | `RBDL` | Rear barn door LH |
        | `FDL` | Front door LH | `RBDR` | Rear barn door RH |
        | `FDR` | Front door RH | `RW` | Rear window |
        | `FOL` | Front opera LH | `RWU` | Rear window upper |
        | `FOR` | Front opera RH | `RWM` | Rear window mid |
        | `RDL` | Rear door LH | `RWL` | Rear window lower |
        | `RDR` | Rear door RH | `X` | Roof |
        | `RQL` | Rear door quarter LH | `IF` | Front windscreen moulding |
        | `RQR` | Rear door quarter RH | `IFT` | Front windscreen moulding upper |
        | `RSL` | Rear side cargo LH | `IFB` | Front windscreen moulding lower |
        | `RSR` | Rear side cargo RH | `IFL` | Front windscreen moulding LH |
        | `RSL2` | Rear side cargo LH 2 | `IFR` | Front windscreen moulding RH |
        | `RSR2` | Rear side cargo RH 2 | `IR` | Rear windscreen moulding |
        | `RSL3` | Rear side cargo LH 3 | `IRU` | Rear windscreen moulding upper |
        | `RSR3` | Rear side cargo RH 3 | `IRL` | Rear windscreen moulding lower |
        | `FCL` | Front cargo LH | `ICL` | Front windscreen clips |
        | `FCR` | Front cargo RH | `IRS` | Rain sensor bracket |
        | `MCL` | Mid cargo LH | `IRP` | Rain sensor patch |
        | `MCR` | Mid cargo RH | `IMS` | Interior mirror shroud |
        | `MCL2` | Mid cargo LH 2 | `IGF` | Door glass fittings |
        | `MCR2` | Mid cargo RH 2 | `IAE` | Windscreen antennae |

        If we resolve your vehicle but carry no glass for it, you get `200` with an empty
        `matches` array and `coverage: "none"` — not a `404`.

        ## Near misses

        When exact matching finds nothing, we widen the search one step at a time
        and stop at the first step that returns parts:

        1. `badge` — relaxed when the vehicle's exact attributes match no
           windscreen glass we hold for it.
        2. `series` — dropped only when we hold no series records for the vehicle in
           our database; `badge` stays relaxed when we do.

        **That is the whole ladder. `make`, `model`, `year` and `bodyType` are never
        relaxed.** If nothing comes back at the floor, you get an empty `matches`
        array. We never mix exact and relaxed results in one response.

        The response carries a `relaxation` object naming what we dropped. That object is
        the single source — because we never mix exact and relaxed results, it holds for
        every part in `matches`, and no part repeats it.

        Where the candidates differ on something you never told us — a rain sensor left
        unknown, say — we return a `disambiguationHints` entry naming the question
        that settles it and, in `splitsMatches`, the SKUs it separates.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GlassFitmentRequest'
            examples:
              infoagentPassthrough:
                summary: InfoAgent report passed through verbatim
                value:
                  vehicle:
                    source: infoagent
                    infoagentReport:
                      data:
                        type: IVDS_AU_ADAS_REPORT
                        result:
                          responseCode: SUCCESS
                          vehicle:
                            identification:
                              plate: 1TAG123
                              state: WA
                              vin: JTEBH3FJ50K000000
                            details:
                              year: 2010
                              make: Toyota
                              model: Landcruiser Prado
                              subModel: GXL
                              series: KDJ150R
                              bodyType: Wagon
                              doors: 5
                            specifications:
                              informationDisplayHeadUp: NOT_AVAILABLE
                              rainSensorAutoWipers: NOT_AVAILABLE
                              camerasADASOnly: NOT_AVAILABLE
                  glassPositions: [F]
                  includeFittingMaterials: true
              explicitAttributes:
                summary: Vehicle entered by hand or from another provider
                value:
                  vehicle:
                    source: attributes
                    attributes:
                      make: TOYOTA
                      model: LANDCRUISER PRADO
                      year: 2010
                      series: KDJ150R
                      badge: GXL
                      bodyType: 5DR WAGON
                      features:
                        headsUpDisplay: false
                        rainSensor: false
                        forwardCamera: false
      responses:
        '200':
          description: Vehicle resolved. `matches` may be empty — check `coverage`.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GlassFitmentResponse'
              examples:
                exactMatch:
                  summary: Exact match — every attribute you sent was honoured
                  value:
                    vehicleResolved:
                      make: TOYOTA
                      model: LANDCRUISER PRADO
                      year: 2010
                      series: KDJ150R
                      badge: GXL
                      bodyType: 5DR WAGON
                      features:
                        headsUpDisplay: false
                        rainSensor: false
                        forwardCamera: false
                      pinnedTo: exact
                      ambiguous: false
                    coverage: full
                    branch:
                      code: WA-WELSHPOOL
                      name: Western Australia — Welshpool
                    matches:
                      - sku: TYRJ150-LFW-GBX
                        description: TY LC PRADO 150SER WINDSCREEN
                        glassPosition: F
                        confidence: catalogue
                        glassAttributes: [laminated]
                        adas:
                          calibrationLikely: false
                          basis: both
                        crossReferences:
                          eurocode: 8394AGNGNVZ
                          nagsau: ['TT1509']
                          oemNumbers: ['56101-60A00']
                          alternatePartNumbers: []
                          supersededBy: null
                        price:
                          unitExGst: { amount: '74.55', currency: AUD }
                          gstRate: '0.10'
                        availability:
                          status: in_stock
                          branchCode: WA-WELSHPOOL
                          quantityAvailable: 10
                          stockAsOf: '2026-08-10T06:30:00Z'
                        fittingMaterialSkus: [TYRJ150-FWM-LH, TYRJ150-FWM-RH]
                    fittingMaterials:
                      TYRJ150-FWM-LH:
                        sku: TYRJ150-FWM-LH
                        description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD LH
                        relationship: moulding
                        position: IFL
                        price:
                          unitExGst: { amount: '24.00', currency: AUD }
                          gstRate: '0.10'
                        availability:
                          status: in_stock
                          branchCode: WA-WELSHPOOL
                          quantityAvailable: 6
                          stockAsOf: '2026-08-10T06:30:00Z'
                      TYRJ150-FWM-RH:
                        sku: TYRJ150-FWM-RH
                        description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD RH
                        relationship: moulding
                        position: IFR
                        price:
                          unitExGst: { amount: '24.00', currency: AUD }
                          gstRate: '0.10'
                        availability:
                          status: in_stock
                          branchCode: WA-WELSHPOOL
                          quantityAvailable: 10
                          stockAsOf: '2026-08-10T06:30:00Z'
                nearMiss:
                  summary: No exact match — badge relaxed, and one question would settle the rest
                  description: |
                    We found nothing for the badge you sent, so we dropped `badge` and
                    re-ran. Both candidates fit the vehicle; they differ on a rain
                    sensor you left unknown, so we name the question that separates
                    them.

                    The rain-sensor variant in this example is illustrative — it shows
                    the shape, not a part you can order.
                  value:
                    vehicleResolved:
                      make: TOYOTA
                      model: LANDCRUISER PRADO
                      year: 2010
                      series: KDJ150R
                      badge: null
                      bodyType: 5DR WAGON
                      pinnedTo: series
                      ambiguous: true
                    coverage: full
                    relaxation:
                      relaxedOn:
                        - badge
                    branch:
                      code: WA-WELSHPOOL
                      name: Western Australia — Welshpool
                    matches:
                      - sku: TYRJ150-LFW-GBX
                        description: TY LC PRADO 150SER WINDSCREEN
                        glassPosition: F
                        confidence: catalogue
                        glassAttributes:
                          - laminated
                        price:
                          unitExGst:
                            amount: '74.55'
                            currency: AUD
                          gstRate: '0.10'
                        availability:
                          status: in_stock
                          branchCode: WA-WELSHPOOL
                          quantityAvailable: 10
                          stockAsOf: '2026-08-10T06:30:00Z'
                      - sku: TYRJ150-LFW-GBY
                        description: TY LC PRADO 150SER WINDSCREEN RAIN SENSOR
                        glassPosition: F
                        confidence: catalogue
                        glassAttributes:
                          - laminated
                          - rain_sensor
                        price:
                          unitExGst:
                            amount: '96.40'
                            currency: AUD
                          gstRate: '0.10'
                        availability:
                          status: out_of_stock
                          branchCode: WA-WELSHPOOL
                          quantityAvailable: 0
                          stockAsOf: '2026-08-10T06:30:00Z'
                    disambiguationHints:
                      - question: Is there a rain-sensor gel pad behind the mirror?
                        resolvesField: rainSensor
                        splitsMatches:
                          - TYRJ150-LFW-GBX
                          - TYRJ150-LFW-GBY
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/UnprocessableVehicle' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
  # ----------------------------------------------------------- Availability --

  /availability:
    post:
      tags: [Availability]
      operationId: getAvailability
      summary: /availability
      description: |
        ## Prices

        Every price we return is the **net price for the trade account your API key is
        issued to** — your pricing policy already applied. We don't publish a list
        price in this API: the number you see is the number that account pays.

        Prices are in Australian dollars, **ex GST**, with the GST rate alongside.
        Order responses additionally carry the GST total and inc-GST total we will
        actually bill.

        ## Limits

        We cap the request at **100 SKUs**. Go above the cap and we reject the whole
        request with `400` — we never silently truncate it. If a SKU is unknown or not
        purchasable we return it in `notFound` rather than dropping it, so you can tell
        "no stock" from "no such part".
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AvailabilityRequest'
            examples:
              windscreenAndMouldings:
                summary: Windscreen and both mouldings for a 150-series Prado
                value:
                  lines:
                    - sku: TYRJ150-LFW-GBX
                      quantity: 1
                    - sku: TYRJ150-FWM-LH
                      quantity: 1
                    - sku: TYRJ150-FWM-RH
                      quantity: 1
                  deliverTo:
                    postcode: '6104'
      responses:
        '200':
          description: Availability resolved.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AvailabilityResponse'
              example:
                branch:
                  code: WA-WELSHPOOL
                  name: Western Australia — Welshpool
                metroDelivery: true
                lines:
                  - sku: TYRJ150-LFW-GBX
                    quantity: 1
                    price:
                      unitExGst: { amount: '74.55', currency: AUD }
                      gstRate: '0.10'
                    availability:
                      status: in_stock
                      branchCode: WA-WELSHPOOL
                      quantityAvailable: 10
                      stockAsOf: '2026-08-10T02:15:00Z'
                notFound:
                  - sku: NOT-A-REAL-SKU
                    reason: unknown_sku
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
  # --------------------------------------------------------------- Delivery --

  /delivery-schedule:
    get:
      tags: [Delivery]
      operationId: getDeliverySchedule
      summary: /delivery-schedule
      description: |
        The delivery runs that serve the store your key is issued to, each with the
        order cut-off that puts an order on it. Times are the branch's local time.

        Version 1 serves our **WA (Welshpool)** branch only, and every response in
        this API is scoped to it — there is no branch selector. Whether the branch's
        run reaches a specific destination at all is `metroDelivery` on
        `POST /availability`.

        An order placed before a run's cut-off goes out on that run. The resolved run
        for a specific order is returned on the order as `fulfilment.expectedDelivery`.

        A store with no afternoon run has no `afternoon` entry. A store outside our
        delivery runs returns an empty `runs` array — the case `metroDelivery: false`
        reports on `POST /availability`.
      responses:
        '200':
          description: Your delivery schedule.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeliverySchedule'
              example:
                branch:
                  code: WA-WELSHPOOL
                  name: Western Australia — Welshpool
                runs:
                  - run: morning
                    deliveryWindow: { start: '10:00', end: '11:00' }
                    orderBy: { time: '16:30', dayPrior: true }
                  - run: afternoon
                    deliveryWindow: { start: '13:30', end: '14:30' }
                    orderBy: { time: '12:00', dayPrior: false }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
  # ----------------------------------------------------------------- Orders --

  /orders:
    post:
      tags: [Orders]
      operationId: createOrder
      summary: /orders
      description: |
        This places an order against your account. We send it to the serving branch,
        commit stock to it, and bill you on account.

        **Placing an order commits you to it.** There is no cancellation endpoint —
        see *Cancellation* below.

        ## What to send

        Your order lines (`sku` and `quantity`, up to 100), and how you want it fulfilled
        — `pickup`, or `local_delivery` to a postcode the branch's run reaches. You can
        also send your own job reference, the vehicle the glass is going into, and a
        price guard.

        `Idempotency-Key` is a **required header** here — see *Idempotency* below for how
        to generate one.

        ## Ordering on account

        **Every order through this API is on account.** There is no payment surface — you
        don't send card details and we don't take a payment. We bill the trade account
        your key resolves to, on that account's terms.

        We check three things on every order, at the moment you call: the account is
        **active**, it is on **approved payment terms**, and it is **not on credit
        hold**. Fail any one and we decline the order with `403` and create nothing —
        see *Errors and retries*.

        Two consequences:

        - **Availability is not entitlement.** A part can be in stock, priced and
          returned by `POST /availability`, and the order still declines. We check
          standing at order time only.
        - **A `403` here is not a client bug.** Don't retry it — surface it to the
          operator as "phone Globe Parts accounts".

        ## Pricing at order time

        We re-resolve prices at submission. Send `expectedTotalExGst` if you want a
        price-change guard.

        **The tolerance is ±0.02 AUD on the order total, excluding GST.** An absolute two
        cents either way, against the whole order — not per line, and not a percentage.
        Anything outside it and we reject the order with `409` and `problem.type`
        `price-changed`, and create nothing.

        The guard is opt-in. Leave `expectedTotalExGst` out and we price the order at
        submission and accept it, whatever the total.

        ## Idempotency

        `Idempotency-Key` is **required** — generate a UUID per order attempt and reuse
        it on every retry of that attempt. If a request times out, retry it with the
        same key.

        Keys are scoped to your account and retained for **30 days**. Replay a key
        that already succeeded and we return your original order with `200` and
        `Idempotency-Replayed: true`, creating nothing. Reuse a key with a
        **different** body — compared as canonicalised JSON, so key order and
        insignificant whitespace don't count — and we reject it with `409`
        `idempotency-key-reused`. Replay a key while the original attempt is still
        processing and we reject it with `409` `idempotency-key-in-flight` — retry
        after a short delay. A missing `Idempotency-Key` is `400` `validation-failed`.

        The key is echoed back as `submissionKey` on every order read.

        Don't derive the key from a job number or purchase-order reference — one job
        may legitimately carry more than one order.

        ## Cancellation

        We give you no cancellation endpoint. To change or cancel an order, phone us on
        **1800 764 994**. Once the branch processes it, you'll see `status: cancelled`
        on `GET /orders/{orderId}`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderRequest'
            examples:
              localDeliveryOrder:
                value:
                  partnerReference: EMPOS-J-88214
                  fulfilment:
                    method: local_delivery
                    deliveryAddress:
                      line1: 12 Example Road
                      suburb: Welshpool
                      state: WA
                      postcode: '6106'
                    instructions: Ring on arrival, roller door at rear.
                  lines:
                    - sku: TYRJ150-LFW-GBX
                      quantity: 1
                    - sku: TYRJ150-FWM-LH
                      quantity: 1
                    - sku: TYRJ150-FWM-RH
                      quantity: 1
                  vehicleContext:
                    registration: 1TAG123
                    state: WA
                    make: TOYOTA
                    model: LANDCRUISER PRADO
                    year: 2010
                  expectedTotalExGst:
                    amount: '122.55'
                    currency: AUD
      responses:
        '201':
          description: Order created.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
            Location:
              description: Canonical URL of the created order.
              schema: { type: string, format: uri }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              example:
                orderId: GP-2026-000418
                partnerReference: EMPOS-J-88214
                submissionKey: 8f14e45f-ea0c-4b1e-9f2a-6b3c1d2e4f50
                status: received
                placedAt: '2026-08-10T02:41:07Z'
                lineCount: 3
                totalExGst: { amount: '122.55', currency: AUD }
                totalIncGst: { amount: '134.81', currency: AUD }
                gstTotal: { amount: '12.26', currency: AUD }
                lines:
                  - sku: TYRJ150-LFW-GBX
                    description: TY LC PRADO 150SER WINDSCREEN
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '74.55', currency: AUD }
                    lineTotalExGst: { amount: '74.55', currency: AUD }
                  - sku: TYRJ150-FWM-LH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD LH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                    lineTotalExGst: { amount: '24.00', currency: AUD }
                  - sku: TYRJ150-FWM-RH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD RH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                    lineTotalExGst: { amount: '24.00', currency: AUD }
                fulfilment:
                  method: local_delivery
                  branchCode: WA-WELSHPOOL
                  deliveryAddress:
                    line1: 12 Example Road
                    suburb: Welshpool
                    state: WA
                    postcode: '6106'
                  expectedDelivery:
                    date: '2026-08-10'
                    run: afternoon
                    deliveryWindow: { start: '13:30', end: '14:30' }
        '200':
          description: |
            Idempotent replay — this key already created an order. The original order is
            returned unchanged and nothing new was created.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
            Idempotency-Replayed:
              description: Always `true` on a replay.
              schema: { type: boolean }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              example:
                orderId: GP-2026-000418
                partnerReference: EMPOS-J-88214
                submissionKey: 8f14e45f-ea0c-4b1e-9f2a-6b3c1d2e4f50
                status: received
                placedAt: '2026-08-10T02:41:07Z'
                lineCount: 3
                totalExGst: { amount: '122.55', currency: AUD }
                totalIncGst: { amount: '134.81', currency: AUD }
                lines:
                  - sku: TYRJ150-LFW-GBX
                    description: TY LC PRADO 150SER WINDSCREEN
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '74.55', currency: AUD }
                  - sku: TYRJ150-FWM-LH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD LH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                  - sku: TYRJ150-FWM-RH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD RH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                fulfilment:
                  method: local_delivery
                  branchCode: WA-WELSHPOOL
                  deliveryAddress:
                    line1: 12 Example Road
                    suburb: Welshpool
                    state: WA
                    postcode: '6106'
                  expectedDelivery:
                    date: '2026-08-10'
                    run: afternoon
                    deliveryWindow: { start: '13:30', end: '14:30' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/OrderConflict' }
        '422': { $ref: '#/components/responses/UnprocessableOrder' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

    get:
      tags: [Orders]
      operationId: listOrders
      summary: /orders
      description: |
        Orders are returned newest first — `placedAt` descending, `orderId` as the
        tiebreak.
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: status
          in: query
          description: |
            Filter by lifecycle status. An unrecognised value is rejected with `400`.
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - name: placedSince
          in: query
          description: Only orders placed at or after this instant.
          schema: { type: string, format: date-time }
        - name: partnerReference
          in: query
          description: Exact match on your own reference.
          schema: { type: string }
      responses:
        '200':
          description: A page of orders.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                type: object
                required: [orders, pagination]
                properties:
                  orders:
                    type: array
                    items:
                      $ref: '#/components/schemas/OrderSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
              example:
                orders:
                  - orderId: GP-2026-000418
                    partnerReference: EMPOS-J-88214
                    submissionKey: 8f14e45f-ea0c-4b1e-9f2a-6b3c1d2e4f50
                    status: completed
                    placedAt: '2026-08-10T02:41:07Z'
                    lineCount: 3
                    totalExGst: { amount: '122.55', currency: AUD }
                    totalIncGst: { amount: '134.81', currency: AUD }
                pagination:
                  page: 1
                  pageSize: 25
                  totalItems: 1
                  totalPages: 1
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /orders/{orderId}:
    get:
      tags: [Orders]
      operationId: getOrder
      summary: /orders/{orderId}
      description: |
        Poll this for fulfilment progress.
      parameters:
        - name: orderId
          in: path
          required: true
          schema: { type: string }
          example: GP-2026-000418
      responses:
        '200':
          description: The order.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
              example:
                orderId: GP-2026-000418
                partnerReference: EMPOS-J-88214
                submissionKey: 8f14e45f-ea0c-4b1e-9f2a-6b3c1d2e4f50
                status: completed
                placedAt: '2026-08-10T02:41:07Z'
                lineCount: 3
                totalExGst: { amount: '122.55', currency: AUD }
                totalIncGst: { amount: '134.81', currency: AUD }
                gstTotal: { amount: '12.26', currency: AUD }
                lines:
                  - sku: TYRJ150-LFW-GBX
                    description: TY LC PRADO 150SER WINDSCREEN
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '74.55', currency: AUD }
                    lineTotalExGst: { amount: '74.55', currency: AUD }
                  - sku: TYRJ150-FWM-LH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD LH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                    lineTotalExGst: { amount: '24.00', currency: AUD }
                  - sku: TYRJ150-FWM-RH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD RH
                    quantityOrdered: 1
                    unitPriceExGst: { amount: '24.00', currency: AUD }
                    lineTotalExGst: { amount: '24.00', currency: AUD }
                fulfilment:
                  method: local_delivery
                  branchCode: WA-WELSHPOOL
                  deliveryAddress:
                    line1: 12 Example Road
                    suburb: Welshpool
                    state: WA
                    postcode: '6106'
                  expectedDelivery:
                    date: '2026-08-10'
                    run: afternoon
                    deliveryWindow: { start: '13:30', end: '14:30' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
  # -------------------------------------------------------------- Catalogue --

  /products/{sku}:
    get:
      tags: [Catalogue]
      operationId: getProduct
      summary: /products/{sku}
      description: |
        Returns everything we hold on the part: cross-references (Eurocode, NAGS AU,
        OEM, obsolete and alternate numbers), glass attributes, and the glass
        dimensions.

        **The number doesn't have to be ours.** `supplier` names the vocabulary your
        number comes from — leave it off (or send `globe`) and we read the path as our
        own SKU; send `eurocode`, `nagsau`, `oem` or `obriens` and we translate your
        number to our part and return it.

        A number that translates to **more than one** of our parts is rejected with
        `422` and `problem.type` `ambiguous-part-number` — the `detail` names the
        candidate SKUs.
      parameters:
        - $ref: '#/components/parameters/Sku'
        - name: supplier
          in: query
          description: |
            The vocabulary the path's part number belongs to. Default `globe` — our own
            SKU. **Closed set** — branch on it exhaustively; a further vocabulary ships
            as an additive, optional value (you never receive this parameter, so adding
            one cannot break you).
          schema:
            type: string
            enum: [globe, eurocode, nagsau, oem, obriens]
            default: globe
      responses:
        '200':
          description: The product.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
              example:
                sku: TYRJ150-LFW-GBX
                description: TY LC PRADO 150SER WINDSCREEN
                productType: windscreen
                glassPosition: F
                manufacturingStandard: aftermarket
                glassAttributes: [laminated]
                glassSize: 1518*771
                crossReferences:
                  eurocode: 8394AGNGNVZ
                  nagsau: []
                  oemNumbers: ['56101-60A00']
                  alternatePartNumbers: []
                  supersededBy: null
                status: active
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422':
          description: |
            The supplied number translates to more than one of our parts. The `detail`
            names the candidate SKUs; fetch each by `sku`, or ask your operator.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://api.globeparts.com.au/problems/ambiguous-part-number
                title: Part number matches more than one part
                status: 422
                detail: 'OEM 56101-60A00 matches TYRJ150-LFW-GBX and TYRJ150-LFW-GBH. Fetch each by sku to choose.'
                instance: 01J9Z8Q3K7B2XN4V
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }

  /products/{sku}/fitting-materials:
    get:
      tags: [Catalogue]
      operationId: listFittingMaterials
      summary: /products/{sku}/fitting-materials
      description: |
        Returns the mouldings, rubbers, clips and brackets that go with a glass part.

        Each entry carries a `relationship`, distinguishing a moulding that is
        effectively mandatory from an optional accessory. We don't assert quantities.
      parameters:
        - $ref: '#/components/parameters/Sku'
      responses:
        '200':
          description: Fitting materials. May be an empty array.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            RateLimit: { $ref: '#/components/headers/RateLimit' }
          content:
            application/json:
              schema:
                type: object
                required: [sku, fittingMaterials]
                properties:
                  sku:
                    type: string
                    description: The glass part these materials accompany.
                  fittingMaterials:
                    type: array
                    items:
                      $ref: '#/components/schemas/FittingMaterial'
              example:
                sku: TYRJ150-LFW-GBX
                fittingMaterials:
                  - sku: TYRJ150-FWM-LH
                    description: TOYOTA LANDCRUISER PRADO 150SER 11/09- MOULD LH
                    relationship: moulding
                    position: IFL
                    price:
                      unitExGst: { amount: '24.00', currency: AUD }
                      gstRate: '0.10'
                    availability:
                      status: in_stock
                      branchCode: WA-WELSHPOOL
                      quantityAvailable: 6
                      stockAsOf: '2026-08-10T06:30:00Z'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '500': { $ref: '#/components/responses/InternalError' }
        '503': { $ref: '#/components/responses/ServiceUnavailable' }
  # ---------------------------------------------------------------- Service --

  /health:
    get:
      tags: [Service]
      operationId: getHealth
      summary: /health
      description: |
        An unauthenticated liveness and dependency check.
      security: []
      responses:
        '200':
          description: Healthy or degraded. `degraded` means orders may be accepted but
            availability figures could be stale.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: string
                    enum: [healthy, degraded]
                  pricingFreshAsOf:
                    type: string
                    format: date-time
                  stockFreshAsOf:
                    type: string
                    format: date-time
              example:
                status: healthy
                pricingFreshAsOf: '2026-08-10T02:00:00Z'
                stockFreshAsOf: '2026-08-10T02:15:00Z'
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503':
          description: Unhealthy — do not send traffic.
          headers:
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    const: unhealthy

# ============================================================== components ==
components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Globe-Api-Key
      description: |
        An API key we issue to you, transmitted over TLS on every
        request.

        Your key resolves to exactly one trade account. That binding determines the
        prices returned, the catalogue visible, and the branch served — there is no
        account parameter on any request, and you cannot address another account's
        data by any means.

        Keys are rotatable. Two keys may be active on one account during a rotation
        window so you can cut over without downtime.

        `401` and `403` are terminal — never retry them. See *Errors and retries* in the
        overview for the full retry policy.

  parameters:

    Sku:
      name: sku
      in: path
      required: true
      description: |
        Our part number, in its API rendering: where our internal code contains
        spaces or slashes they are rendered `-` (internal `TYRJ150 LFW/GBX` →
        `TYRJ150-LFW-GBX`). Invoices and cartons may show the internal form.
      schema:
        type: string
        minLength: 1
        maxLength: 50
      example: TYRJ150-LFW-GBX

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        A partner-generated UUID identifying this attempt. Reused on retries of the same
        attempt; never shared between two distinct intents. Retained for 30 days.
      schema:
        type: string
        format: uuid
      example: 8f14e45f-ea0c-4b1e-9f2a-6b3c1d2e4f50

    Page:
      name: page
      in: query
      description: 1-based page number.
      schema:
        type: integer
        minimum: 1
        default: 1

    PageSize:
      name: pageSize
      in: query
      description: |
        Items per page. Capped at 100. A value above the cap is rejected with `400`, not
        silently reduced.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25

  headers:

    X-Request-Id:
      description: |
        Correlation id for this request, echoed from the inbound `X-Request-Id` if the
        partner supplied one. Quote it when reporting a problem.
      schema: { type: string }

    RateLimit:
      description: |
        Remaining quota, in the style of the IETF
        `draft-ietf-httpapi-ratelimit-headers` structured field — for example
        `limit=60, remaining=58, reset=42`.

        Reflects whichever of the applicable limits is **nearest exhaustion**: the
        per-key per-minute rate, or the per-account daily budget. `reset` is seconds
        until that limit refreshes, so it may be a few seconds (minute window) or several
        hours (daily budget). Read the header rather than assuming which limit is binding.
      schema: { type: string }

    Retry-After:
      description: Seconds to wait before retrying.
      schema: { type: integer }

  # ------------------------------------------------------------- schemas ----

  schemas:

    # --- money and quantities ---

    GlassFitmentRequest:
      type: object
      required: [vehicle]
      properties:
        vehicle:
          $ref: '#/components/schemas/VehicleInput'
        glassPositions:
          type: array
          minItems: 1
          uniqueItems: true
          description: |
            Restrict results to these positions. Omit the property for every position we
            carry for the vehicle. An empty array is rejected with `400` — omission, not
            `[]`, is how you ask for everything.
          items:
            $ref: '#/components/schemas/GlassPosition'
        includeFittingMaterials:
          type: boolean
          default: false
          description: |
            When true the response carries a `fittingMaterials` map of the mouldings, rubbers and
            clips those matches need, and each match lists its own by SKU in
            `fittingMaterialSkus`. Deduplicated: a material shared across matches appears once.
      additionalProperties: false

    VehicleInput:
      type: object
      description: |
        The vehicle, in one of two forms. Exactly one of the `infoagentReport` or
        `attributes` properties must be present, matching `source` (`infoagent` or
        `attributes` respectively).
      required: [source]
      oneOf:
        - required: [infoagentReport]
          not:
            required: [attributes]
            properties:
              attributes: {}
          properties:
            source: { const: infoagent }
            infoagentReport: {}
        - required: [attributes]
          not:
            required: [infoagentReport]
            properties:
              infoagentReport: {}
          properties:
            source: { const: attributes }
            attributes: {}
      additionalProperties: false
      properties:
        source:
          type: string
          enum: [infoagent, attributes]
        infoagentReport:
          type: object
          description: |
            An InfoAgent IVDS report envelope, passed through verbatim as received —
            including the outer `data` wrapper. We read the properties below and
            ignore the rest, so you can forward the payload without stripping
            anything.

            **Light vehicles only.** Send the light-vehicle IVDS report
            (`data.type: IVDS_AU_ADAS_REPORT`). We don't serve heavy vehicles
            through this API — a report for one is rejected with `422`.
          required: [data]
          properties:
            data:
              type: object
              required: [result]
              properties:
                type:
                  type: string
                  description: |
                    The InfoAgent report type. We accept the light-vehicle report,
                    `IVDS_AU_ADAS_REPORT`.
                  examples: ['IVDS_AU_ADAS_REPORT']
                result:
                  type: object
                  required: [responseCode, vehicle]
                  properties:
                    responseCode:
                      type: string
                      description: |
                        InfoAgent's own outcome code, passed through untranslated.
                        Anything other than `SUCCESS` is rejected with `422` —
                        resolve the vehicle with InfoAgent first, then send us the
                        successful report.
                      examples: ['SUCCESS']
                    vehicle:
                      type: object
                      required: [details]
                      properties:
                        identification:
                          type: object
                          description: |
                            Plate, state and VIN. Recorded for warranty
                            traceability; never used to match.
                          properties:
                            plate: { type: [string, 'null'] }
                            state: { type: [string, 'null'] }
                            vin: { type: [string, 'null'] }
                          additionalProperties: true
                        details:
                          type: object
                          description: What we match on.
                          required: [make, model, year]
                          properties:
                            year: { type: integer }
                            make: { type: string }
                            model: { type: string }
                            subModel:
                              type: [string, 'null']
                              description: Read as our `badge`.
                            series: { type: [string, 'null'] }
                            bodyType: { type: [string, 'null'] }
                            doors: { type: [integer, 'null'] }
                          additionalProperties: true
                        specifications:
                          type: object
                          description: |
                            InfoAgent's equipment flags. We read the glass-relevant
                            ones — `informationDisplayHeadUp` →
                            `VehicleFeatures.headsUpDisplay`,
                            `rainSensorAutoWipers` → `rainSensor`,
                            `camerasADASOnly` → `forwardCamera` — and map each
                            onto the same tri-state.

                            The rule, applied to the string InfoAgent sends:
                            a value stating the feature is fitted reads as `true`;
                            one stating it is not fitted reads as `false`; and
                            **everything else reads as unknown** — `NOT_AVAILABLE`,
                            an absent property, `null`, and any value we do not
                            recognise. An unrecognised value is never an error.

                            Unknown widens the result set rather than silently
                            excluding a variant.

                            You do not need this mapping to call us — send the report
                            as InfoAgent gave it to you. It is stated so you can
                            predict which variants come back.
                          additionalProperties: true
                      additionalProperties: true
              additionalProperties: true
          additionalProperties: true
        attributes:
          $ref: '#/components/schemas/VehicleAttributes'

    VehicleAttributes:
      type: object
      description: An explicitly stated vehicle, for partners not using InfoAgent.
      required: [make, model, year]
      additionalProperties: false
      properties:
        make:
          type: string
          examples: ['TOYOTA']
        model:
          type: string
          examples: ['LANDCRUISER PRADO']
        year:
          type: integer
          minimum: 1900
          maximum: 2100
        series:
          type: [string, 'null']
          examples: ['KDJ150R']
        badge:
          type: [string, 'null']
          description: Variant or trim level. Equivalent to InfoAgent `subModel`.
          examples: ['GXL']
        bodyType:
          type: [string, 'null']
          description: |
            Preferred form is `{doors}DR {BODYTYPE}`, e.g. `5DR WAGON`. A bare body type
            is accepted and matched more loosely.
          examples: ['5DR WAGON']
        vin:
          type: [string, 'null']
          description: Recorded against the order for warranty traceability. Not used to match.
        features:
          $ref: '#/components/schemas/VehicleFeatures'

    VehicleFeatures:
      type: object
      description: |
        Glass-relevant vehicle features. These narrow the correct glass variant and drive
        the ADAS calibration flag on a match.

        Every field is tri-state: `true`, `false`, or absent meaning **unknown**. Absent is
        not `false`. An unknown forward camera widens the result set rather than silently
        excluding the camera variant.
      properties:
        headsUpDisplay: { type: boolean }
        rainSensor: { type: boolean }
        forwardCamera: { type: boolean }
        heatedScreen: { type: boolean }
        acoustic: { type: boolean }
        solarTint: { type: boolean }
      additionalProperties: false

    VehicleResolved:
      type: object
      description: |
        What we actually matched on, echoed back.
      required: [make, model, year, series, badge, bodyType, pinnedTo, ambiguous]
      properties:
        make: { type: string }
        model: { type: string }
        year: { type: integer }
        series: { type: [string, 'null'] }
        badge:
          type: [string, 'null']
          description: We looked and there is nothing on file for a vehicle matched without one.
        bodyType: { type: [string, 'null'] }
        features:
          $ref: '#/components/schemas/VehicleFeatures'
        pinnedTo:
          type: string
          enum: [exact, series, model_year]
          description: |
            How tightly the vehicle was pinned — the relaxation ladder read from the
            vehicle's side, one value per step:

            - `exact` — every attribute you sent was honoured, and `relaxation` is absent.
            - `series` — we relaxed `badge`; `relaxation.relaxedOn` is `["badge"]`.
            - `model_year` — we relaxed `badge` and then `series`;
              `relaxation.relaxedOn` is `["badge", "series"]`.

            `make`, `model`, `year` and `bodyType` are the confidence floor and never
            relax, so there is no step below `model_year`. This field and `relaxation`
            are the same fact stated twice over and always agree.
        ambiguous:
          type: boolean
          description: |
            More than one variant fits and the returned parts span them — the operator
            should confirm at the vehicle before ordering. A separate axis from
            `pinnedTo`: a vehicle can be pinned to `series` and still be ambiguous.

    # --- fitment results ---

    GlassFitmentResponse:
      type: object
      required: [vehicleResolved, coverage, matches, branch]
      properties:
        vehicleResolved:
          $ref: '#/components/schemas/VehicleResolved'
        coverage:
          type: string
          enum: [full, partial, none]
          description: |
            - `full` — we carry glass for every requested position.
            - `partial` — some positions had no catalogue coverage; see
              `uncoveredPositions`.
            - `none` — we resolved the vehicle but carry nothing for it.

            Assessed against the positions you asked for. When you omit
            `glassPositions` there is no requested set to fall short of, so the only
            values you can receive are `full` and `none`.
        uncoveredPositions:
          type: array
          uniqueItems: true
          description: |
            The requested positions we hold no glass for. Present only when `coverage`
            is `partial`, and therefore only when you sent `glassPositions`.
          items:
            $ref: '#/components/schemas/GlassPosition'
        branch:
          $ref: '#/components/schemas/BranchRef'
        relaxation:
          type: object
          description: |
            Present only when exact matching found nothing and we widened the search.
            It tells you, once for the whole response, that everything in `matches` is a
            near miss and what we relaxed to get there. It is the ONLY place this is
            stated — a match does not repeat it.

            When exact matching does find parts, we do not relax at all and this object
            is absent — we never mix exact and relaxed results in one `matches` array.
          required: [relaxedOn]
          properties:
            relaxedOn:
              type: array
              description: |
                The attributes we dropped, in the order we dropped them: `badge` first,
                then `series`, only when we hold no series records for the vehicle. We
                stop at the first step that returns parts.

                `make`, `model`, `year` and `bodyType` never appear here, and **`series`
                never appears without `badge`** — the only two values are `["badge"]`
                and `["badge", "series"]`.
              items:
                type: string
                enum: [badge, series]
        matches:
          type: array
          maxItems: 200
          description: |
            Ordered most useful first: the positions you asked for in the order you sent
            them (catalogue order when you sent none), then `confidence`
            `confirmed` before `catalogue` before `inferred`, then `sku`.

            **The ceiling is 200.** A vehicle bodied with many glass positions, asked for
            without a `glassPositions` filter, can exceed it. When it does we return the
            first 200 by that order and set `truncated: true` — narrow with
            `glassPositions` rather than paging. We never truncate silently.

            Empty (`[]`), never absent, when we resolved your vehicle and carry no glass
            for it — see `coverage: none`.
          items:
            $ref: '#/components/schemas/GlassMatch'
        truncated:
          type: boolean
          description: |
            Present only when we hit the `matches` ceiling and dropped parts that fit.
            Absent means the list is complete.
        disambiguationHints:
          type: array
          description: |
            The questions that would narrow the result, ordered most-discriminating
            first — for example "is a forward camera bracket present behind the mirror?".
            Present when `vehicleResolved.ambiguous` is true, and also when relaxation
            split the results across badges or series. Absent when there is no question
            worth asking.
          items:
            type: object
            required: [question, resolvesField]
            properties:
              question: { type: string }
              resolvesField:
                type: string
                enum: [headsUpDisplay, rainSensor, forwardCamera, heatedScreen,
                       acoustic, solarTint, badge, series]
                description: |
                  What an answer would settle. The first six are `VehicleFeatures`
                  properties; `badge` and `series` are the vehicle attributes the
                  relaxation ladder drops, so a badge-relaxed result can ask which
                  badge it is. Branch on which kind you got.
              splitsMatches:
                type: array
                description: |
                  The SKUs in `matches` that this question separates. A question is
                  raised only where the parts differ on something you did not send; we
                  do not ask you to confirm what you already sent.
                items: { type: string }
        fittingMaterials:
          type: object
          description: |
            The mouldings, rubbers and clips for everything in `matches`, keyed by SKU —
            each one appearing ONCE however many matches need it, so a shared moulding
            cannot report two different stock figures inside one response. Present only
            when `includeFittingMaterials` was true; `{}` when nothing in `matches`
            needs any.
          additionalProperties:
            $ref: '#/components/schemas/FittingMaterial'

    GlassMatch:
      type: object
      required: [sku, description, confidence, price, availability]
      properties:
        sku:
          type: string
          description: Our part number — the identifier used everywhere else in this API.
        description:
          type: string
        glassPosition:
          $ref: '#/components/schemas/GlassPosition'
        confidence:
          type: string
          enum: [confirmed, catalogue, inferred]
          description: |
            - `confirmed` — witnessed on this vehicle before.
            - `catalogue` — asserted by the fitment catalogue.
            - `inferred` — derived from a related variant; verify at the vehicle.
        glassAttributes:
          type: array
          description: |
            Attributes of this specific part, from our controlled vocabulary. **Closed
            set of 34**, each appearing at most once — the values and what they mean are
            on `GlassAttribute`.

            `adas` on a windscreen means the job needs a calibration; the `adas` object
            on this match states the basis.
          uniqueItems: true
          items:
            $ref: '#/components/schemas/GlassAttribute'
        adas:
          $ref: '#/components/schemas/AdasIndication'
          description: |
            Absent on a part that carries no driver-assistance hardware and sits on a
            vehicle we hold no ADAS data for — we have nothing to assert either way.
            Where we can assert, the object is present and `calibrationLikely` says
            which way. Absence is not `calibrationLikely: false`.
        crossReferences:
          $ref: '#/components/schemas/CrossReferences'
          description: |
            Absent on a part we hold no cross-reference record for at all. Present means
            the record exists, and every property inside it is then present — `null` or
            `[]` where that particular standard has nothing on file.
        price:
          $ref: '#/components/schemas/Price'
        availability:
          $ref: '#/components/schemas/Availability'
        fittingMaterialSkus:
          type: array
          uniqueItems: true
          description: |
            The materials this part needs, as keys into the response-level
            `fittingMaterials` map. Present only when `includeFittingMaterials` was true.
          items: { type: string }
        imageUrl:
          type: [string, 'null']
          format: uri

    AdasIndication:
      type: object
      description: |
        Whether this glass carries driver-assistance hardware that will need calibration
        after replacement.

        Treat this as an indication for your own workflow, not a compliance
        determination. We state what the part and the vehicle data you sent imply;
        whether a given vehicle requires recalibration, and who is qualified to perform
        it, remains the fitter's responsibility.
      required: [calibrationLikely]
      properties:
        calibrationLikely:
          type: boolean
        basis:
          type: string
          enum: [part_attributes, vehicle_features, both, unknown]
          description: What the indication rests on.
        notes:
          type: [string, 'null']

    CrossReferences:
      type: object
      description: |
        Equivalent identifiers, for matching against other catalogues.

        Note which of these are populated for a given part varies widely — a part with
        no eurocode on file returns `null`, not an error.

        Every property is always present: the singular ones carry `null` when nothing is
        on file, the arrays carry `[]`.
      required: [eurocode, nagsau, oemNumbers, alternatePartNumbers, supersededBy]
      properties:
        eurocode: { type: [string, 'null'] }
        nagsau:
          type: array
          description: |
            NAGS Australia part numbers for this glass. An array because one part
            commonly carries more than one NAGS number.

            NAGS **Australia** numbers only — not the NAGS North America numbering
            (`FW…`/`DW…`/`DD…`), which is a different scheme for a different market.
          items: { type: string }
          examples: [['TT1509']]
        oemNumbers:
          type: array
          items: { type: string }
        alternatePartNumbers:
          type: array
          description: |
            Equivalent numbers from standards not broken out above — competitor and
            our internal codes among them.
          items: { type: string }
        supersededBy:
          type: [string, 'null']
          description: Set when this part is obsolete and a replacement exists.

    AvailabilityRequest:
      type: object
      required: [lines]
      properties:
        lines:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku: { type: string, minLength: 1, maxLength: 50 }
              quantity:
                type: integer
                minimum: 1
                maximum: 9999
        deliverTo:
          type: object
          description: |
            Optional. Send the destination postcode and we'll tell you whether the
            branch's own delivery run reaches it — see `metroDelivery` on the response.
          properties:
            postcode:
              type: string
              pattern: '^\d{4}$'
      additionalProperties: false

    AvailabilityResponse:
      type: object
      required: [branch, lines, notFound]
      properties:
        branch:
          $ref: '#/components/schemas/BranchRef'
        metroDelivery:
          type: [boolean, 'null']
          description: |
            Whether the branch's own delivery run reaches the requested `deliverTo`
            postcode — that is, whether `local_delivery` may be selected for it. When
            `false`, the order must be placed as `pickup`, or delivery arranged by
            phoning the branch. Null when no `deliverTo` was supplied.
        lines:
          type: array
          items:
            type: object
            required: [sku, quantity, price, availability]
            properties:
              sku: { type: string }
              quantity: { type: integer }
              price:
                $ref: '#/components/schemas/Price'
              availability:
                $ref: '#/components/schemas/Availability'
        notFound:
          type: array
          description: |
            SKUs that could not be priced — unknown, inactive, or not purchasable on this
            account. Returned explicitly rather than omitted.
          items:
            type: object
            required: [sku, reason]
            properties:
              sku: { type: string }
              reason:
                type: string
                enum: [unknown_sku, not_purchasable, discontinued]

    Availability:
      type: object
      required: [status, branchCode, quantityAvailable, stockAsOf]
      properties:
        status:
          type: string
          enum: [in_stock, partial, out_of_stock]
          description: |
            Assessed against the quantity asked for; on `POST /fitment/glass`, against
            quantity 1. `partial` means some but not all of the requested quantity is
            on hand — so on `POST /fitment/glass`, where the quantity is always 1,
            `partial` never occurs and only `in_stock` and `out_of_stock` are returned.
        branchCode:
          type: string
          examples: ['WA-WELSHPOOL']
        quantityAvailable:
          type: integer
          minimum: 0
          description: |
            What you can order right now: stock net of quantities already promised to
            other orders. It is the figure `status` is assessed against.

            Indicative, not a reservation: stock is only committed when an order is
            placed.
        stockAsOf:
          type: string
          format: date-time
          description: When this stock figure was last refreshed from the source system.

    # --- orders ---

    OrderRequest:
      type: object
      required: [fulfilment, lines]
      properties:
        partnerReference:
          type: [string, 'null']
          maxLength: 100
          description: |
            Your own reference — typically your job number. We carry it through to
            the order and onto paperwork. Not an idempotency key and not required to be
            unique.
        fulfilment:
          $ref: '#/components/schemas/FulfilmentRequest'
        lines:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku: { type: string, minLength: 1, maxLength: 50 }
              quantity:
                type: integer
                minimum: 1
                maximum: 9999
              partnerLineReference:
                type: [string, 'null']
                maxLength: 100
        vehicleContext:
          type: object
          description: |
            Optional. Recorded against the order so a later warranty claim can be tied to
            the vehicle the glass went into. Never used for pricing or matching.
          properties:
            registration: { type: [string, 'null'] }
            state: { type: [string, 'null'] }
            vin: { type: [string, 'null'] }
            make: { type: [string, 'null'] }
            model: { type: [string, 'null'] }
            year: { type: [integer, 'null'] }
        expectedTotalExGst:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: |
            Optional price-change guard. The **goods total, ex GST** — the sum of the
            lines. There is no other chargeable component in version 1: delivery on the
            branch's own run is not separately charged through this API, and carrier
            despatch is not orderable through it at all.

            Send it and we compare it to the total we resolve at submission. **The
            tolerance is ±0.02 AUD, absolute, on this total** — outside that we reject
            the order with `409` and create nothing. Leave it out and there is no guard.
      additionalProperties: false

    FulfilmentRequest:
      type: object
      required: [method]
      properties:
        method:
          $ref: '#/components/schemas/FulfilmentMethod'
        deliveryAddress:
          allOf:
            - $ref: '#/components/schemas/Address'
          description: Required when `method` is `local_delivery`.
        instructions:
          type: [string, 'null']
          maxLength: 500

    FulfilmentMethod:
      type: string
      enum: [pickup, local_delivery]
      description: |
        - `pickup` — collected from the branch counter.
        - `local_delivery` — our own metro delivery run. Available only to postcodes
          the branch's run reaches. Check `metroDelivery` on `POST /availability`
          before selecting it — send `deliverTo` with your request and we answer it.

        **Version 1 serves these two methods only.** Deliveries beyond the branch's own
        run are arranged by phoning the branch; carrier despatch is not orderable through
        this API. **Fixed** — a further method ships as a new major version.

    Address:
      type: object
      required: [line1, suburb, state, postcode]
      properties:
        line1: { type: string }
        line2: { type: [string, 'null'] }
        suburb: { type: string }
        state:
          type: string
          enum: [WA, SA, NT, QLD, NSW, ACT, VIC, TAS]
        postcode:
          type: string
          pattern: '^\d{4}$'
        contactName: { type: [string, 'null'] }
        contactPhone: { type: [string, 'null'] }

    OrderStatus:
      type: string
      enum: [received, completed, cancelled]
      description: |
        Order lifecycle.

        - `received` — the sales order exists; processing has not started.
        - `completed` — the order has been processed, in part or in full. Line-level
          progress is not reported; an order with any processed line reports
          `completed`.
        - `cancelled` — the order was cancelled by the branch.

        **Closed set.** A finer-grained status would ship as a new major version.
      examples: ['received']

    OrderSummary:
      type: object
      required: [orderId, status, placedAt, totalExGst, totalIncGst]
      properties:
        orderId: { type: string }
        partnerReference: { type: [string, 'null'] }
        submissionKey:
          type: [string, 'null']
          description: |
            The `Idempotency-Key` this order was submitted under, echoed back. Null
            when the order was not placed through this API (phoned in, for instance).
        status:
          $ref: '#/components/schemas/OrderStatus'
        placedAt: { type: string, format: date-time }
        lineCount: { type: integer }
        totalExGst:
          $ref: '#/components/schemas/Money'
        totalIncGst:
          $ref: '#/components/schemas/Money'

    Order:
      allOf:
        - $ref: '#/components/schemas/OrderSummary'
        - type: object
          required: [lines, fulfilment]
          properties:
            lines:
              type: array
              items:
                $ref: '#/components/schemas/OrderLine'
            fulfilment:
              type: object
              required: [method]
              properties:
                method:
                  $ref: '#/components/schemas/FulfilmentMethod'
                branchCode: { type: string }
                deliveryAddress:
                  $ref: '#/components/schemas/Address'
                expectedDelivery:
                  type: [object, 'null']
                  description: |
                    For `local_delivery`: the run we expect this order to go out on,
                    resolved from your delivery schedule (`GET /delivery-schedule`)
                    against the cut-offs at the moment the order was placed. Null on
                    `pickup`, and when no run could be resolved. An expectation, not
                    a guarantee — if the branch reschedules, the branch's word is the
                    binding one.
                  required: [date, run, deliveryWindow]
                  properties:
                    date:
                      type: string
                      format: date
                    run:
                      type: string
                      enum: [morning, afternoon]
                    deliveryWindow:
                      $ref: '#/components/schemas/TimeWindow'
            gstTotal:
              $ref: '#/components/schemas/Money'
            updatedAt: { type: string, format: date-time }

    OrderLine:
      type: object
      required: [sku, quantityOrdered, unitPriceExGst]
      properties:
        sku: { type: string }
        description: { type: string }
        partnerLineReference: { type: [string, 'null'] }
        quantityOrdered: { type: integer }
        unitPriceExGst:
          $ref: '#/components/schemas/Money'
        lineTotalExGst:
          $ref: '#/components/schemas/Money'

    # --- reference ---

    Product:
      type: object
      description: |
        A part in our catalogue. Only parts visible to your account are
        returned; a part outside your catalogue is a `404`, not an empty record.
      required: [sku, description, productType]
      properties:
        sku:
          type: string
          description: Our part number. The identifier used everywhere in this API.
        description:
          type: string
        longDescription:
          type: [string, 'null']
        productType:
          type: string
          enum: [windscreen, side_window, fixed_window, sliding_window, insect_mesh,
                 fitting_material, accessory]
          description: |
            What kind of thing this is. **Closed set** — branch on it exhaustively; a
            value outside this list would be a new major version.

            Our name for each code, for your dropdowns:

            | Code | Name |
            |---|---|
            | `windscreen` | Front windscreen |
            | `side_window` | Side glass — door, quarter, vent, opera, cargo |
            | `fixed_window` | Fixed window |
            | `sliding_window` | Sliding window |
            | `insect_mesh` | Insect mesh |
            | `fitting_material` | Moulding, rubber, clip or bracket |
            | `accessory` | Accessory |

            Distinct from `glassPosition`, which states *where on the vehicle* the part
            sits. `productType` is the catalogue category; a part is typically both — a
            windscreen is `productType: windscreen` at `glassPosition: F`.
          examples: ['windscreen']
        glassPosition:
          $ref: '#/components/schemas/GlassPosition'
        glassAttributes:
          type: array
          description: |
            Attributes from our controlled vocabulary. **Closed set of 34** — the values
            and what they mean are on `GlassAttribute`.
          items:
            $ref: '#/components/schemas/GlassAttribute'
          examples: [['laminated', 'rain_sensor']]
        manufacturingStandard:
          type: [string, 'null']
          enum: [oe, oes, aftermarket, recycled, european_rv_glass, asian_rv_glass,
                 australian, none, unknown, null]
          description: |
            Glass tier. **Closed set** — branch on it exhaustively; a value outside this
            list would be a new major version.

            Three ways of saying "no grade":

            - `none` — the concept does not apply, as for a fitting material.
            - `unknown` — we hold the part but haven't graded it.
            - `null` — we don't carry the field for this part at all.

            Our name for each code, for labelling:

            | Code | Name |
            |---|---|
            | `oe` | Original Equipment |
            | `oes` | Original Equipment Supplier |
            | `aftermarket` | Aftermarket |
            | `recycled` | Recycled |
            | `european_rv_glass` | European RV glass |
            | `asian_rv_glass` | Asian RV glass |
            | `australian` | Australian |
            | `none` | No grade applies |
            | `unknown` | Not graded |
          examples: ['aftermarket']
        brand:
          type: [string, 'null']
        crossReferences:
          $ref: '#/components/schemas/CrossReferences'
        barcodes:
          type: array
          items: { type: string }
        glassSize:
          type: [string, 'null']
          description: |
            The glass dimensions as recorded, typically `{width}*{height}` in
            millimetres. Free text; null where not recorded.
          examples: ['1518*771']
        imageUrls:
          type: array
          items: { type: string, format: uri }
        status:
          type: string
          enum: [active, discontinued]

    FittingMaterial:
      type: object
      required: [sku, description, relationship]
      properties:
        sku: { type: string }
        description: { type: string }
        relationship:
          type: string
          enum: [moulding, rubber, clip, bracket, accessory]
        position:
          $ref: '#/components/schemas/GlassPosition'
          description: |
            Where it sits — a fitting position code (for example `IFT`, front
            windscreen moulding upper). **Omitted** when the catalogue records no
            position for the part, matching `GlassMatch.glassPosition`.
        price:
          $ref: '#/components/schemas/Price'
        availability:
          $ref: '#/components/schemas/Availability'

    # --- availability ---

    BranchRef:
      type: object
      required: [code, name]
      properties:
        code:
          type: string
          examples: ['WA-WELSHPOOL']
        name:
          type: string
          examples: ['Western Australia — Welshpool']

    DeliverySchedule:
      type: object
      required: [branch, runs]
      properties:
        branch:
          $ref: '#/components/schemas/BranchRef'
        runs:
          type: array
          description: |
            The runs that serve your store, in the order they leave. Empty when no
            run reaches you — order as `pickup`, or phone the branch.
          items:
            $ref: '#/components/schemas/DeliveryRun'

    DeliveryRun:
      type: object
      required: [run, deliveryWindow, orderBy]
      properties:
        run:
          type: string
          enum: [morning, afternoon]
        deliveryWindow:
          allOf:
            - $ref: '#/components/schemas/TimeWindow'
          description: When the run reaches your store.
        orderBy:
          type: object
          description: |
            The cut-off that puts an order on this run. `dayPrior: true` means the
            cut-off falls the day before the delivery.
          required: [time, dayPrior]
          properties:
            time:
              type: string
              pattern: '^([01]\d|2[0-3]):[0-5]\d$'
              examples: ['12:00']
            dayPrior:
              type: boolean

    TimeWindow:
      type: object
      description: |
        A time window in the branch's local time (`Australia/Perth`), 24-hour
        `HH:MM`.
      required: [start, end]
      properties:
        start:
          type: string
          pattern: '^([01]\d|2[0-3]):[0-5]\d$'
          examples: ['13:30']
        end:
          type: string
          pattern: '^([01]\d|2[0-3]):[0-5]\d$'
          examples: ['14:30']

    GlassPosition:
      type: string
      enum:
        # Glass openings — 36 codes.
        [F, FVL, FVR, FQL, FQR, FDL, FDR, FOL, FOR, RDL, RDR, RQL, RQR,
         RSL, RSR, RSL2, RSR2, RSL3, RSR3, FCL, FCR, MCL, MCR, MCL2, MCR2,
         RCL, RCR, RCL2, RCR2, RBDL, RBDR, RW, RWU, RWM, RWL, X,
         # Mouldings and fittings — 14 codes.
         IF, IFT, IFB, IFL, IFR, IR, IRU, IRL, ICL, IRS, IRP, IMS, IGF, IAE]
      description: |
        Where the part sits on the vehicle, as one of our position codes.

        **Closed set of 50** — branch on it exhaustively; a value outside this list would
        be a new major version.

        Our name for every code is tabled on `POST /fitment/glass` (*What the position
        codes mean*). Names are labels — we may reword one; codes are the contract.

        Code shape:

        - `F` is the windscreen; `RW` the rear window; `X` the roof.
        - An aperture code names the opening, then the side: `FD` + `R`/`L` for front
          door RH/LH, `RD` for rear door, `RQ` for rear door quarter, `RS` for rear side
          cargo.
        - A handful of repeated openings carry a numeric suffix, and **the numbering is
          not open-ended** — rear side cargo runs to 3 (`RSL3`/`RSR3`), mid and rear cargo
          stop at 2, and nothing else is numbered. There is no `RSR4`. Read the codes as
          a fixed list, not as a pattern you can construct.
        - Codes beginning `I` are mouldings and fittings rather than glass — `IF` front
          windscreen moulding (`IFT`/`IFB`/`IFL`/`IFR` for upper/lower/LH/RH), `ICL`
          windscreen clips, `IRS` rain-sensor bracket.

        A part whose position is not catalogued omits the field entirely. We never send a
        placeholder code to mean "we don't know" — absence carries that.
      examples: ['F', 'RW', 'FDR', 'RQL']

    GlassAttribute:
      type: string
      enum:
        # Features and hardware — 23 codes.
        [acoustic, adas, alarm_wire, antennae, tv_antennae, bottom_clips, bracket,
         brake_light, cowl_retainer, defogging_bracket, defogging_device,
         defogging_patch, encapsulation, gps, heater, hud, humidity_sensor, laminated,
         mirror_button, pins, rain_sensor, solar_pvb_interlayer, tempered,
         # Tints — 6 codes.
         bronze_tint, clear, green_tint, privacy_green_tint, privacy_grey_tint,
         solar_tint,
         # Mouldings — 3 codes.
         top_moulding, side_mouldings, bottom_moulding,
         # Other — 2 codes.
         vin_slot, water_repellent]
      description: |
        One property of a specific piece of glass.

        **Closed set of 34** — branch on it exhaustively; a value outside this list would
        be a new major version.

        Three groups. **Features and hardware** describe what is built into or bonded
        onto the glass — `adas`, `rain_sensor`, `hud`, `mirror_button`. **Tints**
        describe the glass itself. **Mouldings** say which trim ships attached.

        Tints are not mutually exclusive — a part can carry more than one, `clear` and
        `solar_tint` together for instance.

        A part that carries no attributes returns an empty array. We never send a
        placeholder member to mean "none" — `[]` carries that, and it is the only form
        you will receive.

        Our name for every code, for labelling. Names are labels — we may reword one;
        codes are the contract.

        | Code | Name | Code | Name |
        |---|---|---|---|
        | `acoustic` | Acoustic | `mirror_button` | Mirror button |
        | `adas` | ADAS | `pins` | Locating pins |
        | `alarm_wire` | Alarm wire | `rain_sensor` | Rain sensor |
        | `antennae` | Antennae | `solar_pvb_interlayer` | Solar PVB interlayer |
        | `tv_antennae` | TV antennae | `tempered` | Tempered |
        | `bottom_clips` | Bottom clips | `bronze_tint` | Bronze tint |
        | `bracket` | Bracket | `clear` | Clear |
        | `brake_light` | Brake light | `green_tint` | Green tint |
        | `cowl_retainer` | Cowl retainer | `privacy_green_tint` | Privacy green tint |
        | `defogging_bracket` | Defogging bracket | `privacy_grey_tint` | Privacy grey tint |
        | `defogging_device` | Defogging device | `solar_tint` | Solar tint |
        | `defogging_patch` | Defogging patch | `top_moulding` | Top moulding |
        | `encapsulation` | Encapsulation | `side_mouldings` | Side mouldings |
        | `gps` | GPS | `bottom_moulding` | Bottom moulding |
        | `heater` | Heater | `vin_slot` | VIN slot |
        | `hud` | Head-up display | `water_repellent` | Water repellent |
        | `humidity_sensor` | Humidity sensor | `laminated` | Laminated |
      examples: ['rain_sensor']

    Money:
      type: object
      description: |
        A monetary amount. `amount` is a decimal **string** to avoid binary
        floating-point rounding — parse it as a decimal type, never as a float.
      required: [amount, currency]
      properties:
        amount:
          type: string
          pattern: '^-?\d+\.\d{2}$'
          examples: ['74.55']
        currency:
          type: string
          enum: [AUD]
      additionalProperties: false

    Price:
      type: object
      description: |
        The net unit price for the authenticated account, **ex GST**, with the GST
        rate alongside.

        To derive an inc-GST figure, multiply and round to the nearest cent (an exact
        half-cent rounds up). The GST we bill is resolved at order time and returned
        on the order (`gstTotal`, `totalIncGst`).
      required: [unitExGst, gstRate]
      properties:
        unitExGst:
          $ref: '#/components/schemas/Money'
        gstRate:
          type: string
          pattern: '^\d+\.\d{2}$'
          description: GST rate applied, as a decimal string.
          examples: ['0.10']
        priceValidUntil:
          type: string
          format: date-time
          description: |
            After this instant the quoted price may change. Advisory — the binding price
            is always the one resolved at order submission.

    # --- vehicle ---

    Pagination:
      type: object
      required: [page, pageSize, totalItems, totalPages]
      properties:
        page: { type: integer }
        pageSize: { type: integer }
        totalItems: { type: integer }
        totalPages: { type: integer }

    # --- errors ---

    Problem:
      type: object
      description: |
        Error payload, per RFC 9457 Problem Details. Served as
        `application/problem+json`.

        Branch on `type`, never on `title` — titles are human-readable and may be
        reworded. `type` values are stable identifiers.
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri-reference
          description: Stable identifier for the problem kind.
          examples: ['https://api.globeparts.com.au/problems/price-changed']
        title:
          type: string
          description: Short human-readable summary. Not for programmatic branching.
        status:
          type: integer
        detail:
          type: string
          description: Explanation specific to this occurrence.
        instance:
          type: string
          description: The request id, matching the `X-Request-Id` header.
        errors:
          type: array
          description: Field-level validation failures, when applicable.
          items:
            type: object
            properties:
              pointer:
                type: string
                description: JSON Pointer to the offending field.
                examples: ['/lines/2/quantity']
              detail: { type: string }

  # ----------------------------------------------------------- responses ----

  responses:

    BadRequest:
      description: |
        The request is malformed or violates a documented limit — a batch above 100
        lines, a `pageSize` above the cap, a missing required field. Not retryable
        unchanged.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/batch-too-large
            title: Too many lines in one request
            status: 400
            detail: 'A maximum of 100 lines may be submitted per request; 143 were supplied. Split the request — it was not truncated.'
            instance: 01J9Z8Q3K7B2XN4V

    Unauthorized:
      description: |
        Missing, malformed or unrecognised API key. **Terminal — do not retry.**
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/unauthorized
            title: API key missing or not recognised
            status: 401
            detail: Send your key in the X-Globe-Api-Key header.

    Forbidden:
      description: >-
        Your key is valid but this account is not entitled to what you asked for.
        **Terminal — do not retry.** See *Problem types*.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/account-on-hold
            title: Account on hold
            status: 403
            detail: This account cannot place orders. Contact Globe Parts accounts.
            instance: 01J9Z8Q3K7B2XN4V

    NotFound:
      description: |
        No such resource, or it is not visible to this account.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/not-found
            title: No such order
            status: 404
            detail: Order GP-2026-000418 does not exist, or is not visible to this account.

    UnprocessableVehicle:
      description: |
        The request was well-formed but the vehicle could not be resolved — an InfoAgent
        envelope whose `responseCode` is not `SUCCESS`, or attributes too sparse to match.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/vehicle-unresolved
            title: Vehicle could not be resolved
            status: 422
            detail: The supplied InfoAgent report carries responseCode NO_MATCH.
            instance: 01J9Z8Q3K7B2XN4V

    UnprocessableOrder:
      description: |
        The order is well-formed but we cannot accept it as submitted. Nothing was
        created. Branch on `type` — see *Problem types*.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/fulfilment-unavailable
            title: Delivery unavailable to this destination
            status: 422
            detail: 'local_delivery was requested for postcode 6532, which the WA-WELSHPOOL delivery run does not reach. No order was created. Resubmit as pickup, or phone the branch.'
            instance: 01J9Z8Q3K7B2XN4V

    OrderConflict:
      description: |
        The request conflicts with current state. Distinguish by `type`:
        `idempotency-key-reused` (same key, different body — nothing created),
        `price-changed` (guard tripped — nothing created).
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/price-changed
            title: Price changed since quotation
            status: 409
            detail: 'Expected total 122.55 AUD ex GST; resolved total is 127.09 AUD ex GST. No order was created. Re-check availability and resubmit.'
            instance: 01J9Z8Q3K7B2XN4V

    TooManyRequests:
      description: Rate limit exceeded. Retryable after `Retry-After`.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
        RateLimit: { $ref: '#/components/headers/RateLimit' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/rate-limited
            title: Rate limit exceeded
            status: 429
            detail: Per-key limit of 60/minute reached. Retry after the Retry-After interval.

    InternalError:
      description: |
        Unexpected server error. Retryable with exponential backoff and jitter. For
        `POST /orders`, retry with the **same** `Idempotency-Key` — a duplicate order
        cannot result.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/internal-error
            title: Unexpected server error
            status: 500
            detail: Retry with backoff. For POST /orders, reuse the same Idempotency-Key.

    ServiceUnavailable:
      description: |
        A dependency is unavailable. Retryable after `Retry-After`. Check `GET /health`
        before resuming heavy traffic.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://api.globeparts.com.au/problems/unavailable
            title: A dependency is unavailable
            status: 503
            detail: Pricing is temporarily unreachable. Check GET /health before resuming.
