Skip to content

Export API v2

Upcoming

Export API v2 is not yet available in production. This page describes an upcoming release, and details can still change until it ships. If you want to prepare your integration or try v2 early, contact h2-stations@clean-hydrogen.europa.eu.

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 four signal types
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 /fuel-type Fuelling options (700ld, 350ld, 700hd, 350hd)
GET /vehicle-type EU vehicle categories, resolving vehicle_types codes

Two differences from v1.1 to note up front:

  • GET /hrs is 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_option query 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) plus three layout sections:

{
  "hrs_id": "ABCDE",
  "...": "all v1.1 fields unchanged",
  "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": 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 on nozzles is always null for now. It is reserved for filling-protocol details in a future release, do not build against its content yet.

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.


Real-time status: four sections

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"}
    ]
  }
}

Each of the four 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 sections null). Build your consumer to treat every section as optional.

Field notes:

  • datetime is when the source measured the reading, received_datetime is when the platform ingested it. Use datetime for freshness decisions.
  • availability.station carries the whole-station flags. maintenance: true or limitedly_available: true overrides the per-nozzle picture.
  • usage_status is one of free, in-use, reserved.
  • hydrogen_storage exposes only the derived boolean low_h2_storage per storage. The numeric inventory is confidential and never published.
  • Prices are decimal strings ("13.990"), per kg. gross_price is VAT-inclusive retail, net_price (nullable) is pre-VAT. currency is an ISO 4217 code.
  • The id values reference external_ids from the station object's dispensers, storages and products sections. Resolve a price id against products to 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 pricing section can list fewer products than the station's last price signal carried, and the section itself is null when none of the priced products are registered.

GET /hrs/status returns the same objects for all stations, wrapped as {"stations": [ ... ]}.

The v1-style per-fuelling-option status remains available on v1.1, and v2 does not replace it for consumers who only need the five-state summary.


Conditional requests

GET /hrs and GET /hrs/{hrs_id} support conditional requests via Last-Modified / If-Modified-Since:

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

If nothing changed you get a 304 Not Modified with an empty body, which does not count against your patience or bandwidth. The timestamp tracks changes to the exported static content (including layout and related records), not real-time signals. Status endpoints are not conditional, poll them normally.

There is no ETag support, use If-Modified-Since.


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. If you have a portal account, you create tokens yourself under API tokens: pick a name and an expiry, and the value is shown once on the confirmation page. Otherwise request one via h2-stations@clean-hydrogen.europa.eu and we email you a personal, single-use link that reveals it once. 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 photo endpoint returns 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