Export API v2¶
Added in portal 1.9.0 · 29 July 2026
Export API v2 extends the station dataset with the registered station layout (dispensers, nozzles, storages) and widens real-time data from availability alone to four signal types: availability, usage, hydrogen storage, and pricing. This page covers what is new against v1.1 and how to consume it.
If you are new to the export API, read the For Data Users overview first. Everything documented there for v1.1 (the static dataset, authentication, licensing) applies to v2 as well unless this page says otherwise.
The authoritative machine-readable schema is the interactive reference at https://api.h2-stations.eu/v2/doc/ (OpenAPI document at /v2/doc/schema).
Endpoints¶
The base URL is https://api.h2-stations.eu/v2. All endpoints require Authorization: Bearer <your-token>.
| Endpoint | Description |
|---|---|
GET /hrs |
List all registered stations, paginated |
GET /hrs/{hrs_id} |
One station with full static details and layout |
GET /hrs/status |
Real-time status of all stations, all sections |
GET /hrs/{hrs_id}/status |
Real-time status of one station |
GET /hrs/{hrs_id}/events |
Ongoing and upcoming events of a station |
GET /hrs/{hrs_id}/photo |
Station photo (binary image) |
GET /hrs/{hrs_id}/photo/thumbnail |
Station photo scaled to fit 250x250 pixels (binary image) |
GET /fuel-type |
Fuelling options (700ld, 350ld, 700hd, 350hd) |
GET /vehicle-type |
EU vehicle categories, resolving vehicle_types codes |
GET /protocol-type |
Protocol reference data, resolving protocol_configs codes |
Two differences from v1.1 to note up front:
GET /hrsis paginated (v1 and v1.1 return the full list in one response).- The status endpoints changed shape: v2 reports per signal type against the station layout, not per fuelling option, and the
fuelling_optionquery filter from v1/v1.1 does not exist on v2.
Pagination¶
GET /hrs returns a pagination envelope:
{
"count": 312,
"next": "https://api.h2-stations.eu/v2/hrs?limit=100&offset=100",
"previous": null,
"results": [ ... ]
}
Control it with limit (default 100, maximum 500) and offset. Follow next until it is null to fetch the full dataset.
Station objects: what v2 adds¶
A v2 station object carries every field from v1.1 (including the AFIR dataset), the photo_modified_at stamp and three layout sections:
{
"hrs_id": "ABCDE",
"...": "all v1.1 fields unchanged",
"photo_modified_at": "2026-08-14T09:02:31Z",
"dispensers": [
{
"external_id": "DISP-01",
"is_certified": true,
"nozzles": [
{
"external_id": "NOZ-01",
"connector_type": "H70",
"vehicle_side": "left",
"is_certified": true,
"is_effectively_certified": true,
"fuelling_options": ["700ld"],
"protocol_configs": [
{
"governing_protocol": "J2601-1",
"standard_edition": "SAE J2601-1:2020",
"control_methods": ["LOOKUP_TABLE"],
"communication_modes": ["J2799_V1"],
"pre_cooling_categories": ["T40"],
"serviceable_chss_categories": [
{"governing_protocol": "J2601-1", "name": "A", "nominal_working_pressure": "H70"},
{"governing_protocol": "J2601-1", "name": "B", "nominal_working_pressure": "H70"}
],
"tank_type": null,
"fuelling_mass_flow_class": null
}
]
}
]
}
],
"storages": [
{
"external_id": "STOR-01",
"nominal_capacity_kg": 500.0,
"dispenser_external_ids": ["DISP-01"]
}
],
"products": [
{
"external_id": "PROD-700",
"pressure_stage": 700,
"green_hydrogen_share": 42.5,
"nozzle_external_ids": ["NOZ-01"]
}
]
}
Null semantics matter here. dispensers, storages and products are null for stations whose layout has not been registered and confirmed by the operator yet. Expect null for many stations initially, coverage grows as operators adopt the layout editor. Once a layout is confirmed, storages and products can also be an empty array, which is a real statement (this station registered zero storages, or sells no product it has registered) rather than an absence of data.
protocol_configs lists the refuelling protocols the operator declared for the nozzle. An empty list means the operator has not declared any yet. Each entry names one governing protocol and what the nozzle supports under it, as codes that GET /protocol-type resolves:
governing_protocol: the fuelling protocol, for exampleJ2601-1.standard_edition: the standard edition the station follows. When the operator did not choose one, this is the protocol's own standard.control_methods,communication_modesandpre_cooling_categories: lists of codes.serviceable_chss_categories: the vehicle tank (CHSS) categories the nozzle serves. Category names repeat across protocols and pressures, so each entry identifies its category bygoverning_protocol,nameandnominal_working_pressuretogether.tank_typeandfuelling_mass_flow_class: one code each, ornullwhen the protocol does not use them.
A change to a nozzle's protocol configurations counts as a change to the station, so conditional requests pick it up.
photo_modified_at is the time the station photo was last set, replaced or removed, and null when the station has never had one. It does not move when other station data changes, so it is a reliable signal for whether a cached image is still current. See Station photos for how to use it.
A product is what a price applies to: a pressure stage (350 or 700) sold at a station, optionally with a green_hydrogen_share (percentage of renewable hydrogen, null until the operator declares it). nozzle_external_ids maps the product to the nozzles that dispense it, so you can tell a driver which price applies at the nozzle they matched. It may be empty when the operator has not mapped the product yet.
The nozzle, storage and product external_id values are the identifiers you will see again in the status payloads below, so you can join live status to the layout.
Protocol types¶
Added in portal 1.11.0 · 21 October 2026
GET /protocol-type returns one object with the reference lists behind every code in protocol_configs:
| List | Identified by | Other fields |
|---|---|---|
governing_protocols |
code |
label, name, description, standard |
standards |
code |
title, lifecycle_status, supersedes |
control_methods |
code |
label, name, description |
communication_modes |
code |
label, name, description |
pre_cooling_categories |
code |
label, name, description |
chss_categories |
governing_protocol, name, nominal_working_pressure |
volume_min_l, volume_max_l, mass_min_kg, mass_max_kg |
tank_types |
code |
description |
fuelling_mass_flow_classes |
code |
name, max_g_s |
nominal_working_pressures |
code |
mpa |
label is a short display name and falls back to the code. name is the full name, and description gives the scope and technical detail. The standard of a governing protocol is the code of the protocol's own standard.
The lifecycle_status of a standard is IN_FORCE, SUPERSEDED, FORTHCOMING or WITHDRAWN. supersedes is the code of the edition it replaces, or null. The volume and mass ranges of a CHSS category are decimal strings, and an open upper bound is null.
Entries no longer offered to operators stay listed, since existing configurations can still reference them.
{
"governing_protocols": [
{
"code": "J2601-1",
"label": "J2601-1 LDV",
"name": "SAE J2601-1",
"description": "Light-duty vehicles, including H70 category D.",
"standard": "SAE J2601-1:2020"
}
],
"standards": [
{
"code": "SAE J2601-1:2020",
"title": "Fueling Protocols for Light Duty Gaseous Hydrogen Surface Vehicles",
"lifecycle_status": "IN_FORCE",
"supersedes": "SAE J2601-1:2016"
}
],
"chss_categories": [
{
"governing_protocol": "J2601-1",
"name": "A",
"nominal_working_pressure": "H70",
"volume_min_l": "49.70",
"volume_max_l": "99.40",
"mass_min_kg": "2.00",
"mass_max_kg": "4.00"
}
],
"...": "the other six lists, same pattern"
}
Real-time status: six sections¶
A status response carries six sections. Four of them are the signal types the platform ingests (availability, usage, hydrogen_storage, pricing), one per type. The other two are derived: fuel_types aggregates availability per fuel category, and events carries the events in progress. The platform computes both from data it already holds rather than receiving them from a signal source.
GET /hrs/{hrs_id}/status:
{
"hrs_id": "ABCDE",
"availability": {
"datetime": "2026-07-11T09:41:00+02:00",
"received_datetime": "2026-07-11T09:41:02+02:00",
"station": {"maintenance": false, "limitedly_available": false},
"nozzles": [
{"id": "NOZ-01", "available": true},
{"id": "NOZ-02", "available": false}
]
},
"usage": {
"datetime": "2026-07-11T09:41:00+02:00",
"received_datetime": "2026-07-11T09:41:02+02:00",
"nozzles": [
{"id": "NOZ-01", "usage_status": "free"},
{"id": "NOZ-02", "usage_status": "in-use"}
]
},
"hydrogen_storage": {
"datetime": "2026-07-11T09:40:00+02:00",
"received_datetime": "2026-07-11T09:40:01+02:00",
"storages": [
{"id": "STOR-01", "low_h2_storage": false}
]
},
"pricing": {
"datetime": "2026-07-11T06:00:00+02:00",
"received_datetime": "2026-07-11T06:00:01+02:00",
"products": [
{"id": "PROD-700", "gross_price": "13.990", "net_price": null, "currency": "EUR"},
{"id": "PROD-350", "gross_price": "9.500", "net_price": "7.983", "currency": "EUR"}
]
},
"fuel_types": {
"datetime": "2026-07-11T07:41:00+00:00",
"entries": [
{"id": "700ld", "status": "available", "reason": null},
{"id": "350ld", "status": "unavailable", "reason": "in-maintenance"}
]
},
"events": {
"ongoing": [
{
"id": 4711,
"start_datetime": "2026-07-11T08:00:00+02:00",
"end_datetime": "2026-07-11T16:00:00+02:00",
"subject": "Maintenance activities",
"message": "Please contact the technician on site for support with your filling during maintenance work.",
"expected_availability_status": "limitedly-available"
}
]
}
}
Each of the four signal sections is null when no live signal source publishes that type for the station. A station can have live pricing and no usage data, or nothing at all (all four null). Build your consumer to treat every signal section as optional.
The two derived sections have their own null semantics. fuel_types is null while the station has no availability monitoring record at all. events is never null: a station with no ongoing event carries an empty ongoing array.
Field notes:
datetimeis when the source measured the reading,received_datetimeis when the platform ingested it. Usedatetimefor freshness decisions.availability.stationcarries the whole-station flags.maintenance: trueorlimitedly_available: trueoverrides the per-nozzle picture.usage_statusis one offree,in-use,reserved.hydrogen_storageexposes only the derived booleanlow_h2_storageper storage. The numeric inventory is confidential and never published.- Prices are decimal strings (
"13.990"), per kg.gross_priceis VAT-inclusive retail,net_price(nullable) is pre-VAT.currencyis an ISO 4217 code. - The
idvalues referenceexternal_ids from the station object'sdispensers,storagesandproductssections. Resolve a priceidagainstproductsto learn the pressure stage, the green-hydrogen share and the nozzles it applies at. - Prices are published only for products the operator currently has registered. When a product is removed, its price stops being published straight away, so a
pricingsection can list fewer products than the station's last price signal carried, and the section itself isnullwhen none of the priced products are registered.
The fuel_types section¶
Added in portal 1.10.0 · 2 October 2026
"fuel_types": {
"datetime": "2026-07-11T07:41:00+00:00",
"entries": [
{"id": "700ld", "status": "available", "reason": null},
{"id": "350ld", "status": "unavailable", "reason": "in-maintenance"}
]
}
idis a fuel category, from the same vocabulary asGET /fuel-typeand the station payload'sfuel_typesblock.statusis one ofavailable,limitedly-available,unavailable,closed,unknown.reasonisnullor one offailure,in-maintenance,outside-opening-hours,not-in-operation,not-monitored,no-signal.- Entries appear only for the fuel categories the station itself lists.
datetimeis when the aggregated state last changed and can benull. It is reported in UTC, unlike the signal sections above, which carry the timestamp in the offset the data source sent.- The whole section is
nullwhile the station has no availability monitoring record.
Prefer availability.nozzles where granularity matters. fuel_types rolls the same state up per fuel category, as a convenience for consumers that only need a coarse summary. Two limits are worth stating: the rollup collapses several nozzles into a single state, and a fuel category is a platform grouping rather than a physical property of a station. What a vehicle can actually fuel from is a property of the nozzle (its connector coding, flow class and filling protocol), not of the category.
This is the same five-state per-fuel-category summary that v1.1 publishes, so a consumer that only needs that summary can now stay on v2.
The events section¶
Added in portal 1.10.0 · 2 October 2026
"events": {
"ongoing": [
{
"id": 4711,
"start_datetime": "2026-07-11T08:00:00+02:00",
"end_datetime": "2026-07-11T16:00:00+02:00",
"subject": "Maintenance activities",
"message": "Please contact the technician on site for support with your filling during maintenance work.",
"expected_availability_status": "limitedly-available"
}
]
}
idis new in v2 and is the stable handle to key on across polls. An event keeps it for its whole lifetime.expected_availability_statusis one ofavailable,limitedly-available,unavailableand states the availability the operator expects while the event lasts.end_datetimeisnullfor an event with no planned end.- The section covers events in progress only. For events that have not started yet, see Station events.
Choosing sections¶
Added in portal 1.10.0 · 2 October 2026
Both status endpoints accept a sections query parameter, a comma-separated list of section names. Omit it to get all six.
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.h2-stations.eu/v2/hrs/status?sections=availability,pricing"
A section you do not request is not merely filtered out of the response: its data source is never consulted. That is what makes the parameter worth using. Asking for availability alone skips the product registration lookup behind pricing, the availability monitoring records behind fuel_types and the event query behind events.
Valid names are availability, usage, hydrogen_storage, pricing, fuel_types and events. An unknown name returns 400 Bad Request with a detail listing the unknown names and the valid ones. Empty values and stray commas are tolerated and mean all sections.
Status of all stations¶
GET /hrs/status returns the same objects for all stations, wrapped in an envelope that also carries the time the platform assembled the response, in UTC:
{
"datetime": "2026-07-11T07:41:05.132487+00:00",
"stations": [ ... ]
}
Station events¶
GET /hrs/{hrs_id}/events lists the ongoing and upcoming events of one station:
{
"hrs_id": "ABCDE",
"events": [
{
"start_datetime": "2026-06-15T08:00:00+02:00",
"end_datetime": "2026-06-15T17:00:00+02:00",
"subject": "Maintenance activities",
"message": "Annual inspection of the 700 bar dispenser. Refuelling is possible with technician assistance.",
"expected_availability_status": "limitedly-available"
}
]
}
The event objects are the ones the events status section carries, with one difference: this endpoint does not emit id. Use this endpoint for the forward-looking view and the status section for what is in progress right now. An event that has not started yet appears here once the operator has announced it, and an event that has ended stops appearing. All timestamps are ISO 8601 with the station's UTC offset.
Conditional requests¶
Most v2 read endpoints support conditional requests. Which validator to send back depends on the endpoint:
| Endpoints | Response headers | Send back as |
|---|---|---|
GET /hrs, GET /hrs/{hrs_id} |
Last-Modified |
If-Modified-Since |
GET /hrs/status, GET /hrs/{hrs_id}/status |
weak ETag |
If-None-Match |
GET /hrs/{hrs_id}/photo, GET /hrs/{hrs_id}/photo/thumbnail |
ETag and Last-Modified |
If-None-Match or If-Modified-Since |
The remaining endpoints (GET /hrs/{hrs_id}/events, GET /fuel-type, GET /vehicle-type, GET /protocol-type) are not conditional. Request them normally.
A 304 Not Modified comes with an empty body, so it saves you the transfer. It still counts as one request against your rate limits.
Station data¶
GET /hrs and GET /hrs/{hrs_id} validate with Last-Modified:
curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" \
https://api.h2-stations.eu/v2/hrs
Take the Last-Modified response header and send it back:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
-H "If-Modified-Since: Fri, 11 Jul 2026 07:41:00 GMT" \
https://api.h2-stations.eu/v2/hrs
The timestamp tracks changes to the exported static content (including layout and related records), not real-time signals. HTTP dates carry no sub-second part, so a change made within the same second as your stamp reaches you on your next request at the latest.
Real-time status¶
Added in portal 1.10.0 · 2 October 2026
GET /hrs/status and GET /hrs/{hrs_id}/status validate with a weak ETag. Take the ETag response header and send it back as If-None-Match:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: W/"v2-status-1487-29174352"' \
https://api.h2-stations.eu/v2/hrs/status
Two properties to build for:
- The tag is weak. It marks the payload as semantically equivalent, not byte-identical. Do not treat it as a checksum of the response body, and echo it back exactly as received,
W/prefix included. - The tag can be absent. A response without an
ETagis not an error. Poll normally while there is none, and resume sendingIf-None-Matchonce a tag comes back.
The tag also carries a time component, so it changes at least once every few minutes even when no status changed. A poll can therefore receive a full response whose content is identical to the one you already hold.
Station photos¶
Added in portal 1.10.0 · 2 October 2026
GET /hrs/{hrs_id}/photo and GET /hrs/{hrs_id}/photo/thumbnail carry both ETag and Last-Modified, and answer either If-None-Match or If-Modified-Since with a 304 Not Modified. The thumbnail is the same photo scaled to fit 250x250 pixels, behind the same rights gate as the full image.
The station payload carries photo_modified_at, which lets a client skip the request altogether:
- Read
photo_modified_atfrom the station object.nullmeans the station has never had a photo, so there is nothing to fetch. - If the value matches the one you fetched the image for, keep the image you have and make no request.
- Otherwise fetch the photo or the thumbnail and store the
ETagfrom the response alongside it. - On any later fetch of the same image, send that
ETagback asIf-None-Matchand expect a304while the image is unchanged.
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: "3f0c1d9a4b7e5c2081a6d4f39b0e7c15"' \
https://api.h2-stations.eu/v2/hrs/ABCDE/photo/thumbnail
Both endpoints respond with Cache-Control: private, must-revalidate. They are token gated, so the image must not be held in a shared cache.
Authentication and sandbox¶
The header format is unchanged from v1.1: Authorization: Bearer <your-token>, but v2 accepts personal tokens only. A v1/v1.1 export token does not authenticate on v2 (the response tells you so explicitly), and your v1/v1.1 integrations keep working with it unchanged. Each API generation has its own credential: personal tokens are rejected on v1 and v1.1.
A personal token is tied to a portal account, not to an organisation. You create tokens yourself under API tokens in the portal: pick a name and an expiry, and the value is shown once on the confirmation page. If you do not have a portal account yet, ask for one at h2-stations@clean-hydrogen.europa.eu. The value is never stored in readable form, so store it in a secret manager when you see it. A lost token is replaced, not looked up.
You can hold up to five active personal export tokens at a time, and you can change a token's expiry or revoke it from the same page. We email you when a token is created or revoked, and again 30 days and 7 days before it expires.
The shared sandbox token does not work on v2 yet (requests with it return 401). To experiment without your own token, use the v1.1 sandbox. With your own token, v2 works immediately.
Rate limits¶
v2 enforces two limits per user:
- 60 requests per minute (burst)
- 5000 requests per day (sustained)
All personal tokens of one user share that user's budget.
Exceeding either returns 429 Too Many Requests with a Retry-After header. Polling the bulk status endpoint once per minute plus a daily paginated sweep of /hrs fits comfortably. If your use case genuinely needs more, contact us.
Formats and licensing¶
All v2 endpoints return JSON (the two photo endpoints return a binary image). A DATEX II XML representation is planned but not yet available, this documentation will announce it.
The data is licensed under CC BY 4.0, identical to v1.1. See Licensing for the attribution text.
Contact¶
h2-stations@clean-hydrogen.europa.eu