asyncapi: 3.0.0
info:
  title: TDS events binding
  version: 0.10.0
  description: 'Events a provider delivers to a requester''s webhook (SPEC.md §9, §12), and the exception
    events a broker delivers to its own subscribers. The requester registers a callback URL with `POST
    /subscriptions` (see openapi.yaml) and receives the signing secret once. Where the provider supports
    it, the requester later changes the event types or callback with `PATCH /subscriptions/{subscription_id}`
    (same secret) and ends delivery with `DELETE /subscriptions/{subscription_id}` (SPEC.md §9.3).


    **Delivery.** Each event is an HTTPS POST of one JSON event to the callback URL. Delivery is at least
    once with exponential backoff; receivers return 2xx quickly and dedupe on `event_id`. Order is not
    guaranteed; use `occurred_at`. Providers emit an event for every change, including changes the requester
    already saw in a synchronous response.


    **Signing (SPEC.md §12).** Two header sets carry the same HMAC-SHA256 protection. *Standard Webhooks*
    (standardwebhooks.com): `webhook-id` = `event_id`, `webhook-timestamp` = Unix seconds at send time,
    and `webhook-signature` = space-separated `v1,<base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw
    body>">`, one per active secret (several during secret rotation, newest first; a subscriber mints
    a new secret with `POST /subscriptions/{subscription_id}/secrets`, SPEC.md §12.1), keyed with the
    base64-decoded part of a `whsec_` secret. Senders SHOULD send them when the subscription''s secret
    is in `whsec_` form. *Legacy, deprecated since 0.6.0 but still always sent*: `X-TDS-Timestamp` (Unix
    seconds) and `X-TDS-Signature` = `sha256=` + lowercase hex HMAC-SHA256, keyed with the UTF-8 bytes
    of the whole secret, of `<X-TDS-Timestamp>.<raw request body>`. Receivers accept a delivery when either
    signature verifies and its timestamp is within 300 seconds of their clock, and reject it otherwise.


    **CloudEvents (optional).** A subscription created with `cloudevents: true` gets CloudEvents 1.0 binary-content-mode
    headers on every delivery (SPEC.md §9.1): `ce-specversion` `1.0`, `ce-id` = `event_id`, `ce-source`
    = `urn:tds:provider:<provider_id>`, `ce-type` = `org.tds-openapi.` + the event type in lower case,
    `ce-time` = `occurred_at`, `ce-subject` = `trips/<trip_id>[/legs/<leg_id>]`, `ce-dataschema` = the
    event schema at `https://tds-openapi.org/schemas/<schema_version>/...`, and the extensions `ce-providerid`
    and `ce-schemaversion`. The body and `Content-Type: application/json` are unchanged. Receivers dedupe
    on (`provider_id`, `event_id`), which is CloudEvents (`source`, `id`).'
  contact:
    name: tds-openapi maintainers
    url: https://github.com/kovirlabs/tds-openapi
  license:
    name: Apache-2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
  tags:
  - name: core
    description: Core trip events.
  - name: transfer
    description: Transfer profile events.
defaultContentType: application/json
channels:
  subscriberCallback:
    address: null
    title: Subscriber callback
    description: The callback_url given in the subscription. Events are POSTed here.
    messages:
      TripStatusChanged:
        $ref: '#/components/messages/TripStatusChanged'
      TripScheduled:
        $ref: '#/components/messages/TripScheduled'
      TripCancelled:
        $ref: '#/components/messages/TripCancelled'
      TenderResponse:
        $ref: '#/components/messages/TenderResponse'
      HoldResponse:
        $ref: '#/components/messages/HoldResponse'
      Milestone:
        $ref: '#/components/messages/Milestone'
      EtaUpdate:
        $ref: '#/components/messages/EtaUpdate'
      CustodyHandoff:
        $ref: '#/components/messages/CustodyHandoff'
      ExceptionRaised:
        $ref: '#/components/messages/ExceptionRaised'
      ExceptionResolved:
        $ref: '#/components/messages/ExceptionResolved'
operations:
  deliverTripStatusChanged:
    action: send
    title: Deliver TRIP_STATUS_CHANGED
    description: Provider POSTs a TRIP_STATUS_CHANGED event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/TripStatusChanged'
  deliverTripScheduled:
    action: send
    title: Deliver TRIP_SCHEDULED
    description: Provider POSTs a TRIP_SCHEDULED event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/TripScheduled'
  deliverTripCancelled:
    action: send
    title: Deliver TRIP_CANCELLED
    description: Provider POSTs a TRIP_CANCELLED event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/TripCancelled'
  deliverTenderResponse:
    action: send
    title: Deliver TENDER_RESPONSE
    description: Provider POSTs a TENDER_RESPONSE event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/TenderResponse'
  deliverHoldResponse:
    action: send
    title: Deliver HOLD_RESPONSE
    description: Provider POSTs a HOLD_RESPONSE event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/HoldResponse'
  deliverMilestone:
    action: send
    title: Deliver MILESTONE
    description: Provider POSTs a MILESTONE event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/Milestone'
  deliverEtaUpdate:
    action: send
    title: Deliver ETA_UPDATE
    description: Provider POSTs a ETA_UPDATE event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/EtaUpdate'
  deliverCustodyHandoff:
    action: send
    title: Deliver CUSTODY_HANDOFF
    description: Provider POSTs a CUSTODY_HANDOFF event to the subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/CustodyHandoff'
  deliverExceptionRaised:
    action: send
    title: Deliver EXCEPTION_RAISED
    description: A broker POSTs an EXCEPTION_RAISED event to its own subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/ExceptionRaised'
  deliverExceptionResolved:
    action: send
    title: Deliver EXCEPTION_RESOLVED
    description: A broker POSTs an EXCEPTION_RESOLVED event to its own subscriber's callback URL.
    channel:
      $ref: '#/channels/subscriberCallback'
    messages:
    - $ref: '#/channels/subscriberCallback/messages/ExceptionResolved'
components:
  messageTraits:
    signed:
      headers:
        $ref: '#/components/schemas/DeliveryHeaders'
  schemas:
    DeliveryHeaders:
      description: 'HTTP headers of one webhook delivery: the legacy signature headers (always), the Standard
        Webhooks headers (when the secret is in `whsec_` form) and, for subscriptions with `cloudevents:
        true`, the CloudEvents binary-mode headers (SPEC.md §9.1, §12).'
      type: object
      allOf:
      - $ref: '#/components/schemas/SignatureHeaders'
      - $ref: '#/components/schemas/StandardWebhooksHeaders'
      - $ref: '#/components/schemas/CloudEventsHeaders'
    SignatureHeaders:
      description: Legacy webhook signature headers (SPEC.md §12). Deprecated since 0.6.0 in favour of
        the Standard Webhooks headers, but senders still send them on every delivery; removal needs a
        major version and an RFC.
      type: object
      required:
      - X-TDS-Signature
      - X-TDS-Timestamp
      properties:
        X-TDS-Signature:
          description: sha256=<lowercase hex HMAC-SHA256 of "<X-TDS-Timestamp>.<raw body>">, keyed with
            the UTF-8 bytes of the subscription's signing secret (the newest one during rotation).
          type: string
          pattern: ^sha256=[0-9a-f]{64}$
          deprecated: true
        X-TDS-Timestamp:
          description: Unix seconds when the request was sent.
          type: string
          pattern: ^[0-9]+$
          deprecated: true
    StandardWebhooksHeaders:
      description: Standard Webhooks signature headers (standardwebhooks.com, SPEC.md §12), sent together
        or not at all. Senders SHOULD send them when the signing secret is `whsec_` + base64 of 24 to
        64 random bytes.
      type: object
      dependentRequired:
        webhook-signature:
        - webhook-id
        - webhook-timestamp
        webhook-id:
        - webhook-timestamp
        - webhook-signature
      properties:
        webhook-id:
          description: The envelope's `event_id`; the same on every retry.
          type: string
          minLength: 1
        webhook-timestamp:
          description: Unix seconds when this attempt was sent.
          type: string
          pattern: ^[0-9]+$
        webhook-signature:
          description: Space-separated signatures, each `v1,` + base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw
            body>", keyed with the base64-decoded part of a `whsec_` secret. One per active secret during
            rotation (SPEC.md §12.1), newest first; a secret past its expires_at is not used. Receivers
            ignore versions they do not know (e.g. `v1a`).
          type: string
          pattern: ^v1a?,[A-Za-z0-9+/=]+( v1a?,[A-Za-z0-9+/=]+)*$
    CloudEventsHeaders:
      description: 'CloudEvents 1.0 binary content mode (HTTP protocol binding). Derived from the envelope
        in the body, which stays the TDS event. Either all of these are sent (`ce-subject` only when the
        event has a `trip_id`) or none. Values are percent-encoded as the CloudEvents HTTP binding requires.
        Binary mode carries `datacontenttype` as `Content-Type: application/json`, never as a `ce-` header.'
      type: object
      dependentRequired:
        ce-id:
        - ce-specversion
        - ce-source
        - ce-type
        - ce-time
        - ce-dataschema
        - ce-providerid
        - ce-schemaversion
      properties:
        ce-specversion:
          description: CloudEvents version.
          type: string
          const: '1.0'
        ce-id:
          description: The envelope's `event_id`, stable across retries.
          type: string
          minLength: 1
        ce-source:
          description: '`urn:tds:provider:` followed by the envelope''s `provider_id`, percent-encoded
            as a URI component. (`source`, `id`) is unique per event; receivers dedupe on it.'
          type: string
          pattern: ^urn:tds:provider:.+$
        ce-type:
          description: '`org.tds-openapi.` followed by the envelope''s `event_type` in lower case, e.g.
            `org.tds-openapi.trip_status_changed`. Upper-casing the suffix gives the event type back.'
          type: string
          pattern: ^org\.tds-openapi\.[a-z][a-z0-9_]*$
        ce-time:
          description: The envelope's `occurred_at` (RFC 3339).
          type: string
        ce-subject:
          description: '`trips/<trip_id>` or `trips/<trip_id>/legs/<leg_id>` (ids percent-encoded as URI
            path segments); absent when the event has no `trip_id`.'
          type: string
          pattern: ^trips/[^/]+(/legs/[^/]+)?$
        ce-dataschema:
          description: 'The event type''s schema, versioned by the envelope''s `schema_version`: `https://tds-openapi.org/schemas/<schema_version>/<profile>/<schema>.yaml`.
            Removing the version segment gives the schema''s `$id`.'
          type: string
          format: uri
        ce-providerid:
          description: Extension attribute; the envelope's `provider_id`.
          type: string
        ce-schemaversion:
          description: Extension attribute; the envelope's `schema_version`.
          type: string
          pattern: ^[0-9]+\.[0-9]+\.[0-9]+$
    event:
      title: Event
      description: 'Envelope of every webhook event (SPEC.md §9). Providers emit an event for every change
        they make, including changes the requester already saw in a synchronous response. Delivery is
        at least once: receivers dedupe on event_id. Each event type has its own schema fixing `event_type`
        and `payload`.'
      type: object
      required:
      - event_id
      - event_type
      - occurred_at
      - provider_id
      - schema_version
      - payload
      properties:
        event_id:
          description: Unique id of this event, stable across delivery retries.
          type: string
        event_type:
          description: Which event this is; selects the payload schema.
          type: string
          pattern: ^[A-Z][A-Z0-9_]*$
        occurred_at:
          description: When the change happened at the provider, RFC 3339 with offset.
          type: string
          format: date-time
        provider_id:
          description: 'Party that emitted the event: the provider, or for broker events (EXCEPTION_RAISED,
            EXCEPTION_RESOLVED) the broker.'
          type: string
        trip_id:
          description: Requester-assigned trip id the event concerns, if any.
          type: string
        leg_id:
          description: Leg the event concerns, if any.
          type: string
        schema_version:
          description: Version of this binding the payload follows (SemVer).
          type: string
          pattern: ^[0-9]+\.[0-9]+\.[0-9]+$
        payload:
          description: Event-specific content.
          type: object
      examples:
      - event_id: evt-0001
        event_type: TRIP_STATUS_CHANGED
        occurred_at: '2026-11-03T09:05:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261103-0001
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          status: IN_PROGRESS
          previous_status: SCHEDULED
    trip-status:
      title: TripStatus
      description: 'Lifecycle state of a trip (SPEC.md §8.1). Terminal states: COMPLETED, FAILED, REJECTED,
        EXPIRED, CANCELLED, NO_SHOW. Allowed transitions are enforced by the conformance kit, not by this
        schema.'
      type: string
      enum:
      - REQUESTED
      - OFFERED
      - CONFIRMED
      - SCHEDULED
      - IN_PROGRESS
      - COMPLETED
      - FAILED
      - REJECTED
      - EXPIRED
      - CANCELLED
      - NO_SHOW
      examples:
      - REQUESTED
      - SCHEDULED
    event-trip-status-changed:
      title: TripStatusChangedEvent
      description: Emitted on every trip status change.
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      properties:
        event_type:
          description: Always TRIP_STATUS_CHANGED.
          const: TRIP_STATUS_CHANGED
        payload:
          description: The new and previous status.
          type: object
          required:
          - status
          properties:
            status:
              description: Status after the change.
              $ref: '#/components/schemas/trip-status'
            previous_status:
              description: Status before the change; absent for the first event of a trip.
              $ref: '#/components/schemas/trip-status'
      examples:
      - event_id: evt-0001
        event_type: TRIP_STATUS_CHANGED
        occurred_at: '2026-11-03T09:05:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261103-0001
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          status: IN_PROGRESS
          previous_status: SCHEDULED
    vehicle-ref:
      title: VehicleRef
      description: Identifies the vehicle assigned to a leg so the rider or an attendant can recognise
        it. Typically set by the provider shortly before pickup.
      type: object
      required:
      - vehicle_id
      properties:
        vehicle_id:
          description: Provider's opaque vehicle identifier.
          type: string
        description:
          description: Rider-facing description, e.g. "White Ford Transit,
          type: string
          maxLength: 200
        plate:
          description: Licence plate as displayed on the vehicle.
          type: string
          maxLength: 20
      examples:
      - vehicle_id: veh-214
        description: 'White Ford Transit, #214'
        plate: ABC1234
    event-trip-scheduled:
      title: TripScheduledEvent
      description: Emitted when a trip's committed times or assigned vehicle are set or change.
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      - leg_id
      properties:
        event_type:
          description: Always TRIP_SCHEDULED.
          const: TRIP_SCHEDULED
        payload:
          description: The leg's current commitments.
          type: object
          required:
          - scheduled_pickup
          properties:
            scheduled_pickup:
              description: Committed pickup time.
              type: string
              format: date-time
            scheduled_dropoff:
              description: Committed dropoff time, if the provider commits to one.
              type: string
              format: date-time
            vehicle_ref:
              description: Assigned vehicle, once known.
              $ref: '#/components/schemas/vehicle-ref'
      examples:
      - event_id: evt-0002
        event_type: TRIP_SCHEDULED
        occurred_at: '2026-11-02T16:40:05-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261103-0001
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          scheduled_pickup: '2026-11-03T09:20:00-05:00'
          vehicle_ref:
            vehicle_id: veh-214
    cancel-reason:
      title: CancelReason
      description: Reason code for a cancellation. Codes, not free text, so events carry no personal information.
        MISSED_CONNECTION is used when an upstream leg in a transfer failed.
      type: string
      enum:
      - RIDER_REQUEST
      - PROVIDER_UNABLE
      - DUPLICATE
      - MISSED_CONNECTION
      - OTHER
      examples:
      - RIDER_REQUEST
    cancellation:
      title: Cancellation
      description: Record of a trip's cancellation, set by the provider when the trip becomes CANCELLED.
      type: object
      required:
      - reason
      - cancelled_by
      - cancelled_at
      properties:
        reason:
          description: Why the trip was cancelled.
          $ref: '#/components/schemas/cancel-reason'
        cancelled_by:
          description: Which side cancelled.
          type: string
          enum:
          - REQUESTER
          - PROVIDER
        detail:
          description: Optional free-text explanation. Must not contain health or diagnosis information.
          type: string
          maxLength: 500
        cancelled_at:
          description: When the cancellation took effect, RFC 3339 with offset.
          type: string
          format: date-time
      examples:
      - reason: RIDER_REQUEST
        cancelled_by: REQUESTER
        cancelled_at: '2026-11-02T18:05:00-05:00'
    event-trip-cancelled:
      title: TripCancelledEvent
      description: Emitted when a trip is cancelled by either side, alongside the TRIP_STATUS_CHANGED
        event, so requesters learn who cancelled and why.
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      properties:
        event_type:
          description: Always TRIP_CANCELLED.
          const: TRIP_CANCELLED
        payload:
          description: The cancellation record.
          type: object
          required:
          - cancellation
          properties:
            cancellation:
              description: Who cancelled, why and when.
              $ref: '#/components/schemas/cancellation'
      examples:
      - event_id: evt-0003
        event_type: TRIP_CANCELLED
        occurred_at: '2026-11-02T18:05:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261105-0007
        schema_version: 0.4.0
        payload:
          cancellation:
            reason: PROVIDER_UNABLE
            cancelled_by: PROVIDER
            cancelled_at: '2026-11-02T18:05:00-05:00'
    refusal-reason:
      title: RefusalReason
      description: 'Why a provider refuses to serve a trip, tender or change. Used as the `code` of a
        422 Problem Details response and as the `reason` on a rejected tender. ELIGIBILITY_REQUIRED: the
        trip needs an eligibility attestation the request does not carry. ELIGIBILITY_NOT_ACCEPTED: the
        request carries one, but the provider does not accept its attesting party or verification method
        (SPEC.md §6.5.1); the detail may say which, never anything about the rider. OUTSIDE_BOOKING_WINDOW:
        the request came too early or too late for the provider''s booking window (SPEC.md §7.1, `/health`
        booking_window); when too early, the Problem Details carry `bookable_from`. A later request is
        a new trip (new trip_id).'
      type: string
      enum:
      - NO_CAPACITY
      - OUTSIDE_SERVICE_AREA
      - OUTSIDE_SERVICE_HOURS
      - ELIGIBILITY_REQUIRED
      - ELIGIBILITY_NOT_ACCEPTED
      - OUTSIDE_BOOKING_WINDOW
      - INVALID_WINDOW
      - UNSUPPORTED_REQUIREMENT
      examples:
      - NO_CAPACITY
      - ELIGIBILITY_NOT_ACCEPTED
      - OUTSIDE_BOOKING_WINDOW
    accepted-terms:
      title: AcceptedTerms
      x-profile: transfer
      description: Interchange terms the provider commits to by accepting a tender. Values may be lower
        than requested; the broker plans with these, not the requested ones.
      type: object
      required:
      - auto_hold_s
      - max_hold_s
      properties:
        auto_hold_s:
          description: Seconds the provider will wait for a late inbound leg without being asked.
          type: integer
          minimum: 0
        max_hold_s:
          description: Longest hold the provider will consider on request.
          type: integer
          minimum: 0
      examples:
      - auto_hold_s: 300
        max_hold_s: 900
    tender:
      title: Tender
      x-profile: transfer
      description: State of a tender at the provider. OFFERED until the provider answers (synchronously
        in the POST response, or later via a TENDER_RESPONSE event), then ACCEPTED, REJECTED or, past
        respond_by, EXPIRED.
      type: object
      required:
      - tender_id
      - trip_id
      - interchange_id
      - respond_by
      - status
      - created_at
      properties:
        tender_id:
          description: Broker-assigned tender identifier.
          type: string
        trip_id:
          description: The offered trip's trip_id; on ACCEPTED, the trip exists under this id.
          type: string
        interchange_id:
          description: The interchange the leg belongs to.
          type: string
        respond_by:
          description: Deadline for the provider's answer.
          type: string
          format: date-time
        status:
          description: Tender lifecycle state.
          type: string
          enum:
          - OFFERED
          - ACCEPTED
          - REJECTED
          - EXPIRED
        reason:
          description: Why the tender was rejected.
          $ref: '#/components/schemas/refusal-reason'
        accepted_terms:
          description: Present when ACCEPTED.
          $ref: '#/components/schemas/accepted-terms'
        provider_trip_id:
          description: Provider's own id for the created trip, when ACCEPTED.
          type: string
        created_at:
          description: When the provider received the tender.
          type: string
          format: date-time
        responded_at:
          description: When the provider answered or the tender expired.
          type: string
          format: date-time
      examples:
      - tender_id: tn-20261110-b
        trip_id: tr-20261110-b
        interchange_id: ix-20261110-1
        respond_by: '2026-11-09T17:00:00-05:00'
        status: ACCEPTED
        accepted_terms:
          auto_hold_s: 300
          max_hold_s: 900
        provider_trip_id: OG-77120
        created_at: '2026-11-09T15:00:00-05:00'
        responded_at: '2026-11-09T15:00:02-05:00'
    event-tender-response:
      title: TenderResponseEvent
      description: Emitted when a tender is answered or expires, so brokers learn asynchronous decisions.
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      properties:
        event_type:
          description: Always TENDER_RESPONSE.
          const: TENDER_RESPONSE
        payload:
          description: The tender after the decision.
          type: object
          required:
          - tender
          properties:
            tender:
              description: Current tender state.
              $ref: '#/components/schemas/tender'
      examples:
      - event_id: evt-tender-response-1
        event_type: TENDER_RESPONSE
        occurred_at: '2026-11-10T09:48:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261110-a
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          tender:
            tender_id: tn-20261110-b
            trip_id: tr-20261110-a
            interchange_id: ix-20261110-1
            respond_by: '2026-11-09T17:00:00-05:00'
            status: REJECTED
            reason: NO_CAPACITY
            created_at: '2026-11-09T15:00:00-05:00'
            responded_at: '2026-11-09T15:20:00-05:00'
    hold:
      title: Hold
      x-profile: transfer
      description: State of a hold request at the outbound provider. PENDING until answered (synchronously,
        or later via a HOLD_RESPONSE event). GRANTED and PARTIAL carry the resulting scheduled_pickup;
        a provider counter-offer ("can't wait that long, can pick up at 10:15") is PARTIAL with that time.
      type: object
      required:
      - hold_id
      - trip_id
      - interchange_id
      - action
      - requested_until
      - respond_by
      - status
      - created_at
      properties:
        hold_id:
          description: Broker-assigned hold identifier.
          type: string
        trip_id:
          description: The outbound trip.
          type: string
        interchange_id:
          description: The interchange being protected.
          type: string
        action:
          description: What was asked.
          type: string
          enum:
          - HOLD
          - RETIME
        requested_until:
          description: As in the request.
          type: string
          format: date-time
        respond_by:
          description: As in the request.
          type: string
          format: date-time
        status:
          description: Hold lifecycle state.
          type: string
          enum:
          - PENDING
          - GRANTED
          - PARTIAL
          - DENIED
          - EXPIRED
        scheduled_pickup:
          description: Outbound pickup time after the decision (GRANTED or PARTIAL).
          type: string
          format: date-time
        held_until:
          description: Latest time the vehicle will wait at the transfer point, if different from scheduled_pickup.
          type: string
          format: date-time
        reason:
          description: Why the hold was denied or only partly granted.
          type: string
          enum:
          - EXCEEDS_MAX_HOLD
          - DOWNSTREAM_COMMITMENT
          - DRIVER_HOURS
          - OTHER
        created_at:
          description: When the provider received the request.
          type: string
          format: date-time
        responded_at:
          description: When the provider answered or the hold expired.
          type: string
          format: date-time
      examples:
      - hold_id: hd-20261110-1
        trip_id: tr-20261110-b
        interchange_id: ix-20261110-1
        action: HOLD
        requested_until: '2026-11-10T10:20:00-05:00'
        respond_by: '2026-11-10T10:00:00-05:00'
        status: GRANTED
        scheduled_pickup: '2026-11-10T10:20:00-05:00'
        created_at: '2026-11-10T09:50:00-05:00'
        responded_at: '2026-11-10T09:50:01-05:00'
    event-hold-response:
      title: HoldResponseEvent
      description: Emitted when a hold request is answered or expires.
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      properties:
        event_type:
          description: Always HOLD_RESPONSE.
          const: HOLD_RESPONSE
        payload:
          description: The hold after the decision.
          type: object
          required:
          - hold
          properties:
            hold:
              description: Current hold state.
              $ref: '#/components/schemas/hold'
      examples:
      - event_id: evt-hold-response-1
        event_type: HOLD_RESPONSE
        occurred_at: '2026-11-10T09:48:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261110-a
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          hold:
            hold_id: hd-20261110-1
            trip_id: tr-20261110-a
            interchange_id: ix-20261110-1
            action: HOLD
            requested_until: '2026-11-10T10:20:00-05:00'
            respond_by: '2026-11-10T10:00:00-05:00'
            status: PARTIAL
            scheduled_pickup: '2026-11-10T10:15:00-05:00'
            reason: DOWNSTREAM_COMMITMENT
            created_at: '2026-11-10T09:50:00-05:00'
            responded_at: '2026-11-10T09:51:30-05:00'
    milestone:
      title: Milestone
      x-profile: transfer
      description: 'Progress point of a leg, always relative to the leg''s own pickup and dropoff (SPEC.md
        §10.3). Valid order: EN_ROUTE_TO_PICKUP (optional), ARRIVED_PICKUP, RIDER_ONBOARD, EN_ROUTE_TO_DROPOFF,
        ARRIVED_DROPOFF, RIDER_HANDED_OFF (required when custody applies), LEG_COMPLETED. RIDER_NO_SHOW
        may follow ARRIVED_PICKUP; LEG_FAILED may follow any non-terminal milestone.'
      type: string
      enum:
      - EN_ROUTE_TO_PICKUP
      - ARRIVED_PICKUP
      - RIDER_ONBOARD
      - EN_ROUTE_TO_DROPOFF
      - ARRIVED_DROPOFF
      - RIDER_HANDED_OFF
      - LEG_COMPLETED
      - RIDER_NO_SHOW
      - LEG_FAILED
      examples:
      - ARRIVED_DROPOFF
    address:
      title: Address
      description: Structured postal address. Supplements, never replaces, coordinates on a Location.
      type: object
      properties:
        street:
          description: Street number and name, including unit if relevant.
          type: string
          maxLength: 200
        city:
          description: City, town or municipality.
          type: string
          maxLength: 100
        region:
          description: State, province or region; US states use the two-letter USPS code.
          type: string
          maxLength: 100
        postal_code:
          description: Postal or ZIP code.
          type: string
          maxLength: 20
        country:
          description: ISO 3166-1 alpha-2 country code.
          type: string
          pattern: ^[A-Z]{2}$
      examples:
      - street: 200 N Main St
        city: Ann Arbor
        region: MI
        postal_code: '48104'
        country: US
    location:
      title: Location
      description: A pickup, dropoff or transfer point. Coordinates are WGS84 and always required; GTFS
        / GTFS-Flex identifiers are given when the point is a published stop or zone.
      type: object
      required:
      - lat
      - lon
      properties:
        lat:
          description: Latitude in decimal degrees (WGS84).
          type: number
          minimum: -90
          maximum: 90
        lon:
          description: Longitude in decimal degrees (WGS84).
          type: number
          minimum: -180
          maximum: 180
        name:
          description: Human-readable place name, e.g. a facility or landmark.
          type: string
          maxLength: 200
        address:
          description: Structured postal address of the point.
          $ref: '#/components/schemas/address'
        gtfs_stop_id:
          description: GTFS `stop_id` in the provider's published feed, if the point is a stop.
          type: string
        flex_location_id:
          description: GTFS-Flex location or location-group id, if the point is a Flex zone.
          type: string
        access_notes:
          description: Wayfinding for the driver, e.g. "east entrance". Not for rider health or condition
            information.
          type: string
          maxLength: 300
      examples:
      - lat: 42.2808
        lon: -83.743
        name: Blake Transit Center
        gtfs_stop_id: BTC
        address:
          street: 331 S 4th Ave
          city: Ann Arbor
          region: MI
          postal_code: '48104'
          country: US
        access_notes: Curb on 4th Ave side
    delay-cause:
      title: DelayCause
      x-profile: transfer
      description: Why a leg is late or failed, modeled on rail delay attribution. Codes only, no free
        text, and no rider-attributed cause. Whether a cause shifts cost is for inter-provider agreements,
        not this spec.
      type: string
      enum:
      - TRAFFIC
      - BOARDING
      - VEHICLE
      - WEATHER
      - PRIOR_TRIP
      - UNKNOWN
      examples:
      - TRAFFIC
    event-milestone:
      title: MilestoneEvent
      description: Emitted at each milestone of a leg (SPEC.md §10.3), instead of raw vehicle positions.
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      - leg_id
      properties:
        event_type:
          description: Always MILESTONE.
          const: MILESTONE
        payload:
          description: The milestone reached.
          type: object
          required:
          - milestone
          properties:
            milestone:
              description: Which milestone.
              $ref: '#/components/schemas/milestone'
            location:
              description: Where it happened, if useful (e.g. the transfer point).
              $ref: '#/components/schemas/location'
            delay_cause:
              description: On LEG_FAILED, why the leg failed.
              $ref: '#/components/schemas/delay-cause'
      examples:
      - event_id: evt-milestone-1
        event_type: MILESTONE
        occurred_at: '2026-11-10T09:48:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261110-a
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          milestone: ARRIVED_DROPOFF
    eta-band:
      title: EtaBand
      x-profile: transfer
      description: Estimated arrival at one end of a leg as a band rather than a point (SPEC.md §10.4).
        Requires earliest <= likely <= latest.
      type: object
      required:
      - target
      - earliest
      - likely
      - latest
      - confidence
      - computed_at
      properties:
        target:
          description: Which end of the leg the estimate is for.
          type: string
          enum:
          - PICKUP
          - DROPOFF
        earliest:
          description: Earliest plausible arrival.
          type: string
          format: date-time
        likely:
          description: Most likely arrival.
          type: string
          format: date-time
        latest:
          description: Latest plausible arrival.
          type: string
          format: date-time
        confidence:
          description: Probability that arrival is no later than `latest`.
          type: number
          minimum: 0
          maximum: 1
        computed_at:
          description: When the estimate was made.
          type: string
          format: date-time
        source:
          description: Where the estimate comes from. SCHEDULE means only the promised time is known;
            brokers may ask for fresher status sooner when the source is weak.
          type: string
          enum:
          - VEHICLE_POSITION
          - PROVIDER_FORECAST
          - DISPATCHER
          - DRIVER
          - SCHEDULE
        delay_cause:
          description: Why the leg is running late, if it is and the cause is known.
          $ref: '#/components/schemas/delay-cause'
      examples:
      - target: DROPOFF
        earliest: '2026-11-10T10:04:00-05:00'
        likely: '2026-11-10T10:12:00-05:00'
        latest: '2026-11-10T10:20:00-05:00'
        confidence: 0.85
        computed_at: '2026-11-10T09:48:00-05:00'
        source: VEHICLE_POSITION
        delay_cause: TRAFFIC
    event-eta-update:
      title: EtaUpdateEvent
      description: An ETA band for one end of a leg (SPEC.md §10.4). While a leg is IN_PROGRESS toward
        a PROTECTED interchange, sent at least every heartbeat_s.
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      - leg_id
      properties:
        event_type:
          description: Always ETA_UPDATE.
          const: ETA_UPDATE
        payload:
          description: The estimate.
          type: object
          required:
          - eta
          properties:
            eta:
              description: ETA band for the leg's next end.
              $ref: '#/components/schemas/eta-band'
      examples:
      - event_id: evt-eta-update-1
        event_type: ETA_UPDATE
        occurred_at: '2026-11-10T09:48:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261110-a
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          eta:
            target: DROPOFF
            earliest: '2026-11-10T10:04:00-05:00'
            likely: '2026-11-10T10:12:00-05:00'
            latest: '2026-11-10T10:20:00-05:00'
            confidence: 0.85
            computed_at: '2026-11-10T09:48:00-05:00'
            source: VEHICLE_POSITION
    custody-party:
      title: CustodyParty
      x-profile: transfer
      description: One side of a custody handoff, by role. No names or contact details.
      type: object
      required:
      - role
      properties:
        role:
          description: Role of the person taking or giving custody.
          type: string
          enum:
          - DRIVER
          - ATTENDANT
          - CAREGIVER
          - FACILITY_STAFF
        provider_id:
          description: Provider the person works for, when the role is DRIVER or ATTENDANT.
          type: string
        party_ref:
          description: Opaque reference the issuing party can resolve (e.g. a driver id). Never a name.
          type: string
      examples:
      - role: DRIVER
        provider_id: aaata-a-ride
    custody-handoff:
      title: CustodyHandoff
      x-profile: transfer
      description: Record that a supervised rider passed from one custodian to another (SPEC.md §10.7;
        proof-of-delivery pattern). A supervised leg needs one at pickup and one at dropoff before LEG_COMPLETED.
      type: object
      required:
      - handoff_from
      - handoff_to
      - timestamp
      - location
      - method
      properties:
        handoff_from:
          description: Who released the rider.
          $ref: '#/components/schemas/custody-party'
        handoff_to:
          description: Who took the rider.
          $ref: '#/components/schemas/custody-party'
        timestamp:
          description: When the handoff happened.
          type: string
          format: date-time
        location:
          description: Where it happened.
          $ref: '#/components/schemas/location'
        method:
          description: How the handoff was confirmed.
          type: string
          enum:
          - DRIVER_CONFIRM
          - ATTENDANT_CONFIRM
          - RIDER_SELF_CONFIRM
          - CODE_EXCHANGE
        caregiver_notified:
          description: Whether the rider's caregiver was told of the handoff.
          type: boolean
          default: false
      examples:
      - handoff_from:
          role: DRIVER
          provider_id: aaata-a-ride
        handoff_to:
          role: DRIVER
          provider_id: oakland-go
        timestamp: '2026-11-10T10:14:00-05:00'
        location:
          lat: 42.473
          lon: -83.2219
          name: Hub transfer point
        method: CODE_EXCHANGE
        caregiver_notified: true
    event-custody-handoff:
      title: CustodyHandoffEvent
      description: Emitted when a supervised rider is handed from one custodian to another (SPEC.md §10.7).
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      required:
      - trip_id
      - leg_id
      properties:
        event_type:
          description: Always CUSTODY_HANDOFF.
          const: CUSTODY_HANDOFF
        payload:
          description: The handoff record.
          type: object
          required:
          - handoff
          properties:
            handoff:
              description: Who handed the rider to whom, where, when and how.
              $ref: '#/components/schemas/custody-handoff'
      examples:
      - event_id: evt-custody-handoff-1
        event_type: CUSTODY_HANDOFF
        occurred_at: '2026-11-10T09:48:00-05:00'
        provider_id: aaata-a-ride
        trip_id: tr-20261110-a
        leg_id: leg-1
        schema_version: 0.4.0
        payload:
          handoff:
            handoff_from:
              role: DRIVER
              provider_id: aaata-a-ride
            handoff_to:
              role: DRIVER
              provider_id: oakland-go
            timestamp: '2026-11-10T10:14:00-05:00'
            location:
              lat: 42.473
              lon: -83.2219
            method: CODE_EXCHANGE
            caregiver_notified: true
    exception:
      title: Exception
      x-profile: transfer
      description: 'Broker-generated alert for coordinators (manage by exception, SPEC.md §10.6). Not
        sent by providers; defined so broker UIs and integrations share vocabulary. Default triggers:
        connection probability below 0.8 is AT_RISK, below 0.5 CRITICAL. ESCALATED is different from the
        other types: it says the broker has moved an unhandled condition up its own escalation ladder
        and a person is now acting on it, so subscribers don''t start a second response. It names the
        condition (escalates_exception_id), is at least AT_RISK, and is resolved together with that condition.'
      type: object
      required:
      - exception_id
      - severity
      - type
      - detected_at
      properties:
        exception_id:
          description: Broker-assigned identifier.
          type: string
        severity:
          description: How urgent the exception is.
          type: string
          enum:
          - INFO
          - AT_RISK
          - CRITICAL
        type:
          description: What went wrong.
          type: string
          enum:
          - LOW_CONNECTION_PROBABILITY
          - TENDER_EXPIRED
          - TENDER_EXHAUSTED
          - STALE_HEARTBEAT
          - MISSED_MILESTONE
          - HOLD_DENIED
          - HOLD_UNANSWERED
          - CUSTODY_GAP
          - ESCALATED
        escalates_exception_id:
          description: 'For ESCALATED: the condition exception being escalated (never another ESCALATED
            one). Further escalation raises a new ESCALATED exception for the same condition with a higher
            escalation_level.'
          type: string
        escalation_level:
          description: 'For ESCALATED: step on the broker''s own escalation ladder, 1 being the first.
            Steps and who is contacted at each are broker-local; no names, phone numbers or roles of the
            people contacted are carried.'
          type: integer
          minimum: 1
        interchange_id:
          description: The affected interchange, if any.
          type: string
        detected_at:
          description: When the broker raised it.
          type: string
          format: date-time
        suggested_actions:
          description: Actions the coordinator might take, as codes (e.g. HOLD, RETIME, REPROTECT, CALL_PROVIDER).
          type: array
          items:
            type: string
            pattern: ^[A-Z][A-Z0-9_]*$
        resolved_at:
          description: When it was resolved, if it has been.
          type: string
          format: date-time
      allOf:
      - if:
          properties:
            type:
              const: ESCALATED
          required:
          - type
        then:
          required:
          - escalates_exception_id
          properties:
            severity:
              enum:
              - AT_RISK
              - CRITICAL
        else:
          not:
            anyOf:
            - required:
              - escalates_exception_id
            - required:
              - escalation_level
      examples:
      - exception_id: ex-1
        severity: AT_RISK
        type: LOW_CONNECTION_PROBABILITY
        interchange_id: ix-20261110-1
        detected_at: '2026-11-10T09:48:30-05:00'
        suggested_actions:
        - HOLD
      - exception_id: ex-2
        severity: CRITICAL
        type: HOLD_UNANSWERED
        interchange_id: ix-20261110-1
        detected_at: '2026-11-10T09:09:00-05:00'
        suggested_actions:
        - CALL_PROVIDER
        - REPROTECT
      - exception_id: ex-3
        severity: CRITICAL
        type: ESCALATED
        escalates_exception_id: ex-2
        escalation_level: 2
        interchange_id: ix-20261110-1
        detected_at: '2026-11-10T09:14:00-05:00'
        suggested_actions:
        - CALL_PROVIDER
    event-exception-raised:
      title: ExceptionRaisedEvent
      description: 'Emitted by a broker when it raises an exception for coordinators (SPEC.md §10.6):
        a connection at risk, an unanswered tender or hold, a stale heartbeat, a missing custody record.
        Broker events go to the broker''s own subscribers (coordinator tools, agency dashboards); providers
        never send them. provider_id is the broker''s id. Payloads carry codes and ids only, no rider
        data.'
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      properties:
        event_type:
          description: Always EXCEPTION_RAISED.
          const: EXCEPTION_RAISED
        payload:
          description: An exception as raised.
          type: object
          required:
          - exception
          properties:
            exception:
              description: The exception.
              $ref: '#/components/schemas/exception'
      examples:
      - event_id: evt-exception-raised-1
        event_type: EXCEPTION_RAISED
        occurred_at: '2026-11-10T09:09:00-05:00'
        provider_id: throughride-demo
        schema_version: 0.5.0
        payload:
          exception:
            exception_id: ex-2
            severity: CRITICAL
            type: HOLD_UNANSWERED
            interchange_id: ix-20261110-1
            detected_at: '2026-11-10T09:09:00-05:00'
            suggested_actions:
            - CALL_PROVIDER
            - REPROTECT
    event-exception-resolved:
      title: ExceptionResolvedEvent
      description: Emitted by a broker when an exception it raised is resolved (SPEC.md §10.6), e.g. the
        hold was answered, the leg re-protected or the custody record arrived. Same exception_id as the
        EXCEPTION_RAISED it closes; resolved_at is set.
      x-profile: transfer
      allOf:
      - $ref: '#/components/schemas/event'
      type: object
      properties:
        event_type:
          description: Always EXCEPTION_RESOLVED.
          const: EXCEPTION_RESOLVED
        payload:
          description: The exception, with resolved_at.
          type: object
          required:
          - exception
          properties:
            exception:
              description: The exception, resolved.
              allOf:
              - $ref: '#/components/schemas/exception'
              - required:
                - resolved_at
      examples:
      - event_id: evt-exception-resolved-1
        event_type: EXCEPTION_RESOLVED
        occurred_at: '2026-11-10T09:10:00-05:00'
        provider_id: throughride-demo
        schema_version: 0.5.0
        payload:
          exception:
            exception_id: ex-2
            severity: CRITICAL
            type: HOLD_UNANSWERED
            interchange_id: ix-20261110-1
            detected_at: '2026-11-10T09:09:00-05:00'
            suggested_actions:
            - CALL_PROVIDER
            - REPROTECT
            resolved_at: '2026-11-10T09:10:00-05:00'
  messages:
    TripStatusChanged:
      name: TRIP_STATUS_CHANGED
      title: Trip status changed
      summary: Every trip status change.
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-trip-status-changed'
    TripScheduled:
      name: TRIP_SCHEDULED
      title: Trip scheduled
      summary: Committed times or vehicle set or changed.
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-trip-scheduled'
    TripCancelled:
      name: TRIP_CANCELLED
      title: Trip cancelled
      summary: Who cancelled, why and when; sent with TRIP_STATUS_CHANGED.
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-trip-cancelled'
    TenderResponse:
      name: TENDER_RESPONSE
      title: Tender answered
      summary: A tender was accepted, rejected or expired.
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-tender-response'
    HoldResponse:
      name: HOLD_RESPONSE
      title: Hold answered
      summary: A hold request was answered or expired.
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-hold-response'
    Milestone:
      name: MILESTONE
      title: Milestone reached
      summary: A leg reached a milestone (SPEC.md §10.3).
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-milestone'
    EtaUpdate:
      name: ETA_UPDATE
      title: ETA update
      summary: ETA band for a leg's next end; heartbeat toward protected interchanges.
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-eta-update'
    CustodyHandoff:
      name: CUSTODY_HANDOFF
      title: Custody handoff
      summary: A supervised rider changed custodian (SPEC.md §10.7).
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-custody-handoff'
    ExceptionRaised:
      name: EXCEPTION_RAISED
      title: Exception raised
      summary: A broker raised an exception for coordinators (SPEC.md §10.6). Sent by brokers, not providers.
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-exception-raised'
    ExceptionResolved:
      name: EXCEPTION_RESOLVED
      title: Exception resolved
      summary: An exception a broker raised was resolved. Sent by brokers, not providers.
      x-profile: transfer
      traits:
      - $ref: '#/components/messageTraits/signed'
      payload:
        schemaFormat: application/schema+json;version=draft-2020-12
        schema:
          $ref: '#/components/schemas/event-exception-resolved'
