openapi: 3.1.0

info:
  title: GTFS Media API
  version: "2.0"
  summary: Static GTFS and GTFS-Realtime data served as JSON.
  description: |
    The read API exposed by a gtfs.media deployment. Static GTFS files are
    modelled as addressable resources under a feed alias; GTFS-Realtime feeds
    are decoded server-side and republished as JSON.

    **Every response uses the same envelope** — a `meta` object and a `data`
    payload — so a client written against one resource can read them all.

    **Authentication: none.** Every endpoint here is a public read. A stock
    deployment grants them to anonymous users through Drupal's permission
    system, so no key, token or header is required. An operator can restrict
    any resource per role, in which case that endpoint returns 403 for clients
    without the permission.

    **Errors.** A missing feed, entity or route returns `404` with an error
    document — `{"message": "..."}` — rather than the usual envelope. There is
    no `400`: a malformed path segment is treated as "matches nothing", so a
    nonsensical `direction_id` or `entity_type` returns `200` with an empty
    `data` array rather than an error. Check `meta.dataCount` rather than
    relying on a status code to tell you a filter was wrong.

    **Rate limiting: none.** A stock deployment applies no request quota. The
    upstream 511-style aggregators some deployments pull from do impose their
    own limits, but that is between the deployment and its source, not between
    you and this API.

    **Caching and polling.** Realtime responses are explicitly uncacheable, so
    poll them directly; static responses carry whatever `max-age` the
    deployment sets (commonly a few minutes). Each realtime feed re-fetches
    from upstream on its own `refresh_interval`, 60 seconds by default, and
    `meta.last_fetched` reports when that last succeeded. Polling faster than
    the refresh interval returns the same payload, so 10–30 seconds is a
    sensible client interval and anything under 10 is waste. A display
    requesting data also triggers an async upstream refresh when the feed is
    stale, so active screens keep feeds warm.

    **CORS: not enabled by default.** A stock deployment sends no
    `Access-Control-Allow-*` headers, so browser code must be same-origin with
    the API — which is how the platform's own displays and embeds work. To read
    it from another origin, the operator enables Drupal's `cors.config` in
    `services.yml`, or fronts the API with a proxy that adds the headers.

    **Realtime is not feed-scoped.** Static paths sit under a `feed_alias`;
    `/rt/*` paths do not. A realtime resource reads every configured feed of
    its type and merges the results. Where a deployment hosts several feeds
    whose `stop_id` or `route_id` values collide, matches from all of them come
    back together, so filter on the returned `trip`/`vehicle` fields if you
    need to tell them apart.

    **Numbers arrive as strings.** GTFS values are served as they appear in the
    feed, so `stop_lat`, `route_type`, `stop_sequence` and similar fields are
    JSON strings (`"37.7212"`, `"3"`, `"1"`), not numbers. Realtime payloads
    are mixed: coordinates and `delay` are real numbers, but `timestamp` on
    trip updates and vehicle positions is a string, while `meta.last_fetched`
    is an integer. Parse defensively rather than assuming a type per name.

    **Dates are ISO 8601, not GTFS.** `start_date`, `end_date` and the dates on
    service exceptions come back as `"2026-07-23T00:00:00"`, not the
    `YYYYMMDD` form used inside `calendar.txt`. Times of day keep their GTFS
    form and may exceed 24 hours (`"25:10:00"`) for service running past
    midnight.

    **Feeds are versioned side by side.** A deployment can host several feeds
    at once, each with its own alias; every static path is scoped to one.

    **Versioning.** v1 was internal and never published. v2 is stable and
    add-only: new resources and new fields may appear, but existing ones will
    not change shape or disappear. Anything genuinely breaking would ship as
    v3, if it ever happens.

    This document describes the surface of a stock deployment. Agency-specific
    modules may add resources and fields beyond it.
  license:
    name: GPL-2.0-or-later
    url: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  contact:
    name: gtfs.media
    url: https://gtfs.media
    email: hello@gtfs.media

servers:
  - url: https://{host}/gtfs/api/v2
    description: A gtfs.media deployment
    variables:
      host:
        default: transit.example.gov
        description: Hostname of the deployment

tags:
  - name: Discovery
    description: Finding out what a deployment serves.
  - name: Static GTFS
    description: Scheduled service, imported from the agency's GTFS feed.
  - name: Realtime
    description: GTFS-Realtime, decoded from Protobuf and served as JSON.
  - name: Display
    description: Operator annotations and overrides attached to GTFS entities.

paths:
  /:
    get:
      tags: [Discovery]
      summary: List every resource the deployment serves
      description: |
        Returns each REST resource with its URI template. Useful for
        discovering the surface of an unfamiliar deployment.
      operationId: getApiRoot
      parameters: [{ $ref: '#/components/parameters/format' }]
      responses:
        '200':
          description: The resource index.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ResourceDescriptor' }
              example:
                meta: { totalCount: 24, dataCount: 24, limit: 0, offset: 0 }
                data:
                  - id: gtfs_stop_resource
                    label: GTFS stop REST
                    uri_template: https://transit.example.gov/gtfs/api/v2/feeds/{feed_alias}/stops/{stop_id}

  /feeds:
    get:
      tags: [Discovery]
      summary: List available feeds
      description: Every feed loaded into the deployment, with its alias.
      operationId: listFeeds
      parameters: [{ $ref: '#/components/parameters/format' }]
      responses:
        '200':
          description: The available feeds.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Feed' }
              example:
                meta:
                  totalCount: 2
                  dataCount: 2
                  links: { self: 'https://transit.example.gov/gtfs/api/v2/feeds', next: false, previous: false }
                  limit: 0
                  offset: 0
                data:
                  - name: Autumn 2026 service change
                    alias: autumn-2026
                    link: https://transit.example.gov/gtfs/api/v2/feeds/autumn-2026

  /feeds/{feed_alias}:
    get:
      tags: [Discovery]
      summary: Describe one feed
      description: |
        Feed metadata, plus links to its collections in `meta.links`.
      operationId: getFeed
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The feed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Feed' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies:
    get:
      tags: [Static GTFS]
      summary: List agencies
      description: Contents of `agency.txt`.
      operationId: listAgencies
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Agencies in this feed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Agency' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}:
    get:
      tags: [Static GTFS]
      summary: Get one agency
      operationId: getAgency
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The agency.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Agency' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes:
    get:
      tags: [Static GTFS]
      summary: List routes
      description: Contents of `routes.txt` for one agency.
      operationId: listRoutes
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Routes operated by this agency.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Route' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}:
    get:
      tags: [Static GTFS]
      summary: Get one route
      operationId: getRoute
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The route.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Route' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/geojson:
    get:
      tags: [Static GTFS]
      summary: Get a route's shape as GeoJSON
      description: |
        A GeoJSON `Feature` whose geometry is a `MultiLineString` covering every
        shape the route's trips reference. Route identity and styling
        (`route_color`, `route_text_color`) travel in `properties`, ready to
        hand to a map library.
      operationId: getRouteGeoJson
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The route geometry.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeoJsonFeature' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/directions:
    get:
      tags: [Static GTFS]
      summary: List a route's directions
      description: |
        The directions the route operates in, derived from its trips. Direction
        names come from the feed where present.
      operationId: listRouteDirections
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Directions for this route.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Direction' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/directions/{direction_id}:
    get:
      tags: [Static GTFS]
      summary: Get one direction of a route
      operationId: getRouteDirection
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/directionId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The direction.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Direction' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/stops/{direction_ids}:
    get:
      tags: [Static GTFS]
      summary: List the stops a route serves
      description: |
        Stops along the route, in service order, for one or more directions.
      operationId: listRouteStops
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - name: direction_ids
          in: path
          required: true
          description: A direction id, or several separated by commas.
          schema: { type: string, examples: ['0,1'] }
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Stops served by this route.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Stop' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/trips:
    get:
      tags: [Static GTFS]
      summary: List a route's trips
      description: |
        Contents of `trips.txt` for this route. Large routes run to thousands
        of trips; page with `limit` and `offset`.
      operationId: listRouteTrips
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Trips on this route.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Trip' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/trips/{trip_id}:
    get:
      tags: [Static GTFS]
      summary: Get one trip
      operationId: getTrip
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/tripId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The trip.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Trip' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/trips/{trip_id}/stop_times:
    get:
      tags: [Static GTFS]
      summary: List a trip's stop times
      description: |
        Contents of `stop_times.txt` for the trip, in `stop_sequence` order.
        Times may exceed 24 hours (`"25:10:00"`) for service running past
        midnight, per the GTFS specification.
      operationId: listTripStopTimes
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/tripId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Stop times for this trip.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/StopTime' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/schedules/{service_id}/{direction_id}:
    get:
      tags: [Static GTFS]
      summary: Get a route timetable
      description: |
        A timetable for one route, service and direction: the ordered stops and
        the trips that call at them, assembled ready to render as a grid.
      operationId: getRouteSchedule
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/serviceId'
        - $ref: '#/components/parameters/directionId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The timetable.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Schedule' }
              example:
                meta: { totalCount: 0, dataCount: 0, limit: 500, offset: 0 }
                data:
                  agency_id: SFMTA
                  route_id: "1"
                  service_id: "2"
                  direction_id: "0"
                  schedule:
                    stops:
                      - { label: "Clay St & Drumm St (4015)", stop_id: "4015", stop_name: "Clay St & Drumm St" }
                      - { label: "Sacramento St & Kearny St (3892)", stop_id: "3892", stop_name: "Sacramento St & Kearny St" }
                    trips:
                      - label: Geary + 33rd Avenue
                        trip_id: "12053368"
                        route_id: "1"
                        service_id: "2"
                        direction_id: "0"
                        stop_times:
                          - null
                          - { stop_id: 3892, arrival_time: 4:38am, arrival_time_seconds: 16680 }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/services:
    get:
      tags: [Static GTFS]
      summary: List service calendars
      description: |
        Service calendars from `calendar.txt`. Each item carries the weekly
        pattern and date range; fetch a single service to add its exceptions
        from `calendar_dates.txt`.
      operationId: listServices
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Service calendars in this feed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Service' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/services/{service_id}:
    get:
      tags: [Static GTFS]
      summary: Get one service calendar
      description: |
        The weekly service pattern, the date range it applies over, and any
        exceptions from `calendar_dates.txt`.
      operationId: getService
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/serviceId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The service calendar.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Service' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/shapes/{shape_id}:
    get:
      tags: [Static GTFS]
      summary: Get a shape's points
      description: |
        The ordered points of one shape from `shapes.txt`. For route geometry
        ready to draw, prefer the route GeoJSON resource.
      operationId: getShape
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - name: shape_id
          in: path
          required: true
          description: A `shape_id` from the feed.
          schema: { type: string, examples: ['102'] }
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The shape points.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ShapePoint' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops:
    get:
      tags: [Static GTFS]
      summary: List stops
      description: |
        Contents of `stops.txt`. Systems commonly have thousands of stops and
        the default page is 500; follow `meta.links.next` to page through.
      operationId: listStops
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Stops in this feed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Stop' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops/{stop_id}:
    get:
      tags: [Static GTFS]
      summary: Get one stop
      operationId: getStop
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/stopId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Stop' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops/{stop_id}/routes:
    get:
      tags: [Static GTFS]
      summary: List the routes calling at a stop
      operationId: listStopRoutes
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/stopId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Routes serving this stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Route' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/wkt:
    get:
      tags: [Static GTFS]
      summary: Get a route's shape as WKT
      description: |
        The same geometry as the GeoJSON resource, as a Well-Known Text
        `MULTILINESTRING`. Useful for loading straight into PostGIS or another
        geospatial tool. The payload is a bare string, not an object.
      operationId: getRouteWkt
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The route geometry as WKT.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: string
                        description: WKT `MULTILINESTRING`.
              example:
                meta: { totalCount: 0, dataCount: 0, limit: 500, offset: 0 }
                data: "MULTILINESTRING((-122.397 37.7954,-122.3968 37.7955))"
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/trips/{trip_id}/geojson:
    get:
      tags: [Static GTFS]
      summary: Get a trip's shape as GeoJSON
      description: |
        A GeoJSON `Feature` for one trip's path — a `LineString`, where the
        route resource returns a `MultiLineString` covering every variant.
        `properties` carries the trip's identity and headsign.
      operationId: getTripGeoJson
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/tripId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The trip geometry.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeoJsonFeature' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/agencies/{agency_id}/routes/{route_id}/trips/{trip_id}/stops:
    get:
      tags: [Static GTFS]
      summary: List the stops a trip calls at
      description: Stops on this trip, in `stop_sequence` order.
      operationId: listTripStops
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/agencyId'
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/tripId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Stops on this trip.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Stop' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/shapes:
    get:
      tags: [Static GTFS]
      summary: List shape ids
      description: |
        Every `shape_id` in the feed, with a link to its points. Identifiers
        only — fetch a shape for its geometry.
      operationId: listShapes
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Shape ids in this feed.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ShapeStub' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/shapes/{shape_id}/geojson:
    get:
      tags: [Static GTFS]
      summary: Get a shape as GeoJSON
      description: |
        A GeoJSON `Feature` whose geometry is the shape's `LineString`. Carries
        no `properties`; the shape id is in the request.
      operationId: getShapeGeoJson
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - name: shape_id
          in: path
          required: true
          description: A `shape_id` from the feed.
          schema: { type: string, examples: ['102'] }
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: The shape geometry.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeoJsonFeature' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops/{stop_id}/directions:
    get:
      tags: [Static GTFS]
      summary: List the route directions serving a stop
      description: |
        Every route-and-direction combination that calls at this stop. Each
        item embeds the full route, so a departure board can be built from one
        request.
      operationId: listStopDirections
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/stopId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Route directions serving this stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Direction' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops/{stop_id}/trips:
    get:
      tags: [Static GTFS]
      summary: List the trips calling at a stop
      description: |
        Every trip that stops here. Busy stops run to thousands of trips across
        all service days; page with `limit` and `offset`.
      operationId: listStopTrips
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/stopId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Trips calling at this stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Trip' }
        '404': { $ref: '#/components/responses/NotFound' }

  /feeds/{feed_alias}/stops/{stop_id}/stop_times:
    get:
      tags: [Static GTFS]
      summary: List scheduled calls at a stop
      description: |
        Every scheduled arrival and departure at this stop, across all trips
        and service days — the timetable for one stop. Filter by service day
        using the `service_id` on the referenced trips.
      operationId: listStopStopTimes
      parameters:
        - $ref: '#/components/parameters/feedAlias'
        - $ref: '#/components/parameters/stopId'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/offset'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Scheduled calls at this stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/StopTime' }
        '404': { $ref: '#/components/responses/NotFound' }

  /rt/tripupdates/{stop_id}:
    get:
      tags: [Realtime]
      summary: Predicted arrivals and departures at a stop
      description: |
        GTFS-Realtime TripUpdate entities affecting this stop, decoded from
        Protobuf.

        Realtime feeds may identify stops differently from the static feed —
        many agencies publish `stop_code` in realtime and `stop_id` in the
        schedule. Use the identifier your realtime feed uses.

        `meta.last_fetched` is the Unix timestamp of the last successful poll
        of the upstream feed. Compare it against your own clock to decide
        whether a prediction is still trustworthy.
      operationId: getTripUpdates
      parameters:
        - name: stop_id
          in: path
          required: true
          description: The stop identifier as it appears in the realtime feed.
          schema: { type: string, examples: ['17948'] }
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Trip updates for this stop.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/TripUpdate' }
              example:
                meta:
                  totalCount: 16
                  dataCount: 16
                  limit: 500
                  offset: 0
                  last_fetched: 1787101680
                data:
                  - trip:
                      tripId: 12066664_M21
                      routeId: "25"
                      directionId: 0
                      startDate: "20260818"
                      scheduleRelationship: SCHEDULED
                      tripHeadsign: Treasure Island
                    vehicle: { id: "8744", label: "8744", licensePlate: "" }
                    stopTimeUpdate:
                      - stopSequence: 1
                        stopId: "17948"
                        departure: { delay: 0, time: "1787101800" }
                      - stopSequence: 2
                        stopId: "18000"
                        arrival: { delay: 117, time: "1787102517" }
                    timestamp: "1787101656"

  /rt/vehicles/{route_id}:
    get:
      tags: [Realtime]
      summary: Live vehicle positions on a route
      description: |
        GTFS-Realtime VehiclePosition entities for the route, with coordinates,
        bearing and speed where the agency publishes them.
      operationId: getVehiclePositions
      parameters:
        - $ref: '#/components/parameters/routeId'
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Vehicles currently on this route.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/VehiclePosition' }
              example:
                meta: { totalCount: 12, dataCount: 12, limit: 500, offset: 0, last_fetched: 1787102550 }
                data:
                  - trip:
                      tripId: 12053257_M21
                      routeId: "1"
                      directionId: 1
                      startDate: "20260818"
                      scheduleRelationship: SCHEDULED
                    vehicle: { id: "5737", label: "5737", licensePlate: "" }
                    position: { latitude: 37.795387, longitude: -122.39716, bearing: 75, speed: 0 }
                    currentStopSequence: 48
                    stopId: "14015"
                    currentStatus: STOPPED_AT
                    timestamp: "1787102547"
                    occupancyStatus: EMPTY

  /rt/alerts/{entity_type}/{entity_id}:
    get:
      tags: [Realtime]
      summary: Service alerts affecting an entity
      description: |
        GTFS-Realtime Alert entities whose informed entities include the one
        addressed. Returns an empty array when nothing is disrupted.
      operationId: getServiceAlerts
      parameters:
        - name: entity_type
          in: path
          required: true
          description: |
            The kind of entity the alert is attached to. Singular and plural
            are both accepted — the server strips one trailing `s` — so
            `stop` and `stops` behave identically.
          schema:
            type: string
            enum: [stop, stops, route, routes, trip, trips, agency, agencies]
            examples: ['stops']
        - name: entity_id
          in: path
          required: true
          description: Identifier of that entity in the realtime feed.
          schema: { type: string, examples: ['17948'] }
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Active alerts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Alert' }
              example:
                meta: { totalCount: 1, dataCount: 1, limit: 500, offset: 0, last_fetched: 1787101680 }
                data:
                  - id: alert_4471
                    headerText:
                      translation:
                        - { text: "Route 17: weekend detour", language: en }
                        - { text: "Ruta 17: desvío de fin de semana", language: es }
                    descriptionText:
                      translation:
                        - { text: "Stops 4030-4034 are closed. Board at Elm St.", language: en }
                    informedEntity:
                      - { agencyId: SFMTA, routeId: "17", stopId: null }

  /display/annotations/{entity_type}/{entity_id}:
    get:
      tags: [Display]
      summary: Operator annotations for an entity
      description: |
        Notes and overrides staff have attached to a GTFS entity — a boarding
        change, a temporary closure, a message for one route at one stop.

        Only annotations currently within their active time window are
        returned. `body` is text-format processed HTML.
      operationId: getAnnotations
      parameters:
        - name: entity_type
          in: path
          required: true
          description: |
            The GTFS entity type the annotation is attached to, matching the
            type used when the annotation was created.
          schema:
            type: string
            enum: [stops, routes, trips, agency]
            examples: ['stops']
        - name: entity_id
          in: path
          required: true
          description: Identifier of that entity in the static feed.
          schema: { type: string, examples: ['7948'] }
        - $ref: '#/components/parameters/format'
      responses:
        '200':
          description: Active annotations.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Annotation' }
              example:
                meta: { totalCount: 1, dataCount: 1, limit: 500, offset: 0 }
                data:
                  - id: 12
                    annotation_type: override
                    entity_type: stop
                    entity_id: "4021"
                    secondary_entity_type: route
                    secondary_entity_id: "17"
                    body: "<p>Board across the street this weekend.</p>"
                    override_entity_id: "4099"
                    active: true

components:

  parameters:
    format:
      name: _format
      in: query
      required: false
      description: |
        Response format. Send `Accept: application/json` instead if you prefer;
        one or the other is required.
      schema: { type: string, enum: [json], default: json }
    limit:
      name: limit
      in: query
      required: false
      description: Items per page. Collections default to 500.
      schema: { type: integer, minimum: 1, default: 500 }
    offset:
      name: offset
      in: query
      required: false
      description: Items to skip. Prefer following `meta.links.next`.
      schema: { type: integer, minimum: 0, default: 0 }
    feedAlias:
      name: feed_alias
      in: path
      required: true
      description: |
        Alias of the feed to read, from `/feeds`. A deployment may host several
        feed versions at once.
      schema: { type: string, examples: ['autumn-2026'] }
    agencyId:
      name: agency_id
      in: path
      required: true
      description: An `agency_id` from the feed.
      schema: { type: string, examples: ['SFMTA'] }
    routeId:
      name: route_id
      in: path
      required: true
      description: A `route_id` from the feed.
      schema: { type: string, examples: ['1'] }
    tripId:
      name: trip_id
      in: path
      required: true
      description: A `trip_id` from the feed.
      schema: { type: string, examples: ['12053339'] }
    stopId:
      name: stop_id
      in: path
      required: true
      description: A `stop_id` from the static feed.
      schema: { type: string, examples: ['390'] }
    serviceId:
      name: service_id
      in: path
      required: true
      description: A `service_id` from `calendar.txt`.
      schema: { type: string, examples: ['1'] }
    directionId:
      name: direction_id
      in: path
      required: true
      description: GTFS direction id.
      schema: { type: string, enum: ['0', '1'] }

  responses:
    NotFound:
      description: |
        No such resource. The body is an error document, not the standard
        envelope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:

    Envelope:
      type: object
      description: |
        The wrapper every successful response shares. `data` is an object for
        single resources and an array for collections.
      required: [meta, data]
      properties:
        meta: { $ref: '#/components/schemas/Meta' }
        data:
          description: The payload; shape depends on the resource.
      examples:
        - meta: { totalCount: 1, dataCount: 1, limit: 500, offset: 0 }
          data: {}

    Meta:
      type: object
      description: Counts, paging state and related links.
      properties:
        totalCount:
          type: integer
          description: Total items matching the request, ignoring paging.
        dataCount:
          type: integer
          description: Items in this response.
        limit:
          type: integer
          description: Page size in effect. `0` where paging does not apply.
        offset:
          type: integer
          description: Items skipped.
        links: { $ref: '#/components/schemas/Links' }
        last_fetched:
          type: integer
          description: |
            Realtime resources only. Unix timestamp of the last successful poll
            of the upstream GTFS-Realtime feed.

    Links:
      type: object
      description: |
        Related resources. Absent keys mean the relation does not apply.

        `next` and `previous` are `false` rather than `null` when there is no
        further page — test truthiness, not presence.
      additionalProperties:
        oneOf:
          - { type: string, format: uri }
          - { type: boolean, const: false }
      properties:
        self: { type: string, format: uri, description: This resource. }
        next:
          oneOf:
            - { type: string, format: uri }
            - { type: boolean, const: false }
          description: Next page, or `false`.
        previous:
          oneOf:
            - { type: string, format: uri }
            - { type: boolean, const: false }
          description: Previous page, or `false`.
        feed: { type: string, format: uri, description: The feed this belongs to. }
        list: { type: string, format: uri, description: The collection this item came from. }

    Error:
      type: object
      description: Error document returned for 4xx and 5xx responses.
      properties:
        message:
          type: string
          description: Human-readable explanation.

    ResourceDescriptor:
      type: object
      description: One resource advertised by the API root.
      properties:
        id: { type: string, description: Internal plugin id., examples: ['gtfs_stop_resource'] }
        label: { type: string, description: Human-readable name. }
        uri_template:
          type: string
          description: RFC 6570 URI template for the resource.
          examples: ['https://transit.example.gov/gtfs/api/v2/feeds/{feed_alias}/stops/{stop_id}']

    Feed:
      type: object
      description: An imported GTFS feed.
      properties:
        name: { type: string, description: Name given at import, often the source filename. }
        alias: { type: string, description: Alias used in paths., examples: ['autumn-2026'] }
        link: { type: string, format: uri, description: The feed's own resource. }

    Agency:
      type: object
      description: A row of `agency.txt`.
      properties:
        label: { type: string, description: Human-readable label for the row. }
        agency_id: { type: string }
        agency_name: { type: string }
        agency_url: { type: string, format: uri }
        agency_timezone: { type: string, examples: ['America/Los_Angeles'] }
        agency_lang: { type: [string, 'null'] }
        agency_phone: { type: [string, 'null'] }
        agency_fare_url: { type: [string, 'null'] }
        agency_email: { type: [string, 'null'] }
        links: { $ref: '#/components/schemas/Links' }

    Route:
      type: object
      description: |
        A row of `routes.txt`.

        Deployments may enrich route payloads with their own fields through
        `hook_gtfs_route_data_alter()` — an agency logo URL, a route map image.
        Treat unrecognised keys as additive rather than an error.
      additionalProperties: true
      properties:
        label: { type: string }
        route_id: { type: string }
        agency_id: { type: string }
        route_short_name: { type: [string, 'null'], description: 'Public-facing designation, e.g. "38R".' }
        route_long_name: { type: [string, 'null'] }
        route_desc: { type: [string, 'null'] }
        route_type: { type: string, description: 'GTFS route type as a string, e.g. "3" for bus.' }
        route_url: { type: [string, 'null'] }
        route_color: { type: [string, 'null'], description: 'Six-digit hex, no leading "#".' }
        route_text_color: { type: [string, 'null'] }
        route_sort_order: { type: [string, 'null'] }
        continuous_pickup: { type: [string, 'null'] }
        continuous_drop_off: { type: [string, 'null'] }
        network_id: { type: [string, 'null'] }
        links: { $ref: '#/components/schemas/Links' }

    Stop:
      type: object
      description: A row of `stops.txt`.
      properties:
        label: { type: string }
        stop_id: { type: string }
        stop_code: { type: [string, 'null'], description: Rider-facing code, often shown on the pole. }
        stop_name: { type: string }
        stop_desc: { type: [string, 'null'] }
        stop_lat: { type: string, description: Latitude as a string. }
        stop_lon: { type: string, description: Longitude as a string. }
        zone_id: { type: [string, 'null'] }
        stop_url: { type: [string, 'null'] }
        location_type: { type: [string, 'null'] }
        parent_station: { type: [string, 'null'] }
        stop_timezone: { type: [string, 'null'] }
        wheelchair_boarding: { type: [string, 'null'] }
        platform_code: { type: [string, 'null'] }
        tts_stop_name: { type: [string, 'null'], description: Pronunciation hint for speech output. }
        level_id: { type: [string, 'null'] }
        links: { $ref: '#/components/schemas/Links' }

    Trip:
      type: object
      description: A row of `trips.txt`.
      properties:
        label: { type: string }
        trip_id: { type: string }
        route_id: { type: string }
        service_id: { type: string }
        trip_headsign: { type: [string, 'null'], description: Destination shown to riders. }
        trip_short_name: { type: [string, 'null'] }
        direction_id: { type: [string, 'null'] }
        block_id: { type: [string, 'null'] }
        shape_id: { type: [string, 'null'] }
        wheelchair_accessible: { type: [string, 'null'] }
        bikes_allowed: { type: [string, 'null'] }
        links: { $ref: '#/components/schemas/Links' }

    StopTime:
      type: object
      description: A row of `stop_times.txt`.
      properties:
        label: { type: string }
        trip_id: { type: string }
        arrival_time: { type: [string, 'null'], description: 'May exceed 24h, e.g. "25:10:00".' }
        departure_time: { type: [string, 'null'] }
        stop_id: { type: string }
        location_group_id: { type: [string, 'null'] }
        stop_sequence: { type: string, description: Order within the trip, as a string. }
        stop_headsign: { type: [string, 'null'] }
        start_pickup_drop_off_window: { type: [string, 'null'] }
        end_pickup_drop_off_window: { type: [string, 'null'] }
        pickup_type: { type: [string, 'null'] }
        drop_off_type: { type: [string, 'null'] }
        continuous_pickup: { type: [string, 'null'] }
        continuous_drop_off: { type: [string, 'null'] }
        shape_dist_traveled: { type: [string, 'null'] }
        timepoint: { type: [string, 'null'] }
        pickup_booking_rule_id: { type: [string, 'null'] }
        drop_off_booking_rule_id: { type: [string, 'null'] }
        links:
          description: |
            Present when listed for a stop; a trip's own stop times omit it.
          oneOf:
            - { $ref: '#/components/schemas/Links' }
            - { type: 'null' }

    Direction:
      type: object
      description: A direction a route operates in.
      properties:
        label: { type: string, examples: ['Outbound'] }
        route_id: { type: string }
        direction_id: { type: string, examples: ['0'] }
        direction: { type: string, description: Name of the direction. }
        route:
          description: |
            The full route, embedded when the direction is reached through a
            stop, so a departure board needs one request. Absent on a route's
            own directions.
          oneOf:
            - { $ref: '#/components/schemas/Route' }
            - { type: 'null' }
        service:
          description: |
            The service calendar, where the deployment resolves one. Often
            `null`.
          oneOf:
            - { $ref: '#/components/schemas/Service' }
            - { type: 'null' }
        links: { $ref: '#/components/schemas/Links' }

    Service:
      type: object
      description: |
        A service calendar: which days it runs, over what date range, and the
        dates that deviate.
      properties:
        label: { type: string, examples: ['WEEKDAY'] }
        service_id: { type: string }
        monday: { type: string, description: '"1" when service runs, "0" when not.' }
        tuesday: { type: string }
        wednesday: { type: string }
        thursday: { type: string }
        friday: { type: string }
        saturday: { type: string }
        sunday: { type: string }
        start_date:
          type: string
          format: date-time
          description: First day of service, ISO 8601 — not GTFS `YYYYMMDD`.
          examples: ['2026-07-23T00:00:00']
        end_date:
          type: string
          format: date-time
          description: Last day of service, ISO 8601.
          examples: ['2026-08-28T00:00:00']
        exceptions:
          type: array
          description: |
            Dates that deviate, from `calendar_dates.txt`. Returned by the
            single-service resource; the collection omits it.
          items:
            type: object
            properties:
              label: { type: string }
              service_id: { type: string }
              date:
                type: string
                format: date-time
                description: The affected day, ISO 8601.
              exception_type:
                type: string
                description: '"1" adds service, "2" removes it.'
        links: { $ref: '#/components/schemas/Links' }

    ShapeStub:
      type: object
      description: |
        Item shape returned by the shapes collection — an identifier and a link
        to its points, not the geometry itself.
      properties:
        shape_id: { type: string }
        links: { $ref: '#/components/schemas/Links' }

    ShapePoint:
      type: object
      description: One point of a shape, from `shapes.txt`.
      properties:
        label: { type: string }
        shape_id: { type: string }
        shape_pt_lat: { type: string }
        shape_pt_lon: { type: string }
        shape_pt_sequence: { type: string }
        shape_dist_traveled: { type: [string, 'null'] }

    Schedule:
      type: object
      description: |
        A timetable for one route, service and direction, pre-assembled as a
        grid: `schedule.stops` are the rows and `schedule.trips` the columns.

        Each trip's `stop_times` array is **positionally aligned to
        `schedule.stops`** and always the same length: index *n* is that trip's
        call at `stops[n]`, or `null` where the trip skips it. Render the grid
        by walking the two in step — do not match on `stop_id`.
      properties:
        agency_id: { type: string }
        route_id: { type: string }
        service_id: { type: string }
        direction_id: { type: string }
        schedule:
          type: object
          description: The grid itself.
          properties:
            stops:
              type: array
              description: Stops in service order — the rows of the grid.
              items: { $ref: '#/components/schemas/Stop' }
            trips:
              type: array
              description: Trips running this service and direction — the columns.
              items: { $ref: '#/components/schemas/ScheduleTrip' }

    ScheduleTrip:
      allOf:
        - $ref: '#/components/schemas/Trip'
        - type: object
          description: A trip inside a timetable, carrying its row of cells.
          properties:
            stop_times:
              type: array
              description: |
                One entry per stop in `schedule.stops`, in the same order.
                `null` means this trip does not call at that stop.
              items:
                oneOf:
                  - { $ref: '#/components/schemas/ScheduleCell' }
                  - { type: 'null' }

    ScheduleCell:
      type: object
      description: One call in a timetable grid.
      properties:
        stop_id:
          type: integer
          description: |
            The stop for this cell. Note this is an **integer** here, while
            `Stop.stop_id` elsewhere in the API is a string.
        arrival_time:
          type: string
          description: Arrival formatted for display.
          examples: ['4:38am']
        arrival_time_seconds:
          type: integer
          description: |
            The same arrival as seconds after midnight, for sorting and
            comparison. Values above 86400 are service past midnight.
          examples: [16680]

    GeoJsonFeature:
      type: object
      description: |
        A GeoJSON `Feature` (RFC 7946). Coordinates are `[longitude, latitude]`
        and are real numbers, unlike the string coordinates on static
        resources.

        Geometry and properties depend on which resource produced it: a route
        returns a `MultiLineString` covering every shape its trips use, with
        route identity and styling in `properties`; a trip returns the
        `LineString` it follows, with trip identity; a shape returns its
        `LineString` and no `properties` at all.
      properties:
        type: { type: string, const: Feature }
        properties:
          type: [object, 'null']
          description: |
            Identity and styling for the feature. Route features carry the
            route fields below; trip features carry `trip_id`, `route_id`,
            `service_id`, `trip_headsign` and `block_id`; shape features
            carry none.
          additionalProperties: true
          properties:
            route_id: { type: string }
            route_short_name: { type: [string, 'null'] }
            route_long_name: { type: [string, 'null'] }
            route_type: { type: string }
            route_color: { type: [string, 'null'] }
            route_text_color: { type: [string, 'null'] }
            route_url: { type: [string, 'null'] }
        geometry:
          type: object
          properties:
            type:
              type: string
              enum: [LineString, MultiLineString]
            coordinates:
              description: |
                For `LineString`, an array of `[lon, lat]` pairs. For
                `MultiLineString`, an array of those arrays — one per segment.
              type: array
              items:
                type: array
                items:
                  oneOf:
                    - type: number
                    - type: array
                      items: { type: number }

    TripUpdate:
      type: object
      description: |
        A GTFS-Realtime TripUpdate. Field names follow the Protobuf schema in
        camelCase.
      properties:
        trip: { $ref: '#/components/schemas/RtTripDescriptor' }
        vehicle: { $ref: '#/components/schemas/RtVehicleDescriptor' }
        stopTimeUpdate:
          type: array
          description: Predictions for the stops ahead on this trip.
          items:
            type: object
            properties:
              stopSequence: { type: integer }
              stopId: { type: string }
              arrival: { $ref: '#/components/schemas/StopTimeEvent' }
              departure: { $ref: '#/components/schemas/StopTimeEvent' }
        timestamp: { type: string, description: Unix timestamp of this update, as a string. }

    RtTripDescriptor:
      type: object
      description: |
        GTFS-Realtime TripDescriptor — which scheduled trip an update refers
        to. Shared by trip updates and vehicle positions.
      properties:
        tripId: { type: string }
        routeId: { type: string }
        directionId:
          type: integer
          description: Direction as an integer here, unlike the string on static trips.
        startDate:
          type: string
          description: Service date the trip started, `YYYYMMDD`.
          examples: ['20260818']
        scheduleRelationship:
          type: string
          description: How this trip relates to the schedule.
          examples: ['SCHEDULED', 'ADDED', 'CANCELED']
        tripHeadsign: { type: [string, 'null'] }

    RtVehicleDescriptor:
      type: object
      description: GTFS-Realtime VehicleDescriptor — which vehicle is running the trip.
      properties:
        id: { type: string, description: Agency's vehicle identifier. }
        label: { type: [string, 'null'], description: Fleet number shown to riders. }
        licensePlate:
          type: [string, 'null']
          description: Often an empty string where the agency does not publish it.

    StopTimeEvent:
      type: object
      description: A predicted arrival or departure.
      properties:
        time: { type: string, description: Predicted Unix timestamp, as a string. }
        delay:
          type: integer
          description: Seconds behind schedule; negative is early. Absent when not published.

    VehiclePosition:
      type: object
      description: A GTFS-Realtime VehiclePosition.
      properties:
        trip: { $ref: '#/components/schemas/RtTripDescriptor' }
        vehicle: { $ref: '#/components/schemas/RtVehicleDescriptor' }
        position:
          type: object
          description: Coordinates here are numbers, not strings.
          properties:
            latitude: { type: number }
            longitude: { type: number }
            bearing: { type: number, description: Degrees clockwise from north. }
            speed: { type: number, description: Metres per second, where published. }
        currentStopSequence: { type: integer }
        currentStatus:
          type: string
          description: |
            Where the vehicle is relative to `stopId` — GTFS-Realtime's
            VehicleStopStatus enum.
          examples: ['STOPPED_AT', 'IN_TRANSIT_TO', 'INCOMING_AT']
        stopId: { type: string }
        timestamp: { type: string }
        occupancyStatus:
          type: string
          description: GTFS-Realtime occupancy enum, where the agency publishes it.
          examples: ['FEW_SEATS_AVAILABLE']

    Alert:
      type: object
      description: |
        A GTFS-Realtime Alert. Text fields are TranslatedStrings; pick the
        translation matching your locale.
      properties:
        id: { type: string }
        headerText: { $ref: '#/components/schemas/TranslatedString' }
        descriptionText: { $ref: '#/components/schemas/TranslatedString' }
        informedEntity:
          type: array
          description: The stops, routes or trips this alert applies to.
          items:
            type: object
            properties:
              agencyId: { type: [string, 'null'] }
              routeId: { type: [string, 'null'] }
              stopId: { type: [string, 'null'] }

    TranslatedString:
      type: object
      description: GTFS-Realtime translated text.
      properties:
        translation:
          type: array
          items:
            type: object
            properties:
              text: { type: string }
              language: { type: string, examples: ['en'] }

    Annotation:
      type: object
      description: An operator note or override attached to a GTFS entity.
      properties:
        id: { type: integer }
        annotation_type:
          type: string
          enum: [comment, override]
          description: |
            `comment` adds a message; `override` redirects predictions to
            another entity.
        entity_type: { type: string, examples: ['stop'] }
        entity_id: { type: string }
        secondary_entity_type:
          type: [string, 'null']
          description: Narrows the annotation, e.g. one route at one stop.
        secondary_entity_id: { type: [string, 'null'] }
        body: { type: string, description: Processed HTML. }
        override_entity_id:
          type: [string, 'null']
          description: Replacement entity, for overrides.
        active: { type: boolean, description: Whether it is within its time window now. }
