Home Assistant
Dutch electricity prices with the energy tax and VAT already on them, the price you actually pay, as sensors in Home Assistant. Four steps, ten minutes.
1. Get an API key
Fill in your email address on the front page and the key appears on screen. No account, no waiting, free. It gives you 50,000 requests a day; a Home Assistant setup needs two or three.
Copy it now: only a hash of it is stored here, so a key can be replaced but never shown again.
2. Put the key in secrets.yaml
secrets.yaml sits next to configuration.yaml and is
the file that stays out of your screenshots, forum posts and git history.
# secrets.yaml
stroomprijzen_api_key: sp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3. Send it with every request
The key travels as a query parameter called apiKey, in a
params: block. Home Assistant builds the query string itself, so
the URL in resource: stays clean:
params:
apiKey: !secret stroomprijzen_api_key
Three things worth knowing about that block. It applies to the one
rest: entry it sits in, so every entry needs its own. Anything
else that belongs in the query goes in there too, such as date and
resolution, because a ? in resource: and
a params: block are not merged. And a key that is unknown or
revoked is served as if it were not there, with an
x-api-key-status header, rather than as a 401: your sensors keep
working, but a typo is invisible until you look.
4. Your first sensor
Paste this into configuration.yaml, restart Home Assistant, and
you have the current price:
rest:
- resource: https://stroomprijzenapi.nl/api/v1/prices/now
params:
apiKey: !secret stroomprijzen_api_key
scan_interval: 900
sensor:
- name: "Electricity price now"
unique_id: stroomprijs_now
value_template: "{{ value_json.point.allInEurKwh }}"
unit_of_measurement: "EUR/kWh"
state_class: measurement
json_attributes_path: "$.point"
json_attributes:
- localTime
- rawEurMwh
- energyTaxEurKwh
- odeEurKwh
- vatRate
Check it under Developer tools → States:
sensor.electricity_price_now should hold something like
0.3159, and its attributes the breakdown that adds up to it.
Use allInEurKwh, not rawEurKwh.
The raw field is the bare market price: in 2026 it sits about 0.11 EUR/kWh
below what a household pays, because energy tax and VAT are not in it. The
gap is nearly constant, so the wrong field looks plausible rather than
broken, which is why it gets shipped by accident.
Set Home Assistant's time zone to Europe/Amsterdam. A day
here is a Dutch calendar day, and now() returns your instance's
local time. On UTC, everything below is off by an hour or two.
5. More examples
Same shape as above, each with its own key. Open what you need.
Today and tomorrow, as two sensors
For charts and for automations that plan ahead you want whole days. The date is templated because the endpoint defaults to tomorrow once tomorrow has been published.
rest:
- resource: https://stroomprijzenapi.nl/api/v1/prices
params:
date: "{{ now().strftime('%Y-%m-%d') }}"
apiKey: !secret stroomprijzen_api_key
scan_interval: 21600
sensor:
- name: "Electricity prices today"
unique_id: stroomprijzen_today
value_template: "{{ value_json.summary.averageEurKwh }}"
unit_of_measurement: "EUR/kWh"
state_class: measurement
json_attributes:
- date
- count
- summary
- points
- resource: https://stroomprijzenapi.nl/api/v1/prices
params:
date: "{{ (now() + timedelta(days=1)).strftime('%Y-%m-%d') }}"
apiKey: !secret stroomprijzen_api_key
scan_interval: 3600
sensor:
- name: "Electricity prices tomorrow"
unique_id: stroomprijzen_tomorrow
value_template: "{{ value_json.count }}"
json_attributes:
- date
- count
- summary
- points
Tomorrow's prices appear around 14:00 CE(S)T. Before then the endpoint
returns count: 0 and an empty points array. That is
a normal 200 rather than an error, so guard your templates for it.
Add resolution: quarterhour to params: for
quarter-hourly prices. The Dutch market has traded on quarter-hours since
1 October 2025; an hourly price is the mean of its four quarters.
Cheapest hour, and "is it cheap right now"
These need the today sensor above. Adjust the thresholds to your own habits.
template:
- sensor:
- name: "Cheapest hour today"
unique_id: stroomprijs_cheapest_hour
state: >-
{% set p = state_attr('sensor.electricity_prices_today', 'points') %}
{% if p %}
{{ (p | sort(attribute='allInEurKwh') | first).localTime[11:16] }}
{% else %}
unknown
{% endif %}
- binary_sensor:
- name: "Electricity is cheap now"
unique_id: stroom_is_cheap
state: >-
{% set p = state_attr('sensor.electricity_prices_today', 'points') %}
{% set now_price = states('sensor.electricity_price_now') | float(-1) %}
{% if p and now_price >= 0 %}
{% set cheapest = (p | map(attribute='allInEurKwh') | sort | list)[:6] %}
{{ now_price <= cheapest[-1] }}
{% else %}
false
{% endif %}
That binary sensor is on during the six cheapest hours of the day. Change the
[:6] to whatever suits the appliance.
Running something when power is cheap
automation:
- alias: "Charge the car when power is cheap"
triggers:
- trigger: state
entity_id: binary_sensor.electricity_is_cheap_now
to: "on"
actions:
- action: switch.turn_on
target:
entity_id: switch.car_charger
On Home Assistant older than 2024.10, write trigger:,
platform:, action: and service: in
place of the newer keys.
A chart of the day (ApexCharts)
The points attribute plugs straight into
ApexCharts Card:
type: custom:apexcharts-card
graph_span: 24h
span:
start: day
series:
- entity: sensor.electricity_prices_today
name: All-in price
unit: EUR/kWh
type: column
data_generator: |
return entity.attributes.points.map(p => [
new Date(p.momentUtc).getTime(),
p.allInEurKwh
]);
Use momentUtc for the timestamp, not localTime:
Date handles the UTC instant unambiguously and the browser
renders it in the viewer's own zone.
The Energy dashboard
Settings → Dashboards → Energy, then under your grid consumption
source pick Use an entity with current price and select
sensor.electricity_price_now.
Because that sensor is the all-in price, the costs the dashboard reports are what you pay per kWh, excluding the fixed parts of your bill that no per-kWh price can express. Standing charges, the annual tax rebate and your supplier's markup all sit outside this API.
6. Details, once it works
Fetching two or three times a day instead of on a timer
Day-ahead prices change twice in twenty-four hours: tomorrow's are published
around 14:00 CE(S)T, and at midnight what was tomorrow becomes today. A
published day never changes again, so a long scan_interval plus
two automations beats any polling interval.
rest:
- resource: https://stroomprijzenapi.nl/api/v1/prices
params:
date: "{{ now().strftime('%Y-%m-%d') }}"
apiKey: !secret stroomprijzen_api_key
scan_interval: 86400
sensor:
- name: "Electricity prices today"
# ... as in section 5
automation:
- alias: "Prices: today, after the date rolls over"
triggers:
- trigger: homeassistant
event: start
- trigger: time
at: "00:05:00"
actions:
# Spread the load: without this every installation calls at 00:05:00 sharp.
- delay: "{{ range(0, 600) | random }}"
- action: homeassistant.update_entity
target:
entity_id: sensor.electricity_prices_today
- alias: "Prices: tomorrow, until it lands"
triggers:
- trigger: time_pattern
hours: "14"
minutes: "/20"
- trigger: time_pattern
hours: "15"
minutes: "/20"
conditions:
# Stop as soon as the sensor holds real data for the actual tomorrow.
# Without the date check it would stop early: before the first afternoon
# fetch the sensor still holds yesterday's idea of "tomorrow".
- condition: template
value_template: >-
{% set want = (now() + timedelta(days=1)).strftime('%Y-%m-%d') %}
{{ state_attr('sensor.electricity_prices_tomorrow', 'date') != want
or (state_attr('sensor.electricity_prices_tomorrow', 'count') | int(0)) == 0 }}
actions:
- delay: "{{ range(0, 300) | random }}"
- action: homeassistant.update_entity
target:
entity_id: sensor.electricity_prices_tomorrow
Two calls on a normal day, four when publication runs late. The random delays matter more than they look: fixed times mean every installation in the country arrives in the same second.
Deriving the current price without asking for it
Today's prices are already in Home Assistant, so the price this hour is a lookup rather than a request. This replaces the sensor from step 4: same entity name, same value, no requests at all.
template:
- triggers:
- trigger: time_pattern
minutes: "/15"
- trigger: state
entity_id: sensor.electricity_prices_today
sensor:
- name: "Electricity price now"
unique_id: stroomprijs_now
unit_of_measurement: "EUR/kWh"
state_class: measurement
state: >-
{% set points = state_attr('sensor.electricity_prices_today', 'points') or [] %}
{% set hit = points | selectattr('localTime', 'match', now().strftime('%Y-%m-%dT%H')) | list %}
{{ hit[0].allInEurKwh if hit else none }}
It matches on the local hour, so Home Assistant has to be on
Europe/Amsterdam. On quarter-hourly data, match
'%Y-%m-%dT%H:%M' against a quarter-hour boundary instead.
What a price point looks like
{
"momentUtc": "2026-08-14T22:00:00.000Z",
"localTime": "2026-08-15T00:00:00+02:00",
"rawEurMwh": 169.45,
"rawEurKwh": 0.16945,
"energyTaxEurKwh": 0.09161,
"odeEurKwh": 0,
"vatRate": 0.21,
"exclVatEurKwh": 0.26106,
"allInEurKwh": 0.315883
}
A Dutch day has 23 or 25 entries on the clock-change days in March and
October. Never assume 24, and never line two series up by array position;
match on momentUtc.
Checking the tax numbers yourself
The tax layer is the part you should not have to take on faith. /api/v1/tax-rates publishes the rates and the formula, and /api/v1/test-vectors gives a worked amount for every tax year since 2019, with the sum written out:
(100.0 / 1000 + 0.09161 + 0) * (1 + 0.21) = 0.2318481
Those come from the official Belastingdienst tables, computed with exact decimal arithmetic rather than by running the API, so they check it rather than agree with it by construction.
When something looks wrong
The price is far lower than my energy bill
You are reading rawEurKwh. Use allInEurKwh.
The prices are shifted by an hour or two
Home Assistant is not on Europe/Amsterdam, or something is building its own
UTC-midnight window. A Dutch day starts at 22:00Z in summer and 23:00Z in
winter, so pass date and let the API work it out.
Tomorrow is empty
Normal before roughly 14:00 CE(S)T. Check /api/v1/status to see when data last landed.
My key shows no usage
Either it is not being sent or it is not recognised, and both look like working sensors, because a key we do not recognise is served anonymously. Ask the API what it thinks:
curl -s "https://stroomprijzenapi.nl/api/v1/keys/me?apiKey=sp_live_xxxx"
That answers with your tier, your limit and what you spent per day. Counts staying low is not by itself a fault: keyed URLs are cached, so what you see there is a lower bound.