Skip to main content

Publish a GBFS feed

Bikeshare and scooter programs are usually asked for GBFS by two audiences at once: the city that issued the permit, and the aggregators and trip planners riders actually use. Both need the same thing, a discovery document at a stable URL with current member files under it. This page covers getting from a query to that.

What has to be true

A GBFS feed is a set of files. The address you hand out answers with gbfs.json, the discovery document, and that document names the member files underneath it. A publish attempt validates the whole system against the version's JSON Schemas before anything is served, and GBFS findings are always blocking.

Two versions are available, 2.3 and 3.0. If your permit or your consumer names one, pick that. Otherwise pick the version your largest consumer reads today, and plan to publish the other later.

Which vocabulary you are mapping

The form asks for a shape before it asks for a mapping, and the shape determines which field list you see. Both shapes ship in a community build.

Stations, for a docked system: one row per station. The required list is longer than GTFS-Realtime's, mostly because station status carries several booleans a rider app has to be able to trust.

RequiredOptional
station_id, name, lat, lonnum_docks_available
num_vehicles_available, last_reportedcapacity
is_installed, is_renting, is_returningaddress

You map that as one list. The split into station_information and station_status happens automatically.

Vehicles, for a free-floating system: one row per vehicle. The list is shorter, since a free-floating vehicle has no dock state to report.

RequiredOptional
vehicle_id, lat, loncurrent_range_meters
is_reserved, is_disabled, last_reported

A field outside the shape's list is refused rather than skipped silently.

Before you start

  • A query returning one row per station, or one row per vehicle for a free-floating system, which has run at least once.
  • An administrator account, since publishing is administered.
  • The system facts, which no query returns and which you type into the form: a system id, a language, a display name and a timezone. On 3.0, also opening hours and a contact email. These are published as system_information.json.

The steps

1. Normalize before you bind

Skipping this step costs more in GBFS than elsewhere, because a single bad row blocks the entire system instead of degrading one file.

For example, a station whose last_reported sits below the earliest timestamp GBFS accepts blocks the whole publish, and the reason names the row. Source systems write values like that without complaint, but the standard rejects them.

Add a data source of type results and bind the feed to a query over the cached results of your connector read:

SELECT station_id, name, lat, lon,
num_bikes_available, num_docks_available,
is_installed, is_renting, is_returning,
last_reported
FROM cached_query_7
WHERE last_reported >= 1450155600
AND lat IS NOT NULL
AND lon IS NOT NULL

The same query is where you drop stations that are out of service, join a name table the upstream feed omits, convert a unit, or combine two systems into one published feed. See Normalizing before you publish.

A free-floating query works the same way over a different table: one row per vehicle, with the vehicles that have gone quiet filtered out before they reach the mapping.

2. Declare the feed

Connect → Feeds → Publish a feed. Source is the query above. The slug becomes the feed's address, so pick something a permit officer can read back over the phone: bikeshare-gbfs rather than feed2. It cannot be renamed later.

Visibility has to be Public for a permit or an aggregator to read it. A public slug is claimed across the whole instance rather than within your org, and requesting one another tenant already holds is refused with a 409 that does not say who holds it.

Under Shape, choose standard gbfs, then the version, then stations or vehicles. Fill in the system facts. Then map the fields for the shape you picked against your query's columns. Missing required fields are named when you submit, and the list updates as you map them.

3. Pick an on-failure mode

Block keeps serving the last artifact that passed, however old it gets. Last known good adds a maximum age, past which the address stops answering.

For a permit feed, consider which failure the city would rather see. A station list that is a day stale still describes a real system, while a 503 looks like the feed is down. Most operators pick block here and watch freshness separately, which is the opposite of the advice for vehicle positions.

4. Publish, and read what came back

Publish now runs a single attempt. A blocked attempt lists the validator's findings grouped by rule, with the individual locators behind a disclosure, so forty rows from one broken rule do not read as forty separate problems.

Where a rule reports more occurrences than it exported, the disclosure says showing 2 of 12 occurrences rather than presenting the visible count as the total.

5. Hand out the address

GET /api/public/feeds/<slug>

That answers with the discovery document. Which member files it names depends on the shape you published.

A stations feed:

/api/public/feeds/<slug>/station_information.json
/api/public/feeds/<slug>/station_status.json
/api/public/feeds/<slug>/system_information.json

A vehicles feed writes no station file, and its status file is named differently in each version, because 3.0 renamed the file, the data key inside it and the id field together:

/api/public/feeds/<slug>/free_bike_status.json # 2.3
/api/public/feeds/<slug>/vehicle_status.json # 3.0
/api/public/feeds/<slug>/system_information.json

Give the permit office and every aggregator the discovery URL rather than a member file. Consumers that follow the discovery document pick up files you add later without re-registering, and they do not hardcode the 2.3 spelling of a file you may publish as 3.0.

How you know it worked

There is no need to re-check conformance. The attempt could not have published without passing whole-system validation, and the feed's page carries the verdict and every finding. Running another validator over the same bytes only confirms that two validators agree.

What is worth checking is whether the feed is reachable from outside the instance. Follow the discovery document the way an aggregator would:

curl -s https://your-node.example.gov/api/public/feeds/bikeshare-gbfs
curl -s https://your-node.example.gov/api/public/feeds/bikeshare-gbfs/station_status.json

The first answers with gbfs.json naming the member files, and the second is one of them. If the discovery document resolves from outside your network and the file it names resolves too, a consumer can read your system. A failure at this point points at hosting rather than at the feed itself.

Then have the permit office or the aggregator confirm they can read it before you write the URL into anything.

What takes it off the air

What happenedWhat consumers see
A row the standard rejectsNothing publishes. The last clean artifact keeps serving, and the attempt names the row
You edited the live feedDark until a new attempt succeeds. The confirmation says so before you commit
The artifact aged past the cap under last known good503 with Retry-After
A member file the version requires is not in your mappingThe publish is blocked rather than partially served

What this does not cover

Vehicle types are not covered. Nothing here writes vehicle_types.json, so vehicle_type_id is not a field you can map, and a mixed fleet of bikes, e-bikes and scooters publishes as one undifferentiated list of vehicles. Raise that with the permit office before you commit to a date.

Reading another operator's system goes the other way and uses a connector: the GBFS connector takes a discovery URL and returns station information and status from it.