# EU Fuel Prices Scraper (Official Weekly Oil Bulletin) (`steadfetch/eu-fuel-price-extractor`) Actor

Official European Commission weekly fuel prices for all EU-27 countries: petrol (Euro-super 95), diesel, heating gas oil, fuel oil and LPG, plus the EU-27 average, with or without taxes. For fleet and energy cost analysis and fuel price monitoring. Public XLSX, no login or browser.

- **URL**: https://apify.com/steadfetch/eu-fuel-price-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Other, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 result delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## EU Fuel Prices Scraper (Official Weekly Oil Bulletin)

Extract the official European Commission **Weekly Oil Bulletin** for all EU-27 countries: Euro-super 95 petrol, automotive diesel, heating gas oil, low- and high-sulphur fuel oil and LPG motor fuel, plus the EU-27 weighted average. Pick prices with taxes, prices without taxes or the published duties and taxes. No browser, no login, no CAPTCHA.

### What it does

Every week DG Energy publishes the Weekly Oil Bulletin as three public XLSX files linked from [the bulletin page](https://energy.ec.europa.eu/data-and-analysis/weekly-oil-bulletin_en):

- **Prices with taxes** - consumer prices in EUR per 1000 litres (or per tonne for fuel oils).
- **Prices without taxes** - the same series with duties and taxes excluded.
- **Duties and taxes** - three sheets: VAT (percent), Excise duties and Other Indirect Taxes (national currency per 1000 litres or per tonne).

The Actor reads the landing page, downloads only the workbook it needs and emits one validated record per **country x fuel x series** (and x tax type for duties).

- **Official source only:** the files linked from `energy.ec.europa.eu`. No third-party copies or HTML tables.
- **Whole EU-27 in one query:** 27 member states plus the EU-27 weighted average (country code `EU`). The "Euro Area 20" row is out of scope and never emitted.
- **Three series:** `with_taxes`, `without_taxes` and `duties_taxes`.
- **Values as published, never converted:** every record carries `unit` (`1000 l`, `t` or `%`) and `currency` (EUR for the weekly price series, the national currency for excise and other indirect taxes, `null` for VAT).
- **Derived per-litre value:** `pricePerLiter` is `price / 1000` when the published unit is `1000 l`, rounded to 5 decimals and expressed in `currency`. It is `null` for per-tonne and percent values.
- **Week comes from inside the file:** `weekStart` is read from the date cell declared in the workbook, not from the file name. The Commission sometimes leaves an older date in the link file name; the in-file date is the authoritative one.
- **Monitor deduplication:** with `monitorMode` enabled, delivered keys are stored in a named key-value store and a repeated run for the same bulletin week emits zero records, so it is not charged per result.
- **Schema validation:** every record is validated against `.actor/dataset_schema.json` before it reaches the dataset. An invalid record fails the run instead of pushing malformed data.
- **Scope guards:** robots.txt is checked on every run. The download must stay on `energy.ec.europa.eu` under the document route, must return an XLSX content type and must fit the size cap; otherwise the run aborts.

#### Data and personal information

The source is official aggregated statistics published by the European Commission: country-level fuel prices and tax rates. **No personal data** is read, derived or added. Each run makes at most four requests: robots.txt, the bulletin page and two XLSX files.

### Example output

Prices with taxes (Spain, Euro-super 95, week of 2026-10-19):

```json
{
    "url": "https://energy.ec.europa.eu/data-and-analysis/weekly-oil-bulletin_en",
    "sourceUrl": "https://energy.ec.europa.eu/data-and-analysis/weekly-oil-bulletin_en",
    "country": "ES",
    "countryName": "Spain",
    "fuel": "e95",
    "series": "with_taxes",
    "weekStart": "2026-10-19",
    "price": 1780.47,
    "pricePerLiter": 1.78047,
    "unit": "1000 l",
    "currency": "EUR",
    "sourceFile": "Weekly Oil Bulletin Weekly prices with Taxes - 2026-09-21.xlsx",
    "scrapedAt": "2026-10-25T10:00:00.000Z"
}
```

EU-27 weighted average (same week):

```json
{
    "country": "EU",
    "countryName": "European Union (EU-27 weighted average)",
    "fuel": "e95",
    "series": "with_taxes",
    "weekStart": "2026-10-19",
    "price": 1991.33,
    "pricePerLiter": 1.99133,
    "unit": "1000 l",
    "currency": "EUR"
}
```

Duties and taxes (Spain, Euro-super 95 excise):

```json
{
    "country": "ES",
    "countryName": "Spain",
    "fuel": "e95",
    "series": "duties_taxes",
    "weekStart": "2026-10-19",
    "price": 272.69,
    "pricePerLiter": 0.27269,
    "unit": "1000 l",
    "currency": "EUR",
    "taxType": "excise",
    "effectiveSince": "2026-10-12"
}
```

A full `with_taxes` run of the current workbook returns **115 records** (28 country rows x the fuels each one publishes). A full `duties_taxes` run returns **390 records** (27 countries x fuels x 3 tax sheets). Missing source values are skipped, never estimated.

### Input

```json
{
    "countries": ["ES", "DE"],
    "fuels": ["e95", "diesel"],
    "series": "with_taxes",
    "week": "latest",
    "monitorMode": false,
    "maxResults": 100,
    "strictSchema": true
}
```

| Field | Type | Description |
| --- | --- | --- |
| `countries` | array | ISO 3166-1 alpha-2 codes to keep. The EU-27 weighted average is `EU`. Empty returns all 28 country rows. |
| `fuels` | array | `e95`, `diesel`, `heating_gas_oil`, `fuel_oil`, `fuel_oil_high_sulphur`, `lpg`. Empty returns every fuel in the workbook. |
| `series` | string | `with_taxes` (default), `without_taxes` or `duties_taxes`. |
| `week` | string | `latest` (default) or a date (`YYYY-MM-DD`) inside the week you want. Only the bulletin currently published can be downloaded; any other week returns 0 records without a per-result charge. |
| `monitorMode` | boolean | When true, rows already delivered for the same bulletin week are skipped, so a monitor run without a new bulletin delivers 0 rows. |
| `maxResults` | integer | Maximum validated records to push. Default 100. |
| `strictSchema` | boolean | Fail the run if a record does not match the dataset schema. Keep enabled. |

Fuel slugs map to the Commission columns: `e95` = Euro-super 95, `diesel` = automotive gas oil, `heating_gas_oil` = gas oil de chauffage, `fuel_oil` = fuel oil with the lower sulphur column, `fuel_oil_high_sulphur` = the "> 1%" sulphur column, `lpg` = LPG motor fuel.

### Project structure

```
.actor/actor.json                  Actor definition (payPerEvent, dataset schema, output schema)
.actor/input_schema.json           INPUT options (countries, fuels, series, week, monitor)
.actor/dataset_schema.json         Item JSON Schema (fields) + table view for the Store
.actor/pay_per_event.json          PPE pricing (two synthetic platform events)
src/bulletin-client.ts             Landing page parsing, file classification, guarded download
src/robots.ts                      robots.txt parser and per-run allow check
src/workbook.ts                    exceljs XLSX reader (isolated from pure extraction)
src/countries.ts                   EU-27 ISO-2 and currency reference data
src/extract.ts                     Pure extraction: fuels, week, filters, units, dedupe keys
src/dedupe.ts                      Pure cross-run dedupe state for the key-value store
src/validate-output.ts             Ajv validator shared by the run and the tests
src/main.ts                        Entry point: robots, landing, download, parse, validate, push
test/                              Offline Vitest suites and the real XLSX fixtures
```

### Pay-per-event pricing

The Actor uses Apify **pay-per-event** pricing with the two synthetic platform events:

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | a run starts (synthetic, automatic) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute) |
| `apify-default-dataset-item` | a new, validated record is saved to the dataset (synthetic, automatic) | 0.001 USD per record = **1.00 USD per 1,000 results** |

There is no custom per-item event and no `Actor.charge()` call: the platform already charges `apify-default-dataset-item` per pushed item, so adding another event would bill users twice. Failed runs, rows outside the selected filters, missing source values and monitor duplicates are not charged per result. A monitor check that finds no new bulletin costs **0.00005 USD** (the `apify-actor-start` event) and nothing per result.

The 1.00 USD per 1,000 price matches the leader of the `fuelprices` cluster (54 users in 30 days at 1.00 USD per 1,000) and stays inside Steadfetch's 1-10 USD band. There is no direct competitor for the official EU-wide series.

**Works on the Apify free plan** - about 4,999 results for the $5 monthly credit ((5.00 USD - 0.00005 USD run-start at the default 256 MB memory) / 0.001 USD per result).

#### Cost per 1,000 results

Measured locally on 2026-10-25 with the real Commission workbooks, without any platform run:

- Files: 16,946 B (with taxes), 17,330 B (without taxes), 23,078 B (duties).
- Parse all three workbooks: **95 ms** (55.8 ms with taxes, 15.7 ms without, 23.4 ms duties).
- Pure extraction of 115 + 115 + 390 records: **3.5 ms**.
- Peak resident memory: **114 MB**, inside the 256 MB container limit.

A typical run downloads two small files and spends well under a second of compute. Even with a conservative bound of 10 seconds at 256 MB, the run-start event covers the first 5 seconds and the remaining compute is a fraction of a cent. Total cost is **below 0.01 USD per 1,000 records**, so the 1.00 USD per 1,000 price keeps a margin above 99%.

### Development

```bash
npm install
npm test                 # lint + typecheck + offline Vitest suites (no credit, no APIFY_TOKEN)
npm run publish:check    # Steadfetch publish gate
npm run schemas:validate # apify-cli schema validation
```

The default test suite runs entirely offline against the committed 16-23 KB XLSX fixtures and never touches the Apify platform. It covers the spec example row (Spain 1780.47), the EU-27 average (1991.33), without-taxes and duties rows, VAT as a percentage, zero-valued taxes, national currencies, week normalisation, filters, maxResults, landing-page parsing, download guards, dedupe state, two-run monitor behaviour and output-schema validation.

### FAQ

**Does the Actor need a login or an API key?**
No. The bulletin page and the XLSX downloads are public and anonymous.

**Does it need a browser?**
No. The source is a server-rendered page plus XLSX files, so plain HTTP plus an XLSX parser is far cheaper and faster than a browser.

**Why is `weekStart` 2026-10-19 when the file name says 2026-09-21?**
The Commission keeps the old date in the link file name while the workbook content declares the current week in cell A2. The Actor reads the date inside the workbook, because that is the week the values belong to. `sourceFile` keeps the original file name so you can audit the link.

**Why are duties in different currencies?**
The duties workbook states "in national currency per unit". The Actor reports the published value and its `currency` (for example CZK for Czechia) instead of converting it. Converting would require foreign-exchange rates the source does not publish. The weekly price series are always in EUR.

**Why is `pricePerLiter` null for some rows?**
`pricePerLiter` only makes sense for volumes. Fuel oil is published per tonne and VAT is a percentage, so `pricePerLiter` is `null` there instead of an invented value.

**Where is the Euro Area 20 average?**
Out of scope. The Commission also publishes a Euro Area 20 weighted average; this Actor covers EU-27 plus the EU-27 average, as declared in its scope.

**How does monitor mode work?**
The Actor stores the `(series, taxType, country, fuel, weekStart)` key of every delivered record in the named key-value store `eu-fuel-price-state`, which persists between runs. A second run for the same week finds those keys and pushes nothing, so it is not charged per result. A new bulletin week starts with an empty set.

**When am I charged?**
Once per run start (`apify-actor-start`) and once per new record saved to the dataset (`apify-default-dataset-item`). Failed runs, filtered-out rows, missing values and monitor duplicates are not charged per record.

**Is the output schema stable?**
Yes. New fields are additive and the required fields in `.actor/dataset_schema.json` do not change.

**Does it return personal data?**
No. It only returns country-level prices and tax rates.

### Maintenance

This Actor is **tested daily**: automated runs check that the Commission bulletin page and workbooks are reachable and that the output still matches the published schema. Failures are reported as issues in the Steadfetch Actors repository. On every run the header, bulletin date, fuel columns and data rows are validated: if the layout cannot be found, the run fails loudly instead of returning an empty dataset.

# Actor input Schema

## `countries` (type: `array`):

ISO 3166-1 alpha-2 codes to keep, for example ES, DE, FR. The EU-27 weighted average is country "EU". Empty returns all 27 countries plus the EU-27 average.

## `fuels` (type: `array`):

Fuel slugs to keep: e95, diesel, heating_gas_oil, fuel_oil, fuel_oil_high_sulphur, lpg. Empty returns every fuel in the workbook.

## `series` (type: `string`):

with_taxes returns pump prices including duties and taxes; without_taxes excludes them; duties_taxes returns the tax amounts published by the Commission (VAT in percent, excise and other indirect taxes in national currency per 1000 l or tonne).

## `week` (type: `string`):

Use "latest" for the current bulletin, or a date (YYYY-MM-DD) inside the week you want. Only the bulletin currently published by the Commission can be downloaded; a different week returns 0 rows.

## `monitorMode` (type: `boolean`):

When enabled, rows already delivered for the same bulletin week are not pushed again, so a monitor run without a new bulletin delivers 0 rows and is not charged per result.

## `maxResults` (type: `integer`):

Maximum number of validated rows to push to the dataset.

## `strictSchema` (type: `boolean`):

Fail the run when an item does not match the OUTPUT schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "countries": [
    "ES",
    "DE"
  ],
  "fuels": [
    "e95",
    "diesel"
  ],
  "series": "with_taxes",
  "week": "latest",
  "monitorMode": false,
  "maxResults": 50,
  "strictSchema": true
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "countries": [
        "ES",
        "DE"
    ],
    "fuels": [
        "e95",
        "diesel"
    ],
    "series": "with_taxes",
    "week": "latest",
    "monitorMode": false,
    "maxResults": 50,
    "strictSchema": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/eu-fuel-price-extractor").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 = {
    "countries": [
        "ES",
        "DE",
    ],
    "fuels": [
        "e95",
        "diesel",
    ],
    "series": "with_taxes",
    "week": "latest",
    "monitorMode": False,
    "maxResults": 50,
    "strictSchema": True,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/eu-fuel-price-extractor").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 '{
  "countries": [
    "ES",
    "DE"
  ],
  "fuels": [
    "e95",
    "diesel"
  ],
  "series": "with_taxes",
  "week": "latest",
  "monitorMode": false,
  "maxResults": 50,
  "strictSchema": true
}' |
apify call steadfetch/eu-fuel-price-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/eu-fuel-price-extractor"
        }
    }
}
```

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/BQTQrzw1PycW6mr0c/builds/w194tpIIyq7Ua84E0/openapi.json
