# GENERATED by `go run ./cmd/gen-docs`, edit a descriptor.go or docs/api/curated.yaml.
openapi: 3.1.0
info:
  title: Swoop API
  version: 1.1.0
  description: |
    The **Swoop API** is the partner-facing contract for integrating an ordering, delivery,
    or course system with Swoop's on-course food & beverage platform. A Swoop member orders
    from their phone — at the turn, on the range, or out on the course — and the order flows
    to the systems that fulfil it, with live status flowing back to the member.

    > **Access to this API requires a partner sandbox.** Email
    > [partners@swoopgolf.com](mailto:partners@swoopgolf.com) to be provisioned; see the
    > Getting Started guide in the navigation panel.

    ### Overview
    The Swoop APIs enable partners to programmatically manage locations, menus, orders, and
    on-course delivery on the Swoop platform. Build integrations that automate order
    processing, synchronize menus in real time, and hand ready orders to delivery operators.

    ### What You Can Build
    **Point-of-Sale (POS) integrations** — connect your POS to receive injected orders and
    report kitchen lifecycle automatically. This is the most common and valuable path.

    **Menu management** — keep menus synchronized with real-time pricing, availability, and
    86'ing.

    **Delivery operations** — drone, robot, or cart: take dispatches for ready orders and
    report vehicle-neutral mission progress with live position.

    **Multi-location management** — one integration serves every bound club location.

    ### API Overview
    This reference is organized into **API Suites**, one per domain:

    | API Suite | Purpose | Key Use Cases |
    |---|---|---|
    | Integration Configuration | Onboarding and setup | Credential exchange, webhook registration, mapping a location to your sites and markers |
    | Locations | Course topology | The club → outlet/site → on-course marker (delivery drop-off) model |
    | Order | Order processing | Order injection, status webhooks, cancel/void |
    | Menu | Menu and item management | Menu reads (items + modifier groups), availability / 86'ing (event + poll) |
    | Delivery | Dispatch operations | Mission create/abort/get, mission-event webhook, live position telemetry |
    | Operations | Health and eventing | Service health, how an order reaches the adapter, the shared acknowledgement / error model |

    ### Settlement
    Golfers settle at the **end of the round**, at the club. Orders injected by Swoop land
    **open and unpaid** — there is no in-app payment, and no payment data crosses this API.

    ### Conventions
    Correlate everything by the Swoop order id, carried as `swoop:<orderId>` wherever a string
    reference is needed. Webhook delivery is at-least-once; Swoop de-duplicates and guards
    against out-of-order events, so a stale event never walks an order's status backwards.

    ### Versioning
    This is **v1**. The API is versioned by **addition**: new fields and new paths are additive
    and backward-compatible, and a field is never removed or repurposed within a version. There
    is no prior public version, so there are no deprecation or "previous version" sections yet —
    when a breaking change is ever needed it will ship under a new version prefix. Changes are
    tracked in the [changelog](../guides/changelog.html).

    ### Tooling
    Besides the OpenAPI download above, a ready-to-run
    [Postman collection](../specs/swoop-api.postman_collection.json) is generated from this
    document — one request per operation, foldered by suite, example bodies filled in. Import
    it into Postman or Insomnia and point the base URL at your staging host.
servers:
  - url: https://adapter.swoopgolf.com
    description: Swoop adapter
security: []
tags:
  - name: Integration Configuration API Suite
    description: |-
      Onboarding-time contract: exchange credentials, register your webhook endpoints, and map a Swoop location to your sites. These shapes are provisioned/agreed at onboarding rather than called ad hoc.

      **In this suite:**

      - `PUT` [Register the integration (credentials + webhook endpoints)](#tag/integration-configuration-api-suite/PUT/integration/registration)
      - `PUT` [Map a Swoop location to your sites and markers](#tag/integration-configuration-api-suite/PUT/integration/locations/{locationId}/mapping)
  - name: Locations API Suite
    description: |-
      The club → outlet/site → on-course marker (delivery drop-off) model your integration reads to route orders and deliveries.

      **In this suite:**

      - `GET` [Get a location](#tag/locations-api-suite/GET/locations/{locationId})
      - `GET` [List on-course markers (delivery drop-off points)](#tag/locations-api-suite/GET/locations/{locationId}/markers)
  - name: Order API Suite
    description: |-
      The order-injection contract, the order-status webhook, and the cancel/void contract. Orders settle at the end of the round, so injected orders are open and unpaid, there is no payment block.

      **In this suite:**

      - `POST` [Report an order-status change](#tag/order-api-suite/POST/webhook/{integration}/{locationId})
      - `POST` [The order-injection contract](#tag/order-api-suite/POST/integration/orders)
      - `POST` [Cancel / void an order](#tag/order-api-suite/POST/integration/orders/{externalRef}/cancel)
  - name: Menu API Suite
    description: |-
      The menu read contract (items + modifier groups) and item availability / 86'ing, a push event and a poll.

      **In this suite:**

      - `GET` [Get the menu](#tag/menu-api-suite/GET/locations/{locationId}/menu)
      - `GET` [List modifier groups](#tag/menu-api-suite/GET/locations/{locationId}/menu/modifier-groups)
      - `GET` [Poll item availability (86'ing)](#tag/menu-api-suite/GET/locations/{locationId}/menu/availability)
      - `POST` [Report an item availability change (86 / restore)](#tag/menu-api-suite/POST/webhook/menu/{locationId})
  - name: Delivery API Suite
    description: |-
      Hand a ready order to a delivery operator: mission create / abort / get, the mission-event webhook, and live position telemetry.

      **In this suite:**

      - `POST` [Create a delivery mission](#tag/delivery-api-suite/POST/delivery/missions)
      - `POST` [Abort a delivery mission](#tag/delivery-api-suite/POST/delivery/missions/{missionId}/abort)
      - `GET` [Get mission status and live position](#tag/delivery-api-suite/GET/delivery/missions/{missionId})
      - `POST` [Report a mission event (and live position)](#tag/delivery-api-suite/POST/webhook/delivery/{locationId})
  - name: Operations API Suite
    description: |-
      Service liveness, how an order reaches the adapter (the internal orderPlaced push), and the acknowledgement / error model shared by every webhook.

      **In this suite:**

      - `POST` [Receive an orderPlaced event (Swoop-internal)](#tag/operations-api-suite/POST/pubsub/push)
      - `GET` [Liveness probe](#tag/operations-api-suite/GET/healthz)
x-tagGroups:
  - name: Swoop API Reference
    tags: [Integration Configuration API Suite, Locations API Suite, Order API Suite, Menu API Suite, Delivery API Suite, Operations API Suite]
paths:
  /webhook/{integration}/{locationId}:
    post:
      operationId: receiveStatusWebhook
      tags: [Order API Suite]
      summary: Report an order-status change
      description: |
        Your integration posts an order-status change here as the order advances. Swoop
        applies it to the member's order (persist + live update). Correlate by `externalRef`
        (`swoop:<orderId>`), echoed from the order Swoop injected.

        **Status buckets** — send your native status name; Swoop maps it onto the
        member-facing lifecycle: `received` → `in_progress` → `ready` → (for delivery)
        `en_route` → `completed`, plus `cancelled` / `failed` (a `failed` order surfaces to
        the member as cancelled; the `reason` is kept for club staff). Unmapped status names
        are acknowledged and ignored, never guessed.

        **Delivery semantics** — at-least-once is expected: retry any non-2xx with backoff.
        Swoop absorbs duplicates and out-of-order events (a monotonic guard means a stale
        event never walks status backwards). A `200` means "processed or safely discarded" —
        do not retry-loop on it. Events whose `externalRef` is not a Swoop order are ignored.
        The body is read up to a **1 MiB** limit; request headers are available for signature
        verification.

        **No webhooks?** If your POS can't push, Swoop can instead poll a status read you expose
        (arranged at onboarding) on an interval — the same status mapping applies, and Swoop
        stops polling once the order reaches a terminal status (`completed` / `cancelled` /
        `failed`).
      security:
        - pushToken: []
      parameters:
        - name: integration
          in: path
          required: true
          description: Your integration identifier, issued at onboarding.
          schema:
            type: string
            examples: [my-pos]
        - name: locationId
          in: path
          required: true
          description: The Swoop location (club) id the order belongs to.
          schema:
            type: string
            examples: ["51"]
        - name: token
          in: query
          required: true
          description: Shared-secret webhook token, issued at onboarding.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerStatusEvent"
            examples:
              in-progress:
                summary: Kitchen accepted — in progress
                description: The ticket is on the rail; `orderId` carries your POS-side id back for correlation.
                value:
                  externalRef: "swoop:31287"
                  orderId: "POS-55012"
                  status: IN_PROGRESS
                  occurredAt: "2026-06-22T10:16:00Z"
              ready:
                summary: Order is ready
                description: Ready for counter hand-off (pickup) or delivery dispatch. Send your native status name — Swoop maps it.
                value:
                  externalRef: "swoop:31287"
                  orderId: "POS-55012"
                  status: READY
                  reason: ""
                  occurredAt: "2026-06-22T10:21:00Z"
              cancelled:
                summary: Cancelled — with reason
                description: Expected on `cancelled`/`failed` so staff and the member see why.
                value:
                  externalRef: "swoop:31287"
                  orderId: "POS-55012"
                  status: VOIDED
                  reason: "voided at the till by staff"
                  occurredAt: "2026-06-22T10:24:00Z"
      responses:
        "200":
          description: Acknowledged (applied, or intentionally ignored).
        "400":
          description: Body could not be read.
        "401":
          description: Missing or invalid token.
        "500":
          description: Transient failure — please redeliver.
  /pubsub/push:
    post:
      operationId: receiveOrderPlaced
      tags: [Operations API Suite]
      summary: Receive an orderPlaced event (Swoop-internal)
      description: |
        Entry point for Swoop's `orderPlaced` event, delivered by a Google Pub/Sub **push**
        subscription. The adapter decodes the push envelope, re-fetches the authoritative
        order from swoop-api over GraphQL, canonicalizes it, and pushes it to the POS.

        > This endpoint is **internal to Swoop** (the orderPlaced topic is owned by Swoop).
        > It is documented here so partners understand how an order reaches the adapter, not
        > as an endpoint a partner calls. Processing is **idempotent** on the order id, so
        > Pub/Sub redelivery is safe.

        **Acknowledgement semantics:** `200` for success *and* for poison/non-`orderPlaced`
        messages (so Pub/Sub stops redelivering); `500` triggers redelivery.
      security:
        - pushToken: []
      parameters:
        - name: token
          in: query
          required: true
          description: Shared-secret push token (`PUBSUB_PUSH_TOKEN`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PubSubPushEnvelope"
      responses:
        "200":
          description: Acknowledged (processed, or dropped as poison/unrelated).
        "400":
          description: Body could not be read.
        "401":
          description: Missing or invalid token.
        "500":
          description: Processing failed — Pub/Sub will redeliver.
  /healthz:
    get:
      operationId: healthCheck
      tags: [Operations API Suite]
      summary: Liveness probe
      description: Returns `200 OK` when the service is up. No authentication.
      security: [] # public — no token required
      responses:
        "200":
          description: Service is healthy.
  # ── Order injection (the order Swoop delivers to your POS integration) ──
  /integration/orders:
    post:
      operationId: injectOrder
      tags: [Order API Suite]
      summary: The order-injection contract
      description: |
        **Direction: Swoop → your integration.** When a member checks out, Swoop builds this
        canonical order and hands it to your POS integration's create-order path. It is shown
        here as the **contract your integration receives**, not an endpoint you call on Swoop.

        Correlate by `externalRef` (`swoop:<orderId>`) — your idempotency key. A repeated
        injection with the same `externalRef` MUST resolve to the same order (a duplicate is
        success, not a second ticket). Items reference the item ids/SKUs you mapped to your POS
        at onboarding, and each line's `modifiers[].sku` is a mapped modifier-option id (see
        Menu → modifier groups); an order referencing an **unmapped** item or modifier is
        dead-lettered for staff, never pushed with a guess.

        **Settlement:** golfers settle at the **end of the round**. Injected orders land
        **open and unpaid** — there is no payment block and no card data in this contract.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrderInjection"
            examples:
              beverage-pickup:
                summary: Simple beverage order — counter pickup
                description: One item, collected at the turn. The `collectionCode` is shown to the member and printed on the docket.
                value:
                  externalRef: "swoop:30412"
                  locationId: "51"
                  customer: {name: "Alex Member"}
                  fulfillment: {type: pickup, destination: "Halfway House"}
                  collectionCode: "30412"
                  items:
                    - {sku: "2643", quantity: 2}
                  notes: ""
                  placedAt: "2026-06-22T09:41:00Z"
              multi-item-modifiers:
                summary: Multi-item order with modifiers
                description: Two lines with modifier options (mapped option ids on `modifiers[].sku`), a per-line note, and an age-restricted item.
                value:
                  externalRef: "swoop:31287"
                  locationId: "51"
                  customer: {name: "Jane Golfer"}
                  fulfillment: {type: pickup, destination: "Clubhouse Counter"}
                  collectionCode: "31287"
                  items:
                    - sku: "2417"
                      quantity: 1
                      modifiers: [{sku: "2102", name: "Large"}, {sku: "2118", name: "Add Cheese"}]
                      notes: "no onions"
                    - sku: "3105"
                      quantity: 2
                      ageRestricted: true
                  notes: ""
                  placedAt: "2026-06-22T10:14:00Z"
              delivery-to-marker:
                summary: Delivery order — on-course drop-off marker
                description: "`fulfillment.destination` is the on-course marker to run the order to; a ready order is then handed to the Delivery suite as one mission."
                value:
                  externalRef: "swoop:32160"
                  locationId: "51"
                  customer: {name: "Jane Golfer"}
                  fulfillment: {type: delivery, destination: "4th Hole"}
                  items:
                    - {sku: "2417", quantity: 2, modifiers: [{sku: "2118", name: "Add Cheese"}]}
                    - {sku: "2643", quantity: 1}
                  notes: "no ice"
                  placedAt: "2026-07-07T11:52:00Z"
      responses:
        "200":
          description: Your integration accepted the order and returns its POS-side order id.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: {type: string, description: Your POS-side order id.}
              example: {id: "POS-55012"}
  /integration/orders/{externalRef}/cancel:
    post:
      operationId: swoopCancelOrder
      tags: [Order API Suite]
      summary: Cancel / void an order
      description: |
        **Direction: Swoop → your integration.** When a member or club staff cancels, Swoop asks
        your integration to void the open order. Because golfers settle at the **end of the
        round**, the order is open and unpaid — voiding simply removes it; there is no refund.
        Idempotent on `externalRef`: voiding an already-voided or unknown order returns `200`.

        **Not every POS can void programmatically.** If yours has no void for this order type,
        return `501` (or declare cancel unsupported at onboarding) — Swoop then routes the order
        to **club staff** to void it in your back office, and the resulting `cancelled` status
        flows back through the order-status webhook. Swoop treats "can't cancel" as staff
        routing, never as a hard failure.
      parameters:
        - {name: externalRef, in: path, required: true, schema: {type: string}, description: "`swoop:<orderId>` of the order to void."}
      responses:
        "200": {description: Voided (or already absent).}
        "409":
          description: The order can no longer be voided (already settled/closed on your side).
          content:
            application/json:
              schema:
                type: object
                properties: {error: {type: string}}
              example: {error: "already_closed"}
        "501":
          description: This order type has no programmatic void — Swoop routes it to club staff, who void it manually (the `cancelled` status then arrives via the order-status webhook).
          content:
            application/json:
              schema:
                type: object
                properties: {error: {type: string}}
              example: {error: "cancel_unsupported"}
  # ── Delivery dispatch (hand a ready order to a delivery operator) ──
  /delivery/missions:
    post:
      operationId: createMission
      tags: [Delivery API Suite]
      summary: Create a delivery mission
      description: |
        **Direction: Swoop → your delivery API.** When a ready order is dispatched for
        delivery, Swoop creates one mission from the pickup site to the drop-off site. Shown
        here as the shape a delivery operator implements (or that Swoop maps to at onboarding).

        `externalRef` (`swoop:<orderId>`) is echoed on every event back and is the dedupe key:
        a repeated create with the same `externalRef` MUST resolve to the same mission. The
        load manifest carries item names + quantities only — never prices or payment data.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MissionRequest"
            example:
              externalRef: "swoop:32160"
              origin: {siteId: "site-pickup"}
              destination: {siteId: "site-drop-04", name: "4th Hole"}
              payload:
                items: [{name: "Burger", quantity: 2}, {name: "Sparkling Water", quantity: 1}]
                weightGrams: 1350
                description: "2x Burger, 1x Sparkling Water"
              recipient: {name: "Jane Golfer", phone: "+15555550123"}
              notes: "no ice"
      responses:
        "200":
          description: Mission accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MissionCreated"
              example: {id: "msn-77"}
  /delivery/missions/{missionId}/abort:
    post:
      operationId: abortMission
      tags: [Delivery API Suite]
      summary: Abort a delivery mission
      description: |
        **Direction: Swoop → your delivery API.** Called when a member cancels. Honoured only
        before the operator's point of no return; afterwards the operator refuses with a
        distinct non-2xx (e.g. `409`) and completes or returns the vehicle under its own
        authority — Swoop then routes the order to club staff. A terminal mission event still
        follows.
      parameters:
        - {name: missionId, in: path, required: true, schema: {type: string}}
      responses:
        "200": {description: Abort accepted; a terminal mission event follows.}
        "409":
          description: Past the point of no return — abort refused; the delivery completes or returns on its own authority.
          content:
            application/json:
              schema:
                type: object
                properties: {error: {type: string}}
              example: {error: "past_point_of_no_return"}
  /delivery/missions/{missionId}:
    get:
      operationId: getMission
      tags: [Delivery API Suite]
      summary: Get mission status and live position
      description: |
        **Direction: Swoop → your delivery API.** The optional poll seam for status and live
        telemetry when the operator cannot push events. Returns the mission's current status
        and, while en route, an optional live `position` that drives the vehicle marker on the
        member's order map. Without telemetry, tracking degrades to status milestones.
      parameters:
        - {name: missionId, in: path, required: true, schema: {type: string}}
      responses:
        "200":
          description: Current mission status, with an optional live position while en route.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Mission"
              example:
                missionId: "msn-77"
                externalRef: "swoop:32160"
                status: "in_transit"
                position: {lat: 40.813, lon: -74.174, altMeters: 60, heading: 214, etaSeconds: 95, timestamp: "2026-07-07T12:01:30Z"}
  /webhook/delivery/{locationId}:
    post:
      operationId: receiveMissionEvent
      tags: [Delivery API Suite]
      summary: Report a mission event (and live position)
      description: |
        **Direction: your delivery API → Swoop.** Post mission lifecycle events here as the
        mission progresses; Swoop maps them to the member's order status. Include the optional
        `position` object at a 2–5 s cadence while en route to drive the live vehicle marker.
        Same delivery semantics as the order-status webhook: at-least-once, dedupe, monotonic.
      security:
        - pushToken: []
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location id.}
        - {name: token, in: query, required: true, schema: {type: string}, description: Shared-secret webhook token.}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MissionEvent"
            examples:
              en-route-with-position:
                summary: En route — with live position
                description: Include `position` at a 2–5 s cadence while en route; it drives the vehicle marker on the member's order map.
                value:
                  missionId: "msn-77"
                  externalRef: "swoop:32160"
                  status: "in_transit"
                  occurredAt: "2026-07-07T12:00:00Z"
                  position: {lat: 40.813, lon: -74.174, altMeters: 60, heading: 214, etaSeconds: 95, timestamp: "2026-07-07T12:00:00Z"}
              delivered:
                summary: Delivered — terminal
                description: The terminal success event; Swoop marks the member's order completed.
                value:
                  missionId: "msn-77"
                  externalRef: "swoop:32160"
                  status: "delivered"
                  occurredAt: "2026-07-07T12:03:10Z"
              aborted:
                summary: Aborted — with reason
                description: A terminal abort; `reason` surfaces to club staff, and Swoop routes the order to a human runner or staff.
                value:
                  missionId: "msn-77"
                  externalRef: "swoop:32160"
                  status: "aborted"
                  reason: "wind above operational limit"
                  occurredAt: "2026-07-07T12:01:00Z"
      responses:
        "200": {description: Acknowledged.}
        "401": {description: Missing or invalid token.}
        "500": {description: Transient failure — please redeliver.}
  # ── Integration Configuration (onboarding-time contract; provisioned at onboarding) ──
  /integration/registration:
    put:
      operationId: swoopRegisterIntegration
      tags: [Integration Configuration API Suite]
      summary: Register the integration (credentials + webhook endpoints)
      description: |
        **Contract, provided at onboarding — not a self-serve endpoint.** Captures what Swoop and
        a partner exchange to connect: the credential your integration authenticates with, and the
        webhook endpoint(s) + shared secret Swoop calls back on (order status, delivery, menu).
        Documented so the shape is unambiguous; in practice these values are provisioned during
        onboarding. Secrets travel out of band, never in this body in production.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntegrationRegistration"
            example:
              integration: "my-pos"
              credential: {kind: bearer, note: "provisioned out of band"}
              statusWebhook: {url: "https://your-pos.example.com/swoop/status"}
              deliveryWebhook: {url: "https://your-pos.example.com/swoop/delivery"}
      responses:
        "200": {description: Registration accepted (echoed back without secrets).}
  /integration/locations/{locationId}/mapping:
    put:
      operationId: swoopPutLocationMapping
      tags: [Integration Configuration API Suite]
      summary: Map a Swoop location to your sites and markers
      description: |
        **Contract, provided at onboarding.** Binds a Swoop location (club) to your
        point-of-sale/site ids and maps each on-course marker to a delivery drop-off site.
        Unmapped markers fall back to a human runner; unmapped items dead-letter (see Orders).
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location (club) id.}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LocationMapping"
            example:
              posSiteId: "store-51"
              pickupSiteId: "site-clubhouse"
              markers:
                - {markerId: "368", dropoffSiteId: "site-drop-04"}
                - {markerId: "373", dropoffSiteId: "site-drop-09"}
      responses:
        "200": {description: Mapping stored.}
  # ── Locations (club → outlet/site → on-course marker model) ──
  /locations/{locationId}:
    get:
      operationId: swoopGetLocation
      tags: [Locations API Suite]
      summary: Get a location
      description: |
        The club/outlet/site record your integration is bound to — read it to confirm routing,
        the delivery pickup site, and the timezone. No member data.
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location (club) id.}
      responses:
        "200":
          description: The location.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Location"
              example:
                locationId: "51"
                name: "Sample Golf Club"
                timezone: "America/New_York"
                pickupSiteId: "site-clubhouse"
  /locations/{locationId}/markers:
    get:
      operationId: swoopGetLocationMarkers
      tags: [Locations API Suite]
      summary: List on-course markers (delivery drop-off points)
      description: |
        The course's on-course markers (holes / tees / counters) and the delivery drop-off site
        each maps to. A delivery order references a marker; a marker with no mapped drop-off site
        falls back to a human runner.
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location (club) id.}
      responses:
        "200":
          description: The location's markers.
          content:
            application/json:
              schema:
                type: object
                properties:
                  markers: {type: array, items: {$ref: "#/components/schemas/Marker"}}
              example:
                markers:
                  - {markerId: "368", name: "4th Hole", dropoffSiteId: "site-drop-04"}
                  - {markerId: "381", name: "Halfway House", dropoffSiteId: null}
  # ── Menu (read contract + availability / 86'ing) ──
  /locations/{locationId}/menu:
    get:
      operationId: swoopGetMenu
      tags: [Menu API Suite]
      summary: Get the menu
      description: |
        The menu Swoop maps orders against: items with a stable `sku`, display name, menu
        section (`category`), price, and active flag. `sku` is the id you map to your POS at
        onboarding and the join key for order injection (see Orders). Swoop models a location's
        menu as sections (categories) of items; this read returns the items flat, each carrying
        its `category`.
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location id.}
      responses:
        "200":
          description: The menu.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: {type: array, items: {$ref: "#/components/schemas/MenuItem"}}
              example:
                items:
                  - {sku: "2417", name: "Cheeseburger", category: "Mains", price: 12.5, active: true}
                  - {sku: "2643", name: "Sparkling Water", category: "Drinks", price: 3.0, active: true}
  /locations/{locationId}/menu/modifier-groups:
    get:
      operationId: swoopGetModifierGroups
      tags: [Menu API Suite]
      summary: List modifier groups
      description: |
        The menu's modifier groups and their choices, so Swoop can map member-facing options to
        the option ids your POS expects on an order line — the ids that come back as each
        `modifiers[].sku` on an injected order. Each group's `min`/`max` bound how many choices
        a member must / may pick (`0` = unbounded).

        **Every group is attached to exactly one menu item** — `itemSku` is the item it belongs
        to (join it against the menu read). "Modifier group" and "modifier" are the
        industry-standard names for what Swoop's own data model calls an **option** (the group,
        with `min`/`max`, owned by an item) and its **option items** (the choices) — same
        shapes, one vocabulary at the API boundary regardless of which POS is behind it.
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location id.}
      responses:
        "200":
          description: The location's modifier groups, each attached to its menu item.
          content:
            application/json:
              schema:
                type: object
                properties:
                  modifierGroups: {type: array, items: {$ref: "#/components/schemas/ModifierGroup"}}
              example:
                modifierGroups:
                  - id: "512"
                    itemSku: "2417"
                    name: "Size"
                    min: 1
                    max: 1
                    modifiers:
                      - {id: "2101", name: "Regular", price: 0}
                      - {id: "2102", name: "Large", price: 1.5}
                  - id: "883"
                    itemSku: "2417"
                    name: "Add-ons"
                    min: 0
                    max: 0
                    modifiers:
                      - {id: "2118", name: "Add Cheese", price: 1.0}
  /locations/{locationId}/menu/availability:
    get:
      operationId: swoopGetItemAvailability
      tags: [Menu API Suite]
      summary: Poll item availability (86'ing)
      description: |
        Current availability for menu items — the poll seam for kitchens that "86" (run out of) an
        item. Swoop hides unavailable items from members. Pair with the availability webhook for
        push updates.
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location id.}
      responses:
        "200":
          description: Availability snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  availability: {type: array, items: {$ref: "#/components/schemas/MenuAvailability"}}
              example:
                availability:
                  - {sku: "2417", available: true}
                  - {sku: "2643", available: false, updatedAt: "2026-06-22T10:00:00Z"}
  /webhook/menu/{locationId}:
    post:
      operationId: receiveMenuAvailabilityEvent
      tags: [Menu API Suite]
      summary: Report an item availability change (86 / restore)
      description: |
        **Direction: your integration → Swoop.** Push an availability change as a kitchen 86s or
        restores an item; Swoop updates what members can order. Same delivery semantics as the
        order-status webhook (at-least-once, dedupe). Authenticated by the shared `token`.
      security:
        - pushToken: []
      parameters:
        - {name: locationId, in: path, required: true, schema: {type: string}, description: The Swoop location id.}
        - {name: token, in: query, required: true, schema: {type: string}, description: Shared-secret webhook token.}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MenuAvailability"
            example: {sku: "2417", available: false, updatedAt: "2026-06-22T10:00:00Z"}
      responses:
        "200": {description: Acknowledged.}
        "401": {description: Missing or invalid token.}
components:
  securitySchemes:
    pushToken:
      type: apiKey
      in: query
      name: token
      description: |
        A shared-secret token (`PUBSUB_PUSH_TOKEN`) presented as the `token` query
        parameter on webhook and push deliveries. Requests with a missing or mismatched
        token are rejected with `401`. When no token is configured server-side, the check
        is skipped (local/dev only).
  schemas:
    Fulfillment:
      type: object
      description: Where and how the order is handed to the member.
      required: [type]
      properties:
        type:
          $ref: "#/components/schemas/FulfillmentType"
        destination:
          type: string
          description: |
            On-course delivery marker (e.g. `4th Hole`) when `type` is `delivery`, or the
            pickup counter name when `type` is `pickup`.
          examples: ["Halfway House", "4th Hole"]
    FulfillmentType:
      type: string
      description: How the order is fulfilled.
      enum: [delivery, pickup]
      x-enum-descriptions:
        delivery: On-course delivery to a marker (e.g. the 9th tee).
        pickup: Counter / turn pickup.
    MenuItem:
      type: object
      description: |
        A provider-agnostic menu item, used for menu reconciliation (`MenuReader`). `sku`
        is the stable id the adapter maps orders against — Swoop's own item id. `active`
        is the member-facing orderability flag (the inverse of Swoop's `disabled`).
      required: [sku, name, price, active]
      properties:
        sku:
          type: string
          examples: ["2417"]
        name:
          type: string
          examples: ["Cheeseburger"]
        description:
          type: string
          description: Member-facing item description, when the menu carries one.
        category:
          type: string
          description: The menu section the item belongs to (e.g. "Mains", "Drinks").
          examples: ["Mains"]
        price:
          type: number
          format: double
        active:
          type: boolean
    PubSubPushEnvelope:
      type: object
      description: |
        A Google Pub/Sub push delivery envelope. The adapter decodes `message.data`
        (base64) into the `orderPlaced` event and reads `message.attributes.type`.
      properties:
        subscription:
          type: string
        message:
          type: object
          properties:
            data:
              type: string
              description: Base64-encoded event payload.
            attributes:
              type: object
              additionalProperties:
                type: string
              description: Message attributes; `type` must be `orderPlaced`.
            messageId:
              type: string
            publishTime:
              type: string
              format: date-time
    # ─── Public Swoop API contracts (vendor-neutral; the shapes partners integrate) ───
    IntegrationRegistration:
      type: object
      description: |
        Onboarding registration — the credential + webhook endpoints Swoop and a partner
        exchange to connect. Secrets are provisioned out of band and never sent in this body in
        production; only the credential `kind` is described here.
      required: [integration]
      properties:
        integration: {type: string, description: Your integration identifier.}
        credential:
          type: object
          description: How your integration authenticates to Swoop's callbacks.
          properties:
            kind: {type: string, enum: [bearer, basic, hmac], description: Auth scheme (secret provisioned out of band).}
            note: {type: string}
        statusWebhook: {$ref: "#/components/schemas/WebhookEndpoint"}
        deliveryWebhook: {$ref: "#/components/schemas/WebhookEndpoint"}
        menuWebhook: {$ref: "#/components/schemas/WebhookEndpoint"}
    WebhookEndpoint:
      type: object
      description: A callback endpoint Swoop posts to, with its shared secret (the `token` query parameter).
      required: [url]
      properties:
        url: {type: string, format: uri}
        secret: {type: string, description: Shared secret presented as the `token` query parameter. Provisioned out of band.}
    LocationMapping:
      type: object
      description: Binds a Swoop location to your POS/site ids and maps on-course markers to delivery drop-off sites.
      properties:
        posSiteId: {type: string, description: Your POS/store id for this club.}
        pickupSiteId: {type: string, description: Delivery pickup site id (e.g. the clubhouse).}
        markers:
          type: array
          items: {$ref: "#/components/schemas/Marker"}
    Location:
      type: object
      description: |
        A Swoop location — a golf club's F&B ordering point, and the key everything on the Swoop
        side is addressed by: **every order for a location routes to one fulfilment station**
        (the kitchen/counter that prepares it), and the location's menu, markers, and webhooks
        are all read and registered per `locationId`. No member data.
      required: [locationId, name]
      properties:
        locationId: {type: string, description: "Stable Swoop location id — the key for every per-location read and webhook. All of this location's orders route to one station.", examples: ["51"]}
        name: {type: string, examples: ["Sample Golf Club"]}
        timezone: {type: string, examples: ["America/New_York"]}
        pickupSiteId: {type: string, description: Default delivery pickup site (e.g. the clubhouse).}
    Marker:
      type: object
      description: An on-course marker (hole / tee / counter) and the delivery drop-off site it maps to. A null drop-off site means runner-only.
      required: [markerId]
      properties:
        markerId: {type: string, examples: ["368"]}
        name: {type: string, examples: ["4th Hole"]}
        dropoffSiteId:
          type: ["string", "null"]
          description: Mapped delivery drop-off site id, or null for runner-only.
    MenuAvailability:
      type: object
      description: Availability of one menu item (86'ing). `available:false` hides the item from members.
      required: [sku, available]
      properties:
        sku: {type: string, description: The menu item's stable id (join key to your POS).}
        available: {type: boolean}
        reason: {type: string, description: "Optional reason the item is unavailable (e.g. \"out of stock\")."}
        updatedAt: {type: string, format: date-time}
    ModifierGroup:
      type: object
      description: |
        A choice set attached to **one** menu item (e.g. "Size", "Add-ons"); `min`/`max` bound
        how many choices a member must / may pick (`0` = unbounded). This is the
        industry-standard name for what Swoop's own data model calls an **option** — the
        contract renames, never reshapes.
      required: [id, itemSku, name]
      properties:
        id: {type: string, description: Stable group id (Swoop's option id)., examples: ["512"]}
        itemSku: {type: string, description: The menu item this group is attached to — every group belongs to exactly one item., examples: ["2417"]}
        name: {type: string, examples: ["Size"]}
        min: {type: integer, description: Minimum selections (0 = optional/unbounded).}
        max: {type: integer, description: Maximum selections (0 = unbounded).}
        modifiers:
          type: array
          items: {$ref: "#/components/schemas/ModifierOption"}
    ModifierOption:
      type: object
      description: |
        One selectable choice within a modifier group — Swoop's **option item**. `id` is the
        stable id an order line references as its modifier `sku` (see the order-injection
        contract).
      required: [id, name]
      properties:
        id: {type: string, examples: ["2102"]}
        name: {type: string, examples: ["Large"]}
        price: {type: number, format: double, description: "Added price, if any."}
    PartnerStatusEvent:
      type: object
      description: |
        An order-status change reported by your integration. `status` is your native status
        name; Swoop maps it onto its member-facing lifecycle (see the webhook operation).
      required: [externalRef, status]
      properties:
        externalRef: {type: string, description: "`swoop:<orderId>` — echoed from the injected order.", examples: ["swoop:31287"]}
        orderId: {type: string, description: "Your POS-side order id for this order, if you have one."}
        status: {type: string, description: "Your native order status; Swoop maps it to received/in_progress/ready/en_route/completed/cancelled/failed.", examples: [READY]}
        reason: {type: string, description: "Why the order failed or was cancelled — expected on `failed`/`cancelled` so staff and the member see the reason."}
        occurredAt: {type: string, format: date-time, description: When the change occurred.}
    OrderInjection:
      type: object
      description: |
        The canonical order Swoop delivers to a point-of-sale integration when a member checks
        out. PII-minimal (member name only) and **unpaid** — golfers settle at the end of the
        round, so there is no payment block.

        **Fulfillment.** For **delivery**, `fulfillment.destination` is the on-course marker to
        run the order to (or fly it to — see Delivery). For **pickup**, a short `collectionCode`
        accompanies the order for the counter / kitchen docket.
      required: [externalRef, locationId, items, placedAt]
      properties:
        externalRef: {type: string, description: "`swoop:<orderId>` — your idempotency key.", examples: ["swoop:31287"]}
        locationId: {type: string, description: "The Swoop location (club) id, routed to your integration — the same id every location-keyed read uses.", examples: ["51"]}
        customer: {$ref: "#/components/schemas/OrderCustomer"}
        fulfillment: {$ref: "#/components/schemas/Fulfillment"}
        collectionCode: {type: string, description: "Pickup only — a short code shown to the member and printed on the docket for hand-off at the counter. Derived from the Swoop order id.", examples: ["31287"]}
        items:
          type: array
          items: {$ref: "#/components/schemas/OrderItem"}
        notes: {type: string, description: Order-level instructions from the member.}
        placedAt: {type: string, format: date-time, description: When the member placed the order.}
      example:
        externalRef: "swoop:31287"
        locationId: "51"
        customer: {name: "Jane Golfer"}
        fulfillment: {type: delivery, destination: "4th Hole"}
        items:
          - sku: "2417"
            quantity: 1
            ageRestricted: false
            modifiers: [{sku: "2118", name: "Add Cheese"}]
            notes: "no onions"
        notes: ""
        placedAt: "2026-06-22T10:14:00Z"
    OrderCustomer:
      type: object
      description: The member behind the order — name only (PII-minimal).
      required: [name]
      properties:
        name: {type: string, examples: ["Jane Golfer"]}
    OrderItem:
      type: object
      description: One ordered line, keyed by the SKU you mapped to your POS at onboarding.
      required: [sku, quantity]
      properties:
        sku: {type: string, description: "The item id/SKU you mapped to your POS (Swoop's item id). An unmapped item dead-letters.", examples: ["2417"]}
        name: {type: string, description: "Display name at order time — for dockets and logs; `sku` is authoritative for mapping.", examples: ["Cheeseburger"]}
        quantity: {type: integer, minimum: 1}
        notes: {type: string, description: Per-line instructions from the member.}
        ageRestricted: {type: boolean, default: false, description: Alcohol or other age-restricted item; your POS may enforce an age check.}
        modifiers:
          type: array
          items: {$ref: "#/components/schemas/OrderModifier"}
    OrderModifier:
      type: object
      description: A chosen option on a line — the `sku` is a modifier-option id from the menu's modifier groups (Swoop's option-item id).
      required: [sku]
      properties:
        sku: {type: string, description: The option id/SKU you mapped to your POS modifier., examples: ["2118"]}
        name: {type: string, examples: ["Add Cheese"]}
    MissionRequest:
      type: object
      description: |
        A ready order handed to a delivery operator as one mission, pickup site → drop-off
        site. Carries a load manifest and a recipient contact only — never prices or payment
        data.
      required: [externalRef, origin, destination]
      properties:
        externalRef: {type: string, description: "`swoop:<orderId>` — echoed on every event; the dedupe key.", examples: ["swoop:32160"]}
        origin:
          type: object
          required: [siteId]
          properties:
            siteId: {type: string, description: Pickup site id (e.g. the clubhouse).}
        destination:
          type: object
          required: [siteId]
          properties:
            siteId: {type: string, description: Drop-off site id serving the order's on-course location.}
            name: {type: string, description: Human label for the operator's dispatch view.}
        payload:
          type: object
          description: Load manifest — names and quantities only.
          properties:
            items:
              type: array
              items:
                type: object
                required: [name, quantity]
                properties:
                  name: {type: string}
                  quantity: {type: integer, minimum: 1}
            weightGrams: {type: integer, description: Pre-flight weight estimate; the operator's scale is the authoritative gate.}
            description: {type: string, description: One-line manifest for the loader.}
        recipient:
          type: object
          description: Drop-off confirmation + coordination contact only.
          properties:
            name: {type: string}
            phone: {type: string}
        notes: {type: string, description: Instructions relevant to the hand-off.}
    MissionCreated:
      type: object
      description: Mission-create response.
      properties:
        id: {type: string, description: "The operator's mission id, correlated back via externalRef."}
    Mission:
      type: object
      description: Mission status, with an optional live position while en route.
      required: [status]
      properties:
        missionId: {type: string}
        externalRef: {type: string, examples: ["swoop:32160"]}
        status: {type: string, description: "The operator's native mission state (mapped to the member's order status).", examples: [in_transit]}
        reason: {type: string, description: Present on aborts/failures.}
        position: {$ref: "#/components/schemas/MissionPosition"}
    MissionPosition:
      type: object
      description: Live vehicle telemetry (optional). Throttled and de-duped by timestamp; published only between dispatch and the terminal event.
      properties:
        lat: {type: number, format: double}
        lon: {type: number, format: double}
        altMeters: {type: number}
        heading: {type: number, description: "Degrees, 0–360."}
        etaSeconds: {type: integer, description: Estimated seconds to drop-off.}
        timestamp: {type: string, format: date-time}
    MissionEvent:
      type: object
      description: A mission lifecycle event posted to Swoop, optionally carrying a live position.
      required: [externalRef, status]
      properties:
        missionId: {type: string}
        externalRef: {type: string, description: "`swoop:<orderId>` — echoed for correlation.", examples: ["swoop:32160"]}
        status: {type: string, description: Native mission state; mapped to the member's order status.}
        reason: {type: string, description: Present on aborts/failures.}
        occurredAt: {type: string, format: date-time}
        position: {$ref: "#/components/schemas/MissionPosition"}
