# Danish Electricity Prices: Spot + Full Consumer Price per kWh (`nordic_open_data/dk-electricity-consumer-prices`) Actor

DK1/DK2 spot prices plus the real price per kWh for your grid company (tariffs, elafgift, VAT), cheapest hours and the best charging window.

- **URL**: https://apify.com/nordic_open_data/dk-electricity-consumer-prices.md
- **Developed by:** [Nordic Open Data](https://apify.com/nordic_open_data) (community)
- **Categories:** Integrations, Developer tools
- **Stats:** 1 total users, 0 monthly users, 91.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 full consumer-price days

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Danish Electricity Prices: Spot + Full Consumer Price per kWh (DK1/DK2)

Get today's and tomorrow's Danish electricity prices **as you actually pay them**: the day-ahead spot price for DK1 or DK2 plus your grid company's tariff, Energinet's tariffs, elafgift (electricity tax), 25% VAT and, if you want, your supplier's markup and subscriptions. Give your **grid company** (name or GLN, both on your electricity bill) and the Actor finds the right tariff and price area for you. A postcode works as a best-effort alternative for about half of Danish postcodes.

It also tells you **when to use power**: the cheapest N hours of each day and the cheapest **contiguous block** (for example the best 3 hours to charge an EV, or the best 90 minutes for a dishwasher).

### What it does

- 15-minute day-ahead spot prices for **DK1** (West Denmark) and **DK2** (East Denmark), in DKK/kWh and EUR/MWh

- **Full consumer price per kWh** by grid company: spot + grid tariff (Nettarif C) + Energinet transmission and system tariffs + elafgift + VAT

- **Grid company by name or GLN** (fuzzy: `"radius"`, `"cerius"`, `"N1 A/S - 131"`; an exact full official name such as `"TREFOR El-net A/S"` always wins over fuzzy matches), price area derived automatically; **postcode** lookup as a best-effort fallback (about 50% coverage from open data, weaker in Jutland)

- Optional **supplier markup** and **monthly subscriptions**, shown separately and optionally spread per kWh from your annual consumption

- **Cheapest hours** and **cheapest contiguous window** (in hours or quarter-hours) per day and across the whole range

- **Tomorrow mode**: returns tomorrow's prices once they're published (normally just after 13:00 CET), and otherwise a clear `NOT_YET_PUBLISHED` status

- Date ranges up to 31 days (from 2025-10-01, when Denmark moved to 15-minute prices)

- Clean, stable, documented output with ISO 8601 timestamps that carry the UTC offset

- **Home Assistant summary**: one small record with the current price, next 24 h, cheapest block from now and min/max/average (`mode: "summary"`)

- **EV charging plan**: the cheapest time to charge before your deadline, with expected cost in DKK (`mode: "evCharging"`)

### Use cases

Smart-home and EV charging schedules, heat-pump control, Home Assistant and Node-RED automations, energy dashboards, price alerts, comparison sites and AI agents that need a reliable Danish price feed.

### Home Assistant and automations (`mode: "summary"`)

One small JSON record (about 2 KB) instead of 96–192 rows: the current 15-minute price, the next 24 hours as hourly prices, the cheapest block of whole hours **starting after now**, min/max/average and how the current price compares with the average. Prices are the full variable price per kWh incl. VAT (spot only if you leave out the grid company).

```json
{ "mode": "summary", "gridCompany": "Radius Elnet A/S", "windowHours": 3 }
```

Result (shortened):

```json
{
  "mode": "summary", "status": "OK", "priceArea": "DK2", "unit": "DKK/kWh",
  "current": { "start": "2026-10-15T21:30:00+02:00", "end": "2026-10-15T21:45:00+02:00", "price": 2.2215 },
  "min": { "start": "2026-10-16T13:00:00+02:00", "price": 1.9802 },
  "max": { "start": "2026-10-16T19:00:00+02:00", "price": 3.8004 },
  "average": 2.5418, "currentVsAverage": -0.126,
  "cheapestWindow": { "start": "2026-10-16T00:00:00+02:00", "end": "2026-10-16T03:00:00+02:00", "hours": 3, "averagePrice": 2.0455 },
  "hoursAvailable": 24, "tomorrowPublished": true,
  "next24h": [{ "start": "2026-10-15T21:00:00+02:00", "price": 2.2486 }, "…"]
}
```

The record is both the single dataset item and the `OUTPUT` record, so either API call works:

- `POST https://api.apify.com/v2/acts/nordic_open_data~dk-electricity-consumer-prices/run-sync` returns the record itself (the `OUTPUT` record).
- `POST …/run-sync-get-dataset-items` returns `[record]`.

**Home Assistant REST sensor** (`configuration.yaml`; put `Bearer <your Apify API token>` in `secrets.yaml` as `apify_bearer`):

```yaml
rest:
  - resource: https://api.apify.com/v2/acts/nordic_open_data~dk-electricity-consumer-prices/run-sync
    method: POST
    headers:
      Authorization: !secret apify_bearer
      Content-Type: application/json
    payload: '{"mode": "summary", "gridCompany": "Radius Elnet A/S", "windowHours": 3}'
    timeout: 60
    scan_interval: 3600   # each poll is one paid run, see Pricing
    sensor:
      - name: "Electricity price now"
        unique_id: dk_elpris_now
        value_template: "{{ value_json.current.price }}"
        unit_of_measurement: "DKK/kWh"
        json_attributes: [next24h, cheapestWindow, min, max, average, currentVsAverage, tomorrowPublished]
      - name: "Cheapest 3h block start"
        unique_id: dk_elpris_cheapest_start
        value_template: "{{ value_json.cheapestWindow.start if value_json.cheapestWindow else 'unknown' }}"
        device_class: timestamp
```

Automation: start the dishwasher when the cheapest block begins.

```yaml
automation:
  - alias: "Dishwasher in the cheapest block"
    trigger:
      - platform: time
        at: sensor.cheapest_3h_block_start
    action:
      - service: switch.turn_on
        target: { entity_id: switch.dishwasher }
```

**Generic automation (Node-RED, n8n, Make, cron, scripts):** call `run-sync` with the same JSON and read `current.price`, `cheapestWindow.start` or `next24h`. Example with curl:

```bash
curl -s -X POST -H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
  -d '{"mode":"summary","gridCompany":"Radius Elnet A/S"}' \
  https://api.apify.com/v2/acts/nordic_open_data~dk-electricity-consumer-prices/run-sync | jq .cheapestWindow
```

**Tip:** prices are fixed a day ahead, so you don't need a poll every 15 minutes. Polling once in the afternoon (after 13:30) plus a few times a day is enough: the `next24h` attribute lets Home Assistant templates pick the current hour's price between polls.

### EV charging plan (`mode: "evCharging"`)

> **Estimate, not a quote.** The expected cost uses published spot prices, your grid company's household tariff (C), Energinet tariffs, elafgift, VAT and the markup you enter. Reduced elafgift, supplier-specific or hourly fees, other tariff types and subscriptions are not included, so your bill can differ. Check your supplier's terms. Every plan carries this note in its `disclaimer` field.

Tell it how much energy you need, your charger power and when the car must be ready. It finds the **cheapest continuous charging period** using full consumer prices (spot + tariffs + elafgift + your markup + VAT) at 15-minute resolution, and returns the start, end and expected cost.

```json
{ "mode": "evCharging", "gridCompany": "Radius Elnet A/S",
  "evEnergyKwh": 40, "chargerKw": 11, "chargingEfficiency": 0.9, "readyBy": "07:00" }
```

Result (shortened):

```json
{
  "mode": "evCharging", "status": "OK",
  "start": "2026-10-16T00:30:00+02:00", "end": "2026-10-16T04:33:00+02:00", "durationMinutes": 242,
  "energyToBatteryKwh": 40, "energyFromGridKwh": 44.444, "chargerKw": 11, "efficiency": 0.9,
  "expectedCostDkk": 90.91, "averagePriceDkkPerKwh": 2.0455,
  "chargeImmediately": { "start": "2026-10-15T21:45:00+02:00", "costDkk": 94.59 }, "savingVsImmediateDkk": 3.68,
  "window": { "earliestStart": "2026-10-15T21:37:00+02:00", "readyBy": "2026-10-16T07:00:00+02:00" },
  "schedule": [{ "start": "2026-10-16T00:30:00+02:00", "kwh": 2.75, "price": 2.0431 }, "…"]
}
```

- `readyBy`: `"07:00"` means the next 07:00 after the start, or give a date-time like `"2026-10-16T07:00"` (Danish local time). The window can be up to 48 hours.
- `earliestStart`: empty means now; charging starts at the next quarter-hour.
- `chargingEfficiency` (optional, default 1): the battery gets `kWh`, the grid delivers `kWh ÷ efficiency`, and you pay for the grid energy. The last quarter-hour is charged only partly when the energy doesn't fill it.
- Cost covers energy only (variable price incl. VAT); fixed subscriptions don't depend on when you charge.
- **Infeasible requests fail clearly and cost nothing**, for example: `Not feasible: charging 100 kWh at 2 kW takes 50 h 0 min, but only 9 h 15 min with published prices is available …`. If `readyBy` is later than the last published price (before about 13:00 for tomorrow), the plan uses the published hours and says so in `warnings`.
- Needs the full consumer price, so a grid company (or postcode) is required.
- Daylight-saving days are handled: on 25 October 2026 the 02:00–03:00 hour occurs twice (100 quarter-hours), and plans and windows run on real UTC time, so durations and costs stay correct; times are shown with their `+02:00` / `+01:00` offset.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | `prices` | `summary` | `evCharging` | `prices` | `prices`: one row per 15 minutes. `summary`: one compact record for automations. `evCharging`: one charging plan. In compact modes `period`/`startDate` are ignored |
| `period` | `today` | `tomorrow` | `todayAndTomorrow` | `last7Days` | `last30Days` | `custom` | `todayAndTomorrow` | Which days to fetch (Danish local dates). `last7Days`/`last30Days` are complete past days ending yesterday |
| `startDate`, `endDate` | `YYYY-MM-DD` | – | Only with `custom`. Both dates are included; maximum 31 days |
| `gridCompanySelect` | dropdown | – | **Easiest in the Console.** Pick your grid company from the list of all 32 with a household tariff (labelled with area/towns). Overrides `gridCompany`. In API/JSON input this is a bare GLN, so prefer `gridCompany` there |
| `gridCompany` | string | – (no prefill) | **Recommended for the API.** Full name (`"Radius Elnet A/S"`), short name (`radius`), a former name (`"Ørsted Net"`, `"SEAS-NVE Net"`) or the 13-digit GLN |
| `postcode` | 4 digits | – | Best-effort alternative (about half of postcodes covered). If it can't be resolved, the run asks for `gridCompany` |
| `priceArea` | `DK1` | `DK2` | derived | Only needed for spot-only runs with no location |
| `includeTariffs` | boolean | `true` (on by default) | Return the full consumer price. Needs `gridCompany` or `postcode`; turn off for spot prices only |
| `tariffCode` | string | auto | Advanced: force a specific DatahubPricelist tariff code |
| `supplierMarkupDkkPerKwh` | number | 0 | Supplier markup (tillæg), DKK/kWh, entered **incl. VAT** (shown excl. VAT in the output, see FAQ) |
| `gridSubscriptionDkkPerMonth` | number | 0 | Grid subscription, DKK/month **incl. VAT** |
| `supplierSubscriptionDkkPerMonth` | number | 0 | Supplier subscription, DKK/month **incl. VAT** |
| `annualConsumptionKwh` | integer | – | Required with a subscription: fixed costs are spread per kWh into `fixedCostsDkkPerKwh` and `totalPriceDkkPerKwh`. Without subscriptions `fixedCostsDkkPerKwh` is 0 |
| `cheapestHours` | 0–24 | 3 | Number of cheapest individual hours per day to flag |
| `windowHours` | 0–24 | 3 | Length of the cheapest contiguous block, in hours |
| `windowQuarterHours` | 0–96 | 0 | Length of the cheapest contiguous block, in 15-minute steps |
| `evEnergyKwh`, `chargerKw`, `readyBy` | number, number, string | – | Required for `evCharging` (kWh to add, charger kW, `"07:00"` or `"YYYY-MM-DDTHH:MM"`) |
| `earliestStart`, `chargingEfficiency` | string, number | now, 1 | Optional for `evCharging` |
| `onlyFuture` | boolean | on (Console checkbox shows ON) | Rank cheapest hours/windows only among intervals that start after the run time, so the advice is never in the past. Applied only when the period contains today; fully past or future ranges are ranked in full. Past rows are still returned with prices; they just get no rank. `OUTPUT.onlyFuture` shows whether it was applied |
| `failOnEmpty` | boolean | false | Fail the run when there's no data or the sanity checks fail (for monitoring) |

Invalid input stops the run before any data is fetched or charged. **All problems are reported together** in one message (and as a list in `OUTPUT.errors`), for example `Invalid input (2 problems): (1) startDate must be 2025-10-01 or later … (2) No grid company matches …`. Other examples: `postcode must be a 4-digit Danish postcode`, `Postcode 9999 does not exist in Denmark` (vs. `Postcode 8000 (Aarhus C) exists but isn't covered by the open-data postcode table…`), `Date range is 45 days; maximum is 31`, or `Ambiguous grid company for name "N1": N1 A/S - 016 (grid area 016, DK1) = GLN 5790002502699; N1 A/S - 131 (…) = GLN 5790001089030; N1 A/S - 344 (…) = GLN 5790000611003. Pick one in the grid company list, or set gridCompany to its full name or GLN.` Failed runs also write an `OUTPUT` record with `status: "ERROR"`, the message, `charged: false` and any `candidates`, and are never charged.

#### Example inputs

Tomorrow's full price for a Radius customer, with supplier costs:

```json
{ "period": "tomorrow", "gridCompany": "Radius", "supplierMarkupDkkPerKwh": 0.10,
  "gridSubscriptionDkkPerMonth": 45, "supplierSubscriptionDkkPerMonth": 29, "annualConsumptionKwh": 4000,
  "cheapestHours": 3, "windowHours": 3 }
```

Spot only, DK2, one week:

```json
{ "period": "custom", "startDate": "2026-10-05", "endDate": "2026-10-11", "priceArea": "DK2", "includeTariffs": false }
```

By grid company name, best 90-minute window:

```json
{ "gridCompany": "radius", "windowQuarterHours": 6 }
```

### Output

- **Dataset:** one row per 15-minute interval with the spot price, every price component, `variablePriceDkkPerKwh` (what you pay per kWh incl. VAT, excl. subscriptions), `totalPriceDkkPerKwh` (incl. subscriptions spread per kWh) and cheapest-hour flags. Views: **Prices**, **Full price breakdown**, **Spot prices only** and **Cheapest hours & windows**. Cheapest-hour fields (`cheapestHourRank`, `isCheapestHour`, `inCheapestHourWindow`, `inCheapestQuarterWindow`) appear only when you use `cheapestHours`, `windowHours` or `windowQuarterHours`.
- **Spot-only runs** (`includeTariffs: false`) return only spot fields; tariff, VAT, total and grid-company fields are left out rather than empty, and the status message says `Spot only: add gridCompany for the full price`.
- **`OUTPUT` record:** a summary with status, averages, cheapest hours and windows, billing, and on failure `status: "ERROR"`, the message and candidates.

Full field list and examples: see **Technical** below.

### FAQ

**When are tomorrow's prices available?** The day-ahead auction closes at 12:00 CET and prices normally show up on Energi Data Service shortly after 13:00 CET. Before that, `tomorrow` returns `status: NOT_YET_PUBLISHED` and an empty dataset. The run still succeeds unless `failOnEmpty` is set.

**Why 15-minute prices?** Since 1 October 2025 the Nordic day-ahead market settles in 15-minute periods. Grid tariffs are still hourly. For hourly values, use the `cheapestHours` summary or average the four quarter-hours.

**Does it match my bill exactly?** It comes close for typical households on time-of-use tariffs, but it's an estimate. Suppliers can add fees, use different rounding, or bill on hourly averages.

**Name or GLN in the API?** Use the **name** (`"gridCompany": "Radius Elnet A/S"`). It's readable in your code and logs, survives copy-paste, gives helpful suggestions on typos and works with former names. A GLN is exact but opaque: a wrong digit silently means another company (testers picked Cerius when they meant Radius). Use the GLN only if you already store it, or for N1/Konstant, which have several areas (or use the full name with suffix, e.g. `"N1 A/S - 131"`).

**My grid company has a different name on old bills.** Former names are recognised: DONG Energy Eldistribution / Ørsted Net → **Radius**, SEAS-NVE Net → **Cerius**, Energi Fyn Net → **Vores Elnet**, NRGi Net / Energi Horsens Net → **Konstant** (pick area 151 or 245), Eniig / Evonet / SE Net / HEF Net / EnergiMidt Net → **N1** (pick area 016, 131 or 344). Sources are listed under Technical.

**Why does my 0.40 kr markup show as 0.32?** You enter markup and subscriptions **incl. VAT**, as suppliers advertise them. In the output, all component fields (`supplierMarkupDkkPerKwh`, the tariffs, elafgift) are **excl. VAT**, so 0.40 shows as 0.32. The VAT on all parts is in `vatDkkPerKwh`, and `variablePriceDkkPerKwh`, `fixedCostsDkkPerKwh` and `totalPriceDkkPerKwh` are **incl. VAT**, so the 0.40 is fully in the price you see.

**Grid company vs postcode – which wins?** The grid company list wins over the free-text grid company, which wins over the postcode. If you set both a list choice and free text that point to different companies, the list is used and a warning appears in the status message and `OUTPUT.warnings`. If you set a grid company **and** a postcode, they must agree: a postcode served by another company stops the run with an error (nothing is charged); a postcode the open-data table can't check (e.g. 8000 Aarhus C) gives a warning and the grid company is used.

**Which dataset view should I use?** *Prices (full consumer price)* for normal runs; *Prices (spot only)* for spot-only runs, so you don't get empty tariff/total columns. Every price column says whether it is incl. or excl. VAT.

**Which Konstant / N1 area am I?** The list shows towns where known: Konstant 151 = Aarhus area, Konstant 245 = Favrskov, Horsens, Odder, Skanderborg, Syddjurs (per public tariff sites), TREFOR El-net Øst = Bornholm, TREFOR El-net = Triangle Region, Elektrus = Helsingør town (not Bornholm). N1's three areas aren't published per town; the grid area number is on your bill. If unsure, use the postcode.

**Where's the list of just the cheapest intervals?** Dataset views can't filter rows, so the `cheapest` view shows every interval with its rank/flags. `OUTPUT.selectedIntervals` holds only the selected intervals (ranked hours and window intervals), sorted by time.

**Why doesn't the cheapest window show 12:00 when I run at 21:00?** By default (`onlyFuture`) a run for today ranks only intervals that haven't started yet, so the advice is something you can still act on. Set `onlyFuture: false` to rank the whole day (e.g. for analysis), and `OUTPUT.rankedFrom` shows the cut-off.

**Which grid company am I with?** It's on your electricity bill (name and GLN). Pick it in the grid company list, or type it in `gridCompany`. A postcode works only where the open-data table can resolve it (about half of postcodes, weaker in Jutland).

**Can I schedule it?** Yes. A daily schedule after 13:30 CET with `period: "tomorrow"` and `failOnEmpty: true` gives you a reliable feed and an alert if something breaks.

**What if tariffs are missing for my grid company?** Consumer-price runs are all-or-nothing. Before saving or charging anything, the Actor checks that every row of every day has a grid tariff, Energinet tariffs, elafgift and a total price. If anything is missing, the run fails with a clear error naming the day and fields, returns no data and **charges nothing**. Missing tariffs are never silently counted as 0. Retry later, set `tariffCode`, or use `includeTariffs: false` for spot prices only.

**Is "Include full consumer price" on by default?** Yes. Since build 0.2.13, `includeTariffs` defaults to on, so you need `gridCompany` (or a postcode) unless you switch it off. If you switch it off but still give a grid company, the output warns that only spot prices were returned.

**How is my grid company matched?** A GLN always matches exactly. An exact full official name (case-insensitive, e.g. `"Elnet Midt A/S"`, `"L-Net A/S"`) wins over similar names. Short or fuzzy names (`"trefor"`, `"N1"`) that fit several companies return an error listing the candidates with GLNs.

**Is it affiliated with Energinet?** No. See the attribution below.

**Output schema version 3 (build 0.2.23, 15 Oct 2026).** The output field `supplierMarkupDkkPerKwh` is renamed `supplierMarkupExVatDkkPerKwh` (the value was always excl. VAT; the input `supplierMarkupDkkPerKwh` is unchanged and still entered incl. VAT). Rows that aren't among the cheapest hours now omit `cheapestHourRank` (blank in tables) instead of `null`. Check `schemaVersion` if you parse the output.

### Pricing

Pay per event. You pay only for results; there are no platform usage charges on top.

| Event | Price | When |
|---|---|---|
| Actor start (`actor-start`) | $0.005 | Once per run that returns prices |
| Spot-price day (`spot-price-day`) | $0.01 | Per day returned, spot only (`includeTariffs: false`) |
| Consumer-price day (`consumer-price-day`) | $0.02 | Per day returned with the full consumer price |
| Summary record (`summary-record`) | $0.004 | Per summary record (`mode: summary`), instead of a day event |
| EV charging plan (`ev-charging-plan`) | $0.02 | Per EV plan (`mode: evCharging`), instead of a day event |

Examples: today + tomorrow with the full price = $0.005 + 2 × $0.02 = **$0.045**; one day spot only = **$0.015**.
**Summary and EV modes** charge one `actor-start` plus one record event, whatever the number of days the record covers: **$0.009** per summary ($0.005 + $0.004), **$0.025** per EV plan ($0.005 + $0.02). If your maximum cost per run is below that, nothing is charged and no record is returned. Polling a summary every hour costs about $0.22 a day (≈ $6.5/month); 4 polls a day ≈ $1.1/month (see the Home Assistant tip).

**Not charged:** failed runs (including consumer-price runs stopped because a tariff or total was missing), invalid input, and runs where prices aren't published yet (`NOT_YET_PUBLISHED`) or no data exists.
If your maximum cost per run is lower than the full request, the Actor returns only the days it can charge for and says so in `OUTPUT.warnings`.

### Limitations

- Denmark only (DK1/DK2). Only household (C) tariffs.
- The postcode table is an open-data snapshot that covers about half of all postcodes. Elsewhere, and for postcodes that cross grid boundaries, set `gridCompany`.
- Data before 2025-10-01 (hourly `Elspotprices`) isn't supported.
- Reduced elafgift for electric heating, grid subscriptions (unless entered) and supplier-specific fees aren't modelled.
- Results depend on the availability of Energi Data Service.

Data: Energi Data Service (Energinet), CC BY 4.0. Postcode lookup: derived from © OpenStreetMap contributors (ODbL 1.0) and GeoNames (CC BY 4.0). Not affiliated with Energinet. Details under **Technical**.

### Technical

Background for developers and the curious: exact output format, price formula, how the postcode table is built and data licences.

#### Output in detail

**Dataset:** one row per 15-minute interval. All prices are DKK/kWh unless the field name says otherwise.

```json
{
  "intervalStart": "2026-10-15T13:00:00+02:00",
  "intervalEnd": "2026-10-15T13:15:00+02:00",
  "intervalStartUtc": "2026-10-15T11:00:00Z",
  "date": "2026-10-15", "hour": 13, "resolutionMinutes": 15,
  "priceArea": "DK2", "currency": "DKK",
  "spotPriceDkkPerKwh": 0.76952, "spotPriceEurPerMwh": 102.94,
  "gridTariffDkkPerKwh": 0.31852, "transmissionTariffDkkPerKwh": 0.043, "systemTariffDkkPerKwh": 0.072,
  "electricityTaxDkkPerKwh": 0.008, "supplierMarkupExVatDkkPerKwh": 0.08,
  "variableExVatDkkPerKwh": 1.29104, "vatDkkPerKwh": 0.32276, "variablePriceDkkPerKwh": 1.6138,
  "fixedCostsDkkPerKwh": 0.222, "totalPriceDkkPerKwh": 1.8358,
  "cheapestHourRank": 1, "isCheapestHour": true, "inCheapestHourWindow": true, "inCheapestQuarterWindow": true,
  "gridCompany": "Radius Elnet A/S", "gridCompanyGln": "5790000705689", "gridTariffCode": "DT_C_01",
  "schemaVersion": 3,
  "attribution": "Source: Energi Data Service (Energinet), https://www.energidataservice.dk/, CC BY 4.0. Processed and derived by this Actor; not endorsed by Energinet."
}
```

Price fields:

- `variablePriceDkkPerKwh`: everything you pay per kWh, incl. VAT, excluding subscriptions
- `totalPriceDkkPerKwh`: the variable price plus fixed costs spread per kWh (equal to the variable price when `annualConsumptionKwh` isn't set)
- Component fields (`*TariffDkkPerKwh`, `electricityTaxDkkPerKwh`, `supplierMarkupExVatDkkPerKwh`) exclude VAT. VAT is in `vatDkkPerKwh`.
- For spot-only runs these fields are omitted.

**Key-value store record `OUTPUT`** (summary):

```json
{
  "status": "OK", "message": "192 intervals for DK2, 2 day(s).", "warnings": [],
  "priceArea": "DK2", "startDate": "2026-10-15", "endDate": "2026-10-16",
  "gridCompany": { "name": "Radius Elnet A/S", "gln": "5790000705689", "resolvedVia": "name \"radius\"", "tariffCode": "DT_C_01", "tariffName": "Nettarif C" },
  "fixedCosts": { "totalDkkPerMonth": 74, "totalDkkPerYear": 888, "annualConsumptionKwh": 4000, "amortizedDkkPerKwh": 0.222 },
  "averages": { "spotPriceDkkPerKwh": 1.45853, "variablePriceDkkPerKwh": 2.54142, "totalPriceDkkPerKwh": 2.76342 },
  "cheapestHours": [{ "date": "2026-10-15", "hour": 13, "start": "2026-10-15T13:00:00+02:00", "rank": 1, "averagePriceDkkPerKwh": 1.47 }],
  "cheapestHourWindow": { "perDay": [{ "date": "2026-10-15", "start": "2026-10-15T12:00:00+02:00", "end": "2026-10-15T15:00:00+02:00", "length": 3, "unit": "hour", "averagePriceDkkPerKwh": 1.64345 }],
                          "wholeRange": { "start": "2026-10-15T12:00:00+02:00", "end": "2026-10-15T15:00:00+02:00", "length": 3, "unit": "hour", "averagePriceDkkPerKwh": 1.64345 } },
  "cheapestQuarterWindow": { "perDay": [], "wholeRange": null },
  "attribution": "…"
}
```

`status` is one of `OK`, `PARTIAL` (some days aren't published yet), `NOT_YET_PUBLISHED`, `NO_DATA`, `CHARGE_LIMIT_REACHED`, `HEALTH_CHECK_FAILED` or `ERROR`. `mode` is `consumer-price` or `spot-only`. On failure:

```json
{ "status": "ERROR", "message": "Ambiguous grid company for name \"N1\": …", "charged": false, "candidates": [{ "name": "N1 A/S - 131", "gln": "5790001089030", "label": "N1 A/S - 131 (grid area 131, DK1)" }] }
```

The dataset has four views: **Prices** (overview), **Full price breakdown**, **Spot prices only** and **Cheapest hours & windows**. Each cheapest-hour field is omitted when its option is 0.

#### How the price is calculated (assumptions)

- **Spot:** `DayAheadPrices` from Energi Data Service, DKK/MWh ÷ 1000.
- **Grid tariff:** the grid company's household time-of-use tariff, `Nettarif C` / `Nettarif C time` (customers on the 0.4 kV grid), from `DatahubPricelist`. It uses the price for each Danish local hour (Price1–Price24) and handles validity periods, including mid-range tariff changes. Rebate, production, "lokal kollektiv", B and A tariffs are ignored. Where a company publishes several C codes with identical prices (flex/time variants) the choice makes no difference. Where their prices differ, the time-of-use variant is used and a warning is added. `tariffCode` overrides the choice.
- **Energinet:** transmission net tariff (40000) and system tariff (41000).
- **Elafgift:** the `EA-001` rate in DatahubPricelist (0.008 DKK/kWh in 2026). Reduced rates for electric heating aren't modelled.
- **VAT:** 25% on all per-kWh parts. Your markup and subscriptions are entered **incl. VAT**, the way Danish suppliers show them.
- **Not included:** grid and Energinet subscriptions unless you enter them, nettabstarif for TSO-connected customers, B/A customer tariffs and any supplier-specific fees you don't enter.
- **Cheapest hours/windows** are ranked on `variablePriceDkkPerKwh` (or spot if tariffs are off). Fixed costs are the same every hour, so they don't change the ranking. Hours are clock hours averaged from the four quarter-hours. The whole-range window may span midnight; per-day windows don't.

#### Former grid company names (aliases)

`data/grid-aliases.json` maps historic and group names to today's company. Only verified mappings are included:

- DONG Energy Eldistribution → Radius (Ørsted press release, Dec 2015; renamed April 2016). DONG Energy became Ørsted in 2017; Radius was sold to SEAS-NVE/Andel in 2020 and kept its name (andel.dk).
- SEAS-NVE Net → Cerius (EnergiWatch, turn of the year 2017/18).
- Energi Fyn Net → Vores Elnet (CVR 25587987 name history, 29 May 2018).
- NRGi Net → Konstant Net (Konkurrence- og Forbrugerstyrelsen decision 16 Jan 2018; CVR 21262498, which also lists Energi Horsens Net).
- Eniig's grid → N1 (1 July 2018); Evonet (SE) merged into N1 after the SE/Eniig (Norlys) merger; the earlier N1 came from EnergiMidt and HEF (Green Power Denmark).
  Group names that own several grid companies (e.g. Andel, Norlys, EWII) are deliberately not aliased.

#### Postcode validity

Postcodes are first checked against the full GeoNames list of Danish postcodes (`data/postcodes-dk.json`, 1,159 codes, CC BY 4.0). Unknown numbers get "does not exist"; real postcodes the open-data grid table can't resolve get "not covered".

#### Postcode to grid company

The Actor ships a postcode table (`data/postcode-grid.json`) built by `tools/build_postcode_map.mjs` from **openly licensed sources only**:

| Source | Used for | Licence |
|---|---|---|
| [GeoNames](https://www.geonames.org/) postal codes for Denmark (`DK.zip`) | postcodes, place names, centroids | CC BY 4.0 |
| [OpenStreetMap](https://www.openstreetmap.org/copyright) `power=substation/transformer` objects with an `operator` tag (via Overpass) | which grid company operates near the postcode | ODbL 1.0, © OpenStreetMap contributors |
| [Energi Data Service](https://www.energidataservice.dk/) `DatahubPricelist`, `ConsumptionGridAreaHour` | list of grid companies, GLNs, tariffs, price area per company | CC BY 4.0 |

**How a postcode is resolved:** the build script takes OSM power objects within 3 km of the postcode's GeoNames centroid whose operator matches a current grid company. Operators that are transmission companies, producers, or brands that can't be matched unambiguously (e.g. "Nexel", "EnergiMidt") are ignored.

- **Resolved:** at least 3 matching points, all naming the same company.
- **Several candidates:** the points name more than one company. The run stops and lists the candidates so you can set `gridCompany`.
- **Not in the table:** fewer than 3 matching points. The run stops with a clear error asking for `gridCompany` (name or GLN).
- **Inferred entries:** none. Values are never copied from neighbouring postcodes.

Coverage of the 2026-10-15 snapshot (1,159 GeoNames postcodes): **586 resolved, 9 with several candidates, 564 not in the table, 0 inferred**. Coverage is best on Zealand, Funen and in the larger cities and weakest in parts of Jutland, where OSM has few operator tags. A company with several grid areas (N1, Konstant) also needs the GLN or the "- NNN" suffix, which the error message lists. OSM data can be wrong or outdated, so if the result looks off, set `gridCompany` explicitly; it always takes priority over the table. Because the table is derived from OSM, it is a derivative database under ODbL; `tools/` contains the script and the OSM snapshot used.

#### Data source and licence

Prices and tariffs: **Energi Data Service** (Energinet), https://www.energidataservice.dk/, licensed under **CC BY 4.0** (https://creativecommons.org/licenses/by/4.0/). This Actor transforms the data: it converts units and combines spot, tariffs and taxes. It is **not affiliated with or endorsed by Energinet**. Every output row carries an `attribution` field. Postcode table sources: GeoNames (CC BY 4.0) and OpenStreetMap (ODbL 1.0, © OpenStreetMap contributors).

- **Energi Data Service terms:** https://energidataservice.dk/terms-and-conditions (CC BY 4.0; copy, change and distribute, including commercially, with credit and without implying endorsement). A snapshot of the terms is kept in the source under `licences/`.
- **OpenStreetMap:** © OpenStreetMap contributors, https://www.openstreetmap.org/copyright. `data/postcode-grid.json` is a derivative database of OSM data and is available under the **Open Database License (ODbL) 1.0** (share-alike); the OSM snapshot and build script are included so it can be rebuilt.
- **GeoNames:** postal codes from https://www.geonames.org/ under CC BY 4.0.
- When a run resolves the grid company **by postcode**, each row's `attribution` and the `OUTPUT.dataSources` list also credit OpenStreetMap and GeoNames.

# Actor input Schema

## `mode` (type: `string`):

'prices' returns one row per 15-minute interval. 'summary' returns ONE compact record (current price, next 24 h hourly, cheapest window from now, min/max/average), ideal for Home Assistant REST sensors and the run-sync API. 'evCharging' returns ONE charging plan (start, end, expected cost) using full consumer prices. In summary and EV modes, period/startDate are ignored.

## `period` (type: `string`):

Which days to fetch (Danish local dates). 'tomorrow' returns status NOT_YET_PUBLISHED before the prices are out. 'Last 7/30 days' are complete past days ending yesterday (no ranking cut-off).

## `startDate` (type: `string`):

YYYY-MM-DD, inclusive. Only with period = custom. Earliest 2025-10-01.

## `endDate` (type: `string`):

YYYY-MM-DD, inclusive. Default: same as start. Max 31 days.

## `gridCompanySelect` (type: `string`):

The 32 grid companies (netselskaber) with a household tariff in DatahubPricelist, labelled with area and towns. A choice here overrides the free-text field below. Leave empty to use the text field.

## `gridCompany` (type: `string`):

Your grid company (netselskab) as on your electricity bill: full name (e.g. 'Radius Elnet A/S', 'N1 A/S - 131'), a short name ('radius', 'cerius') or the 13-digit GLN. Exact full names win over fuzzy matches; ambiguous names (e.g. 'N1', 'Konstant') return an error listing the candidates. The price area (DK1/DK2) is derived from it. Former names are recognised too, e.g. 'DONG Energy Eldistribution'/'Ørsted Net' → Radius, 'SEAS-NVE Net' → Cerius, 'Energi Fyn Net' → Vores Elnet, 'NRGi Net' → Konstant, 'Eniig'/'Evonet' → N1. Via the API, prefer the name (e.g. "Radius Elnet A/S") over a bare GLN: it is readable and self-documenting. Precedence: list > this field > postcode. If you also give a postcode, it must match (otherwise the run stops with an error).

## `postcode` (type: `string`):

Danish postcode, e.g. 2100 (København Ø) or 8000 (Aarhus C). Used to find your grid company when neither the grid company list nor the free-text field is set. If you set a grid company too, the postcode must match it, otherwise the run stops with an error (or warns if the postcode can't be checked). Not every postcode is covered; then set your grid company (it's on your bill).

## `priceArea` (type: `string`):

Derived automatically from postcode/grid company. Required only for spot-only runs without a location.

## `includeTariffs` (type: `boolean`):

Add grid tariff, Energinet tariffs, elafgift and 25% VAT (needs gridCompany or postcode). On by default; turn off (or leave gridCompany and postcode empty via API) for spot prices only. If tariffs can't be found the run fails without charging.

## `tariffCode` (type: `string`):

Override the auto-selected household tariff (DatahubPricelist ChargeTypeCode). Rarely needed.

## `supplierMarkupDkkPerKwh` (type: `number`):

Your electricity supplier's surcharge on top of spot (tillæg), entered incl. VAT as on supplier price lists. In the output, supplierMarkupDkkPerKwh is shown excl. VAT (e.g. 0.40 incl. VAT → 0.32), like all other component fields; the 25% VAT on it is part of vatDkkPerKwh, and variablePriceDkkPerKwh/totalPriceDkkPerKwh are incl. VAT.

## `gridSubscriptionDkkPerMonth` (type: `number`):

Your GRID company's monthly subscription (netabonnement; on the bill as 'Netabonnement' from e.g. Radius/N1), entered incl. VAT. Not your supplier's fee – that goes in 'Supplier subscription'. Needs 'Annual consumption'; it is then spread per kWh into fixedCostsDkkPerKwh and totalPriceDkkPerKwh (both incl. VAT).

## `supplierSubscriptionDkkPerMonth` (type: `number`):

Your electricity SUPPLIER's monthly fee (abonnement from the company you buy power from, e.g. Andel Energi, OK, Norlys), entered incl. VAT. Not the grid subscription – that goes in 'Grid subscription'. Needs 'Annual consumption'; spread per kWh into fixedCostsDkkPerKwh and totalPriceDkkPerKwh (incl. VAT).

## `annualConsumptionKwh` (type: `integer`):

Required when you enter a subscription: fixed monthly costs × 12 ÷ this value is added per kWh (fixedCostsDkkPerKwh, incl. VAT). Without subscriptions fixedCostsDkkPerKwh is 0.

## `cheapestHours` (type: `integer`):

Flag the N cheapest individual clock hours per day.

## `windowHours` (type: `integer`):

Find the cheapest run of N consecutive hours, per day and over the whole range.

## `windowQuarterHours` (type: `integer`):

Same, in 15-minute steps (e.g. 6 = 1.5 hours).

## `onlyFuture` (type: `boolean`):

On by default: when the period includes today, rank only hours/intervals that haven't started yet. Ignored for periods entirely in the past or future.

## `failOnEmpty` (type: `boolean`):

Make the run FAIL when no data is returned, a requested day is missing, or sanity checks fail. Useful for schedules and alerts.

## `evEnergyKwh` (type: `number`):

kWh to add to the battery, e.g. 30 (from 40% to 80% of a 75 kWh battery). Required for mode evCharging.

## `chargerKw` (type: `number`):

Charger power, e.g. 11 (3-phase 16 A), 7.4 (1-phase 32 A) or 3.7. Required for mode evCharging.

## `readyBy` (type: `string`):

"07:00" = the next 07:00 after the start, or a date-time "2026-10-16T07:00". Required for mode evCharging. The window can be at most 48 hours.

## `earliestStart` (type: `string`):

"2026-10-15T22:00" (Danish local time). Empty = now (the next quarter-hour).

## `chargingEfficiency` (type: `number`):

Share of grid energy that ends up in the battery, e.g. 0.9 (typical AC charging 0.85–0.95). Default 1 (no losses). Cost is based on grid energy = kWh ÷ efficiency.

## Actor input object example

```json
{
  "mode": "prices",
  "period": "todayAndTomorrow",
  "postcode": "2100",
  "includeTariffs": true,
  "cheapestHours": 3,
  "windowHours": 3,
  "windowQuarterHours": 0,
  "onlyFuture": true,
  "failOnEmpty": false
}
```

# Actor output Schema

## `prices` (type: `string`):

Dataset view 'overview': interval start, area, spot, grid tariff, price incl. VAT, total, grid company.

## `breakdown` (type: `string`):

Dataset view 'breakdown': every price component per interval (DKK/kWh).

## `allFields` (type: `string`):

All dataset fields as defined in the dataset schema.

## `summary` (type: `string`):

Key-value record OUTPUT: status (OK, PARTIAL, NOT_YET_PUBLISHED, NO_DATA, CHARGE_LIMIT_REACHED, HEALTH_CHECK_FAILED, ERROR). On failure OUTPUT holds status ERROR, message, charged:false and candidates (for ambiguous grid companies), grid company, averages, fixed costs, cheapest hours and windows, billing, attribution.

## `spot` (type: `string`):

Dataset view 'spot' for spot-only runs: interval start, area, spot price, cheapest flags.

## `cheapest` (type: `string`):

Dataset view 'cheapest': rank and window flags per interval (only filled when cheapestHours/windowHours/windowQuarterHours are used). Views can't filter rows, so the selected intervals alone, sorted by time, are in OUTPUT.selectedIntervals.

## `compact` (type: `string`):

In mode 'summary' or 'evCharging': the single compact record (also the OUTPUT key-value record).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postcode": "2100",
    "onlyFuture": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("nordic_open_data/dk-electricity-consumer-prices").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postcode": "2100",
    "onlyFuture": True,
}

# Run the Actor and wait for it to finish
run = client.actor("nordic_open_data/dk-electricity-consumer-prices").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postcode": "2100",
  "onlyFuture": true
}' |
apify call nordic_open_data/dk-electricity-consumer-prices --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nordic_open_data/dk-electricity-consumer-prices"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/VmORWPI5QzFkNvhSm/builds/gUswSztNaeh5s2QhB/openapi.json
