Home Assistant
Everything you need to read Dutch electricity prices into Home Assistant, including the energy tax and VAT — the price you actually pay, not the bare market price.
No account, no API key, no rate limit. Copy a block below into your
configuration.yaml, restart, and you have a sensor. The full
endpoint reference lives at /docs.
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 exactly why people ship it by accident.
1. The price right now
The smallest useful thing. One sensor, updated every five minutes. It is the quickest way to see something working — but it asks us the same question 288 times a day, and the answer only changes on the hour. Section 6 builds the same sensor out of today's prices with no repeated requests at all; once this one works, go and read that.
rest:
- resource: https://api.stroomwekker.nl/api/v1/prices/now
scan_interval: 300
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
The attributes give you the full breakdown, so a card can show what the price is made of rather than only the total.
Add ?resolution=quarterhour to the URL for quarter-hourly prices.
The Dutch day-ahead market has traded on quarter-hours since 1 October
2025; an hourly price is simply the mean of its four quarters.
2. All of today, and tomorrow
For charts and for automations that plan ahead you want the whole day. The date has to be templated, because the endpoint defaults to tomorrow once tomorrow has been published.
rest:
- resource_template: >-
https://api.stroomwekker.nl/api/v1/prices?date={{ now().strftime('%Y-%m-%d') }}
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_template: >-
https://api.stroomwekker.nl/api/v1/prices?date={{ (now() + timedelta(days=1)).strftime('%Y-%m-%d') }}
scan_interval: 3600
sensor:
- name: "Electricity prices tomorrow"
unique_id: stroomprijzen_tomorrow
value_template: "{{ value_json.count }}"
json_attributes:
- date
- count
- summary
- points
Those intervals are chosen for the data, not for the clock: today's prices cannot change any more, and tomorrow's arrive once, in the afternoon. Section 6 replaces both timers with two automations and gets it down to two or three requests a day.
Each entry in points looks like this, so a template can reach any
part of it:
{
"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
}
Set Home Assistant's time zone to Europe/Amsterdam.
now() returns your instance's local time, and a day here is a
Dutch calendar day. If Home Assistant is on UTC, now() rolls
over to the next date two hours late in summer and you fetch the wrong day
for the first two hours after midnight.
Tomorrow is empty until the afternoon
Tomorrow's prices appear around 14:00 CE(S)T. Before then the endpoint returns
count: 0 and an empty points array — a normal 200,
not an error. Guard your templates for it rather than treating it as a
failure.
3. Useful derived sensors
These are starting points; adjust the thresholds to your own habits. They assume the sensors from the previous sections.
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 %}
- name: "Cheapest price today"
unique_id: stroomprijs_cheapest_price
unit_of_measurement: "EUR/kWh"
state_class: measurement
state: >-
{% set s = state_attr('sensor.electricity_prices_today', 'summary') %}
{{ s.minEurKwh if s else none }}
- 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 above.
4. Charting the day
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.
5. The Energy dashboard
Home Assistant's Energy dashboard can take a price entity for grid
consumption: 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 actually pay per kWh — excluding the fixed parts of your bill, which no per-kWh price can express. Standing charges, the annual tax rebate and your supplier's markup all sit outside this API; see the reference for exactly what is and is not included.
6. How often to fetch: two or three times a day
Day-ahead prices do not trickle in. They change twice in twenty-four hours and are otherwise completely still:
- Around 14:00 CE(S)T tomorrow's prices are published, all of them at once.
- At midnight what was tomorrow becomes today. No new data — the same numbers, a day later.
A published day never changes again. So a well-written client needs two or three requests a day: today's prices once, tomorrow's once it exists, and a retry or two in the afternoon while waiting for it. Not one every five minutes. Everything else — the price this hour, the cheapest window, whether now is a good moment — can be worked out from those numbers locally, because Home Assistant already has them.
With a key, every call reaches us. A request carrying
Authorization: Bearer bypasses the CDN cache entirely — that is
what makes the usage counts exact, and it is the deal you accept when you use
a key. Without one you mostly hit the cache. Either way the polite number is
the same; with a key it is also the honest one.
Fetch on a schedule, not on a timer
Set the REST sensors to a long scan_interval and drive them from
automations instead. That turns "every 15 minutes, forever" into "when there is
something new".
rest:
- resource_template: >-
https://api.stroomwekker.nl/api/v1/prices?date={{ now().strftime('%Y-%m-%d') }}
scan_interval: 86400
sensor:
- name: "Electricity prices today"
# ... as in section 2
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
That is two calls on a normal day and four on a day 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, which is the one way a handful of polite clients can still behave like a spike.
The current price, without asking
Today's prices are already in Home Assistant, so the price this hour is a lookup, not a request. This replaces the REST sensor from section 1 — same entity name, same value, zero traffic.
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 it needs Home Assistant on Europe/Amsterdam —
as does everything else here. On quarter-hourly data, match
'%Y-%m-%dT%H:%M' against a quarter-hour boundary instead, or keep
the hourly resolution for this sensor and the finer one for charts.
If you would rather just poll
Nothing above is enforced, and a plain scan_interval is a
perfectly reasonable place to start. These numbers are polite:
| What you are reading | Reasonable scan_interval |
|---|---|
| Current price | 900 (15 minutes) — or 0 requests, see above |
| Today's prices | 21600 (6 hours); they cannot change |
| Tomorrow's prices | 3600, or 900 between 13:00 and 16:00 |
Responses are served from a CDN with cache headers matched to how stable they are, so polling faster mostly returns you the same cached bytes. There is no rate limit worth noticing today; clients that fetch on the data's rhythm rather than on a stopwatch are what keeps it that way.
7. Checking the 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 the API rather than agree with it by construction.
8. When something looks wrong
The price is far lower than my energy bill
You are almost certainly 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 — pass ?date= and let the API work it out.
Today has 23 or 25 entries
That is correct on the clock-change days in March and October. Never assume 24,
and never line two series up by array position — match on
momentUtc.
Tomorrow is empty
Normal before roughly 14:00 CE(S)T. Check /api/v1/status to see when the data last landed.