Skip to content

Sending Signals (API v2)

Upcoming

Signal 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.

Signal API v2 splits signal transmission into one endpoint per signal category. This page is the integration reference: endpoints, the shared payload envelope, per-category payload formats with examples, validation rules, and the responses you can expect.

If you are moving an existing v1 or v1.1 integration, read the Migration Guide first. It explains what changed and in which order to proceed.


Endpoints

Endpoint Purpose
POST https://api.h2-stations.eu/import/v2/signal/availability/ Which nozzles can currently dispense, plus whole-station state
POST https://api.h2-stations.eu/import/v2/signal/usage/ Whether each nozzle is free, in use, or reserved
POST https://api.h2-stations.eu/import/v2/signal/hydrogen-storage/ On-site hydrogen inventory, as kilograms or as a low-inventory flag
POST https://api.h2-stations.eu/import/v2/signal/pricing/ Current hydrogen price per registered product
GET https://api.h2-stations.eu/import/v2/active-stations/ The stations your token is assigned to, per signal type

Note the trailing slash. The v1 and v1.1 endpoints are written without one, the v2 endpoints require it.

Every request needs two headers:

Header Value
Authorization Bearer <your-v2-token>
Content-Type application/json

v2 uses a new token system. Tokens are created in the portal by originator managers, or issued by H2-Stations during onboarding, and a v1 token does not authenticate against v2 endpoints. See Authentication in the migration guide.


The shared envelope

The request body of every signal endpoint is a JSON array of transmissions. One request may carry transmissions for several stations, but at most one transmission per station. Every transmission starts from the same envelope:

Field Type Required Notes
hrs string yes The station's HRS ID (e.g. ABCDE). The station must exist on the platform.
datetime string yes When the reading was measured, ISO 8601 with UTC offset. Readings older than 5 minutes are rejected, and so are readings more than 30 seconds in the future.
transmission_id string no Free-form identifier for your transmission run. Stored for log correlation, useful when we investigate an issue together.
dry_run boolean no Defaults to false. When true, the transmission is validated end to end but nothing is stored or published. See Testing with dry_run.

Each category adds its own payload section on top of the envelope: nozzles, storages, or products.

Batches are all or nothing. The whole request is validated before anything is stored. If any transmission in the array fails validation, the request returns 400 and no transmission from that request is applied, including the valid ones. This differs from v1, where valid signals were accepted even when the overall response was a 400.

Transmissions are full snapshots. Each accepted transmission replaces the previous state for that station and signal category. An entity you omit from a transmission no longer has reported state afterwards, so include every nozzle, storage, or product in every transmission.


Registered identifiers

v2 signals reference the station's registered layout. Nozzle, dispenser, storage, and product identifiers must exist in the portal for that station before you can send signals against them. The HRS operator registers the station layout through the portal, and H2-Stations can register it with you during onboarding.

The API never guesses: an unknown identifier is rejected with a 400 naming the identifier, and only identifiers from a layout the operator has confirmed will resolve. If a 400 names an identifier you believe is correct, the usual cause is that the operator has not confirmed that part of the layout yet.

Dispenser shorthand (availability and usage): if your telemetry is only available per dispenser, send dispenser_id instead of id in a nozzle entry. The platform fans the state out to every nozzle of that dispenser. You can mix nozzle entries and dispenser entries in one transmission as long as they do not overlap, and each entry must carry exactly one of the two fields.


Availability signal

Reports which nozzles can currently dispense fuel correctly, plus the whole-station state.

In addition to the envelope:

  • nozzles (array, required, must not be empty). Each entry carries id (a registered nozzle identifier) or dispenser_id (a registered dispenser identifier), plus available (boolean, required, null is rejected).
  • station (object, required): maintenance and limitedly_available, both boolean and both required. These describe the whole station and override the per-nozzle flags downstream: a station in maintenance is treated as out of service regardless of what its nozzles report.
curl -X POST https://api.h2-stations.eu/import/v2/signal/availability/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "hrs": "ABCDE",
      "transmission_id": "run-2041",
      "datetime": "2026-07-11T09:41:00+02:00",
      "nozzles": [
        {"id": "NOZ-01", "available": true},
        {"id": "NOZ-02", "available": false},
        {"dispenser_id": "DISP-02", "available": true}
      ],
      "station": {"maintenance": false, "limitedly_available": false}
    }
  ]'
import datetime
import requests

TOKEN = "..."  # your v2 token

transmission = {
    "hrs": "ABCDE",
    "transmission_id": "run-2041",
    "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "nozzles": [
        {"id": "NOZ-01", "available": True},
        {"id": "NOZ-02", "available": False},
        {"dispenser_id": "DISP-02", "available": True},
    ],
    "station": {"maintenance": False, "limitedly_available": False},
}

response = requests.post(
    "https://api.h2-stations.eu/import/v2/signal/availability/",
    json=[transmission],
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=10,
)
response.raise_for_status()  # 204 on success

Before you copy-paste: replace the datetime value with the current time. Readings older than 5 minutes are rejected.

The platform derives the published per-fuelling-option status from your nozzle-level data and the station layout. If the layout does not allow any fuelling option to be derived, the transmission is rejected with a 400 asking to check the station registration.


Usage signal

Reports whether each nozzle is currently free, dispensing, or reserved. There is no station object on this signal.

  • nozzles (array, required, must not be empty). Each entry carries id or dispenser_id plus usage_status (string, required), one of:
Value Meaning
free Nozzle idle, ready to serve the next vehicle
in-use Nozzle currently dispensing
reserved Nozzle reserved for a known incoming vehicle

When a multi-nozzle dispenser cannot serve two vehicles at once, report the blocked sibling nozzles as in-use too, since they cannot serve a vehicle right now.

curl -X POST https://api.h2-stations.eu/import/v2/signal/usage/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "hrs": "ABCDE",
      "datetime": "2026-07-11T09:41:00+02:00",
      "nozzles": [
        {"id": "NOZ-01", "usage_status": "free"},
        {"id": "NOZ-02", "usage_status": "in-use"},
        {"id": "NOZ-03", "usage_status": "reserved"}
      ]
    }
  ]'
import datetime
import requests

TOKEN = "..."  # your v2 token

transmission = {
    "hrs": "ABCDE",
    "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "nozzles": [
        {"id": "NOZ-01", "usage_status": "free"},
        {"id": "NOZ-02", "usage_status": "in-use"},
        {"id": "NOZ-03", "usage_status": "reserved"},
    ],
}

response = requests.post(
    "https://api.h2-stations.eu/import/v2/signal/usage/",
    json=[transmission],
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=10,
)
response.raise_for_status()

Hydrogen storage signal

Reports the station's on-site hydrogen inventory per storage unit, so that drivers can be warned before arriving at a station that is running low.

  • storages (array, required, must not be empty). Each entry carries the registered storage id plus at least one of:
    • remaining_kg (decimal string or number, up to 8 digits with 2 decimal places, not negative): the current usable inventory. The platform applies the low-inventory threshold itself.
    • low_h2_storage (boolean): send this if you prefer to apply the threshold on your side or your system has no numeric reading. true means inventory is below the threshold.

The threshold is 100 kg per storage (following the current AFIR requirements). A storage at exactly 100 kg is not low. If you send both fields and they contradict each other at that threshold, the transmission is rejected with a 400 explaining the inconsistency.

The numeric inventory is treated as confidential. Data consumers only ever see the derived low-inventory flag, never the kilogram value.

curl -X POST https://api.h2-stations.eu/import/v2/signal/hydrogen-storage/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "hrs": "ABCDE",
      "datetime": "2026-07-11T09:41:00+02:00",
      "storages": [
        {"id": "STOR-01", "remaining_kg": "84.00"},
        {"id": "STOR-02", "low_h2_storage": false}
      ]
    }
  ]'
import datetime
import requests

TOKEN = "..."  # your v2 token

transmission = {
    "hrs": "ABCDE",
    "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "storages": [
        {"id": "STOR-01", "remaining_kg": "84.00"},
        {"id": "STOR-02", "low_h2_storage": False},
    ],
}

response = requests.post(
    "https://api.h2-stations.eu/import/v2/signal/hydrogen-storage/",
    json=[transmission],
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=10,
)
response.raise_for_status()

Pricing signal

Reports the current hydrogen price per product. Products are registered entities, like nozzles and storages: each station registers the products it sells (pressure stage, declared green-hydrogen share, the nozzles that dispense it), and the pricing signal references the product identifier. There is one price per station and product, prices are not keyed to nozzles or dispensers.

Products are registered by the operator in the portal, not through the import API. The identifiers to send are published in the products section of the export API v2 station payload. A price for an identifier the station has not registered is rejected with a validation error naming it, so read the products section to learn what you may price. If the operator removes a product later, its last price stops being published immediately, without waiting for your next transmission.

  • products (array, required, must not be empty). Each entry:
Field Type Required Notes
id string yes A registered product identifier. Duplicates within one transmission are rejected.
gross_price decimal string yes The VAT-inclusive retail price per kg, what a driver pays at the dispenser. Up to 8 digits with 3 decimal places, strictly greater than zero.
net_price decimal string no The pre-VAT price per kg for professional consumers. Same format, must not exceed gross_price.
currency string no ISO 4217 code, defaults to EUR. Accepted: EUR, CHF, GBP, DKK, SEK, NOK, PLN, CZK, HUF, RON.

All products within one transmission must use the same currency, a mix is rejected. A zero price is never valid: if you operate a free-of-charge station, contact us instead of sending a zero.

curl -X POST https://api.h2-stations.eu/import/v2/signal/pricing/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "hrs": "ABCDE",
      "datetime": "2026-07-11T09:41:00+02:00",
      "products": [
        {"id": "PROD-700", "gross_price": "13.990"},
        {"id": "PROD-350", "gross_price": "9.500", "net_price": "7.983", "currency": "EUR"}
      ]
    }
  ]'
import datetime
import requests

TOKEN = "..."  # your v2 token

transmission = {
    "hrs": "ABCDE",
    "datetime": datetime.datetime.now(datetime.timezone.utc).isoformat(),
    "products": [
        {"id": "PROD-700", "gross_price": "13.990"},
        {"id": "PROD-350", "gross_price": "9.500", "net_price": "7.983", "currency": "EUR"},
    ],
}

response = requests.post(
    "https://api.h2-stations.eu/import/v2/signal/pricing/",
    json=[transmission],
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=10,
)
response.raise_for_status()

Checking your assignments: active-stations

GET /import/v2/active-stations/ lists the stations your token is assigned to, grouped per signal type and per assignment state:

curl -H "Authorization: Bearer $TOKEN" https://api.h2-stations.eu/import/v2/active-stations/
{
  "stations": {
    "availability": {"live": ["ABCDE"], "testing": ["FGHIJ"]},
    "usage": {"live": ["ABCDE"], "testing": []},
    "hydrogen-storage": {"live": [], "testing": ["ABCDE"]},
    "pricing": {"live": ["ABCDE"], "testing": []}
  }
}

Assignments in v2 are per signal type. For each station and each of the four signal types, your system is either:

State What happens to your signals
live Signals are processed and published. They drive the public status.
testing Signals are accepted and validated end to end, but not published. Use this to verify your integration before going live.
not assigned Signals are still accepted and visible to us for debugging, but they are not published and do not build up history.

The same system can be live for availability and testing for pricing on the same station. The HRS operator controls these assignments, so if active-stations does not show a station you expect, contact the operator or H2-Stations.


Testing with dry_run

Set "dry_run": true on a transmission to validate it end to end (authentication, schema, identifier resolution, value ranges) without storing or publishing anything. A dry run that returns 204 confirms your payload would have been accepted.

Dry-run transmissions still count against the rate limit, and the dry_run flag applies per transmission, so one batch can mix real and dry-run entries.


Responses

204 No Content: success

All transmissions in the batch were accepted (or validated, for dry runs). The response body is empty. There is no 200 OK with a body.

Acceptance does not by itself publish anything. Publication depends on the per-type assignment state described under active-stations.

400 Bad Request: validation failure

At least one transmission failed validation and the whole batch was rejected, including entries that were individually valid. The body is a JSON array aligned with your request: an empty object {} for transmissions that passed, and an object mapping field names to error messages for those that failed.

[
  {},
  {
    "nozzles": [
      "Unknown nozzle identifier: NOZ-99."
    ]
  }
]

Common validation errors and their causes:

Error message (excerpt) Cause
Unknown station identifier: ... The hrs value does not match a registered station
Unknown nozzle identifier / dispenser identifier / storage identifier / product identifier: ... The identifier is not registered, or the operator has not confirmed that part of the layout yet
Duplicate nozzle reference / storage reference / product reference: ... The same entity appears twice in one transmission
Duplicate station in batch: ... Two transmissions in one request address the same station
The signal may not be older than 5 minutes. Stale datetime, often a catch-up burst after an outage
The signal may not be from the future. Clock skew beyond the 30-second tolerance
Provide exactly one of "id" or "dispenser_id". A nozzle entry carries both fields or neither
Provide at least one of "remaining_kg" or "low_h2_storage". An empty storage entry
Inconsistent low_h2_storage for storage ... Numeric inventory and boolean flag contradict each other at the 100 kg threshold
Inconsistent price for product ... net_price exceeds gross_price
Mixed currencies in one transmission: ... Products in one transmission use different currencies
No fuelling option could be derived from the station layout; check the station registration. The station layout does not map to any fuelling option yet

401 Unauthorized

The Authorization header is missing or malformed, the token is unknown or has been revoked, or it is a v1 token. The body is {"detail": "<message>"}. Nothing is processed.

429 Too Many Requests

You exceeded the rate limit of 300 requests per minute per signal originator (across all v2 endpoints together). The response carries a Retry-After header telling you how long to wait. If your fleet legitimately needs a higher rate, contact us.

500 / 503

500 Internal Server Error is an unexpected platform error, please report it. 503 Service Unavailable is temporary, retry with backoff.


  • Availability and usage: send every minute, even when nothing changed. A station the platform has not heard from in 30 minutes falls back to unknown, and the AFIR reporting cadence expects one signal per minute.
  • Pricing: send on change plus a daily heartbeat.
  • Hydrogen storage: send on change, or on the availability cadence if that is simpler for your transmitter.

Remember the 5-minute freshness ceiling on datetime: after a network outage, send a fresh reading rather than replaying the queue.


Common integration questions

My identifiers are rejected but the operator says the layout is registered. Identifiers only resolve once the operator has confirmed (validated) the layout in the portal. Draft layouts are invisible to the API. Ask the operator to complete the confirmation step.

Signals return 204 but nothing shows on the map. Check GET /import/v2/active-stations/. The station is probably in testing for that signal type, or not assigned to your system at all. Both are normal during onboarding.

Can I send several stations in one request? Yes, one transmission per station per request. But note the all-or-nothing batch semantics: one invalid transmission rejects the whole request. Many integrators prefer one station per request for simpler error handling.

Which currency codes are accepted? EUR, CHF, GBP, DKK, SEK, NOK, PLN, CZK, HUF, RON. The default is EUR, and one transmission must stick to a single currency.

Is there a machine-readable schema for v2? Yes. The interactive reference for v2 is at /import/v2/doc/, with the OpenAPI document at /import/v2/doc/schema. The earlier versions stay available at /import/v1/doc/ and /import/v1.1/doc/.


Contact

h2-stations@clean-hydrogen.europa.eu