Node-RED · price and CO2
Use electricity price and CO2 in Node-RED without unnecessary clicking
For the first step, one importable summary flow is enough. If you want more later, there is also a dedicated price-series path for your own window logic, charts and special cases.
- dedicated Node-RED endpoint with a compact structure for flows
- summary for a fast start, price series for custom logic
- optional Bearer token support and up to 120h in the current test phase
Core logic explained early
The price logic is not “forecast only”. It deliberately combines real market data with forecast values so a flow does not react unnecessarily to pure estimates.
mixed path therefore combines real market data with forecast values only for the still-open part of the horizon.Node-RED can process JSON APIs directly. So you do not need a special node, only a small flow with Inject, Function, HTTP Request and Debug.
In the example, the HTTP Request node is configured to return the response directly as a parsed object. There is also a catch path so errors do not disappear silently.
Recommended entry point: summary
Node-RED has its own summary path with current values, best windows and meta fields for limits and API key state. That is the most sensible starting point for first automations.
https://api.energypriceforecast.eu/api/v1/node-red/summary?country=de&hours=48&window_hours=4msg.*.msg.api_key_state, msg.allowed_horizon_hours and more are included directly.Alternative for your own price logic: node-red/prices
For more advanced flows there is also a second Node-RED path that returns only the price series. It is intended for your own window calculations, charts, thresholds, or combinations with PV, battery and house status.
https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15msummaryBeginner path with current price, CO2 and already computed windows.pricesRaw price series for your own flows, own window logic and dashboards.15m mode, forecast is not smoothed. Each forecast hour is simply repeated across four quarter-hour slots.What the API actually returns
The summary endpoint returns a compact automation view. That is useful for Node-RED because you can use it immediately for simple flows while still keeping enough structure for later extensions.
| Part | Content | Why it matters |
|---|---|---|
flat | Current price, current CO2, boolean fields for active best windows and remaining minutes. | Ready to use in msg.*. |
price | Current price slot plus best_window and next_full_window as objects. | More context for your own flow logic. |
co2 | Current CO2 slot plus best_window and next_full_window. | For CO2-oriented decisions. |
source | Metadata about day-ahead and forecast data. | Important for interpretation and debugging. |
meta | Access and contract information such as API key state, allowed horizon and daily counters. | Clean monitoring per user or flow. |
If you need a real price series for your own calculations instead of the compact summary, use /api/v1/node-red/prices. The summary intentionally stays compact. The price series is intended for advanced custom flow logic.
Price series for your own flows: node-red/prices
The extra price-series path intentionally returns prices only, without CO2 fields and without precomputed window logic. That is exactly the point: in Node-RED you can decide for yourself how to evaluate or combine the slots with other signals.
| Parameter | Meaning | Typical use |
|---|---|---|
mode=mixed | Day-ahead first, forecast only for future hours not yet covered by official day-ahead data. | Practical default. |
mode=forecast_only | Forecast only, without day-ahead mixing. | Comparison or intentionally forecast-centered logic. |
price_mode=base | Default mode with base or market price values. | When the relative shape is enough for your automation. |
price_mode=retail | Assumption-based total retail price with markups, grid fees and VAT for supported markets. | When you want to automate closer to an end-customer price. |
plz=10115 | Required for Germany in retail mode so regional grid-fee assumptions fit better. | Mandatory for price_mode=retail in Germany. |
resolution=15m | Continuous quarter-hour series. Forecast hours are repeated across four quarter-hour slots. | Charts and uniform slot logic. |
resolution=native | Day-ahead stays quarter-hourly, forecast stays hourly. | When you want to keep the original source resolution. |
https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m
https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m&price_mode=retail&plz=10115
Important: in 15m mode, forecast values are not smoothed between hours. The API does not invent intermediate prices. Instead, it clearly marks that those quarter-hour slots come from one hourly forecast block.
Base price or total retail price?
The Node-RED summary path still deliberately returns base or market prices. The prices path also returns base prices by default, but can optionally output assumption-based retail totals.
summaryStays compact and deliberately remains on base prices.prices with price_mode=retailCan return an optional 15-minute or native series as an assumption-based total retail price.Time horizon and resolution
The examples use hours=48 and, for the summary, window_hours=4. That is a sensible starting point, but not the full product limit.
hours to define the requested horizon. Publicly, we currently communicate up to 120 hours for the price forecast.meta.allowed_horizon_hours.source.day_ahead_entries and source.forecast_entries count slots, not hours. That is why values such as 97 day-ahead and 0 forecast can still be perfectly plausible.
Where to put the API key
In the example flow, the API key is set in the Function node. If the string stays empty, the request runs without a key and therefore with the free horizon. With a key you can, for example, test hours=120.
const API_KEY = "";
const API_URL = "https://api.energypriceforecast.eu/api/v1/node-red/summary?country=de&hours=120&window_hours=4";
In the end, the authoritative value is not the number you ask for in the URL, but what the server actually allows in meta.allowed_horizon_hours.
Quick start: import the summary flow
- Click
Importin the top right corner of Node-RED. - Paste the complete JSON block below.
- Import it and then click
Deploy. - Open the debug sidebar and trigger the Inject node once manually.
country=de in the URL and optionally the API key in the Function node.msg.current_price, msg.current_co2_g_kwh, active best windows and the most important meta fields.Import flow for the summary
Copy the full block and import it into Node-RED. The flow also catches HTTP and parsing errors through a catch path.
[
{
"id": "epf_summary_tab",
"type": "tab",
"label": "EPF Summary",
"disabled": false,
"info": ""
},
{
"id": "epf_summary_inject",
"type": "inject",
"z": "epf_summary_tab",
"name": "Load summary",
"props": [],
"repeat": "900",
"once": true,
"onceDelay": "1",
"topic": "",
"x": 130,
"y": 120,
"wires": [["epf_summary_prepare"]]
},
{
"id": "epf_summary_prepare",
"type": "function",
"z": "epf_summary_tab",
"name": "Set API key and URL",
"func": "const API_KEY = \"\";\\nmsg.url = \"https://api.energypriceforecast.eu/api/v1/node-red/summary?country=de&hours=48&window_hours=4\";\\nmsg.headers = {};\\nif (API_KEY.trim()) {\\n msg.headers.Authorization = \"Bearer \" + API_KEY.trim();\\n}\\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 390,
"y": 120,
"wires": [["epf_summary_http"]]
},
{
"id": "epf_summary_http",
"type": "http request",
"z": "epf_summary_tab",
"name": "Fetch summary",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"persist": false,
"x": 630,
"y": 120,
"wires": [["epf_summary_extract"]]
},
{
"id": "epf_summary_extract",
"type": "function",
"z": "epf_summary_tab",
"name": "Expose key fields",
"func": "const data = msg.payload || {};\\nconst flat = data.flat || {};\\nconst meta = data.meta || {};\\nconst price = data.price || {};\\nconst co2 = data.co2 || {};\\nmsg.current_price = flat.current_price;\\nmsg.current_co2_g_kwh = flat.current_co2_g_kwh;\\nmsg.cheapest_window_start = flat.cheapest_window_start || null;\\nmsg.greenest_window_start = flat.greenest_window_start || null;\\nmsg.is_cheapest_window_now = flat.is_cheapest_window_now === true;\\nmsg.cheapest_window_remaining_minutes = flat.cheapest_window_remaining_minutes ?? null;\\nmsg.next_full_cheapest_window_start = price.next_full_window ? price.next_full_window.start : null;\\nmsg.next_full_greenest_window_start = co2.next_full_window ? co2.next_full_window.start : null;\\nmsg.api_key_state = meta.api_key_state || 'missing';\\nmsg.allowed_horizon_hours = meta.allowed_horizon_hours;\\nmsg.used_horizon_hours = meta.used_horizon_hours;\\nmsg.rate_limit_daily = meta.rate_limit_daily;\\nmsg.used_calls_today = meta.used_calls_today;\\nmsg.error = null;\\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 870,
"y": 120,
"wires": [["epf_summary_debug"]]
},
{
"id": "epf_summary_debug",
"type": "debug",
"z": "epf_summary_tab",
"name": "Show summary",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1110,
"y": 120,
"wires": []
},
{
"id": "epf_summary_catch",
"type": "catch",
"z": "epf_summary_tab",
"name": "Catch errors",
"scope": null,
"uncaught": false,
"x": 140,
"y": 220,
"wires": [["epf_summary_error"]]
},
{
"id": "epf_summary_error",
"type": "function",
"z": "epf_summary_tab",
"name": "Make error readable",
"func": "const message = msg.error && msg.error.message ? msg.error.message : 'Unknown error';\\nmsg.payload = { error: message, source: msg.error && msg.error.source ? msg.error.source : null };\\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 400,
"y": 220,
"wires": [["epf_summary_error_debug"]]
},
{
"id": "epf_summary_error_debug",
"type": "debug",
"z": "epf_summary_tab",
"name": "Summary error",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"x": 660,
"y": 220,
"wires": []
}
]
After that, you can replace the Debug node with your own dashboard, notification or scheduler logic.
Advanced: import the price-series flow
If you prefer to calculate things yourself, the second flow is intended for the raw price series. It stores the full slot list in msg.price_entries.
[
{
"id": "epf_prices_tab",
"type": "tab",
"label": "EPF Prices",
"disabled": false,
"info": ""
},
{
"id": "epf_prices_inject",
"type": "inject",
"z": "epf_prices_tab",
"name": "Load price series",
"props": [],
"repeat": "900",
"once": false,
"onceDelay": "1",
"topic": "",
"x": 130,
"y": 120,
"wires": [["epf_prices_prepare"]]
},
{
"id": "epf_prices_prepare",
"type": "function",
"z": "epf_prices_tab",
"name": "Set parameters",
"func": "const API_KEY = \"\";\\nconst BASE_URL = \"https://api.energypriceforecast.eu/api/v1/node-red/prices?country=de&hours=48&mode=mixed&resolution=15m\";\\nconst USE_RETAIL = false;\\nconst PLZ = \"10115\";\\nmsg.url = USE_RETAIL ? BASE_URL + \"&price_mode=retail&plz=\" + encodeURIComponent(PLZ) : BASE_URL;\\nmsg.headers = {};\\nif (API_KEY.trim()) {\\n msg.headers.Authorization = \"Bearer \" + API_KEY.trim();\\n}\\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 360,
"y": 120,
"wires": [["epf_prices_http"]]
},
{
"id": "epf_prices_http",
"type": "http request",
"z": "epf_prices_tab",
"name": "Fetch price series",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"persist": false,
"x": 610,
"y": 120,
"wires": [["epf_prices_extract"]]
},
{
"id": "epf_prices_extract",
"type": "function",
"z": "epf_prices_tab",
"name": "Forward series",
"func": "const data = msg.payload || {};\\nmsg.price_entries = Array.isArray(data.entries) ? data.entries : [];\\nmsg.first_price_slot = msg.price_entries.length ? msg.price_entries[0] : null;\\nmsg.price_series_source = data.source || {};\\nmsg.api_key_state = data.meta && data.meta.api_key_state ? data.meta.api_key_state : 'missing';\\nmsg.allowed_horizon_hours = data.meta ? data.meta.allowed_horizon_hours : null;\\nmsg.series_request = data.request || {};\\nmsg.series_assumptions = data.assumptions || null;\\nmsg.error = null;\\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 860,
"y": 120,
"wires": [["epf_prices_debug"]]
},
{
"id": "epf_prices_debug",
"type": "debug",
"z": "epf_prices_tab",
"name": "Show price series",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"x": 1110,
"y": 120,
"wires": []
}
]
For retail mode in Germany, set USE_RETAIL = true and enter a suitable PLZ postcode in the Function node.
For automations: separate display and execution logic
For dashboards, msg.cheapest_window_start may already be enough. For real control decisions, however, it is often more important to know whether the best window is already active. That is why the summary flow also sets msg.is_cheapest_window_now and msg.cheapest_window_remaining_minutes.
if (msg.is_cheapest_window_now) {
// enable the consumer now
} else if (msg.next_full_cheapest_window_start) {
// use the next full window for planning
}
return msg;
Pragmatically, a boolean for “now” is usually more valuable for automations than only a timestamp in the future.
Which message fields you will have afterwards
| Field | Meaning | Typical use |
|---|---|---|
msg.current_price | Current price in EUR/kWh. | Simple price logic or debug output. |
msg.current_co2_g_kwh | Current CO2 intensity. | CO2-oriented control decisions. |
msg.is_cheapest_window_now | true when the best price window is already active. | Direct run decision for consumers. |
msg.cheapest_window_remaining_minutes | Remaining runtime of the currently best price window. | Run a device only as long as the window is still active. |
msg.next_full_cheapest_window_start | Start time of the next full price window. | Planning instead of immediate triggering. |
msg.price_entries | Full price series from the price-series path. | Your own window logic, charts and special cases. |
msg.series_assumptions | Retail assumptions such as postcode, grid-fee source and VAT. | Debugging and plausibility checks in total-price mode. |
msg.api_key_state | Status of the used API key. | Clean monitoring per user or flow. |
msg.allowed_horizon_hours | Server-side allowed maximum horizon. | Check whether a key unlocks more. |
msg.used_calls_today | Requests already consumed today. | Keep an eye on limits. |
Important notes
Is this an official Node-RED node?
No. It is intentionally a normal flow built from standard nodes. That makes it easier to understand at the beginning and easier to adapt to your own automations.
How often should I poll?
We recommend 30 minutes by default here. For many setups that is enough because day-ahead data does not change all the time. At the same time, the current slot and window status still stay fresh enough. If you intentionally want to react closer to quarter-hour slots, you can switch polling to 15 minutes yourself.
Is there already a warning for extreme hours?
Yes. The product already has a warning for possible extreme hours, for example in the app or web app via the Tail-Beta or extreme-path signal. In the Node-RED endpoint itself, however, that signal is not yet exposed as its own stable field.
What if I get HTML instead of JSON in the debug pane?
Then the URL most likely points to the wrong path or to a broken intermediate address. For Node-RED you should use the API URLs from this page directly.
Small test phase
There is currently a small test phase for the new Node-RED integration. We are mainly looking for real flows where it becomes visible quickly whether the import examples are understandable and whether the data can be processed cleanly in real automations.
If you actively use Node-RED and want to test this, send a short note with your setup and market to StrompreisVorhersage@proton.me. The first test integrations are still handled manually on purpose.
Related: ioBroker and Home Assistant.