EU Fuel Prices Scraper (Official Weekly Oil Bulletin) avatar

EU Fuel Prices Scraper (Official Weekly Oil Bulletin)

Pricing

from $0.80 / 1,000 result delivereds

Go to Apify Store
EU Fuel Prices Scraper (Official Weekly Oil Bulletin)

EU Fuel Prices Scraper (Official Weekly Oil Bulletin)

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.

Pricing

from $0.80 / 1,000 result delivereds

Rating

0.0

(0)

Developer

Steadfetch

Steadfetch

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 hours ago

Last modified

Categories

Share

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:

  • 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):

{
"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):

{
"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):

{
"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

{
"countries": ["ES", "DE"],
"fuels": ["e95", "diesel"],
"series": "with_taxes",
"week": "latest",
"monitorMode": false,
"maxResults": 100,
"strictSchema": true
}
FieldTypeDescription
countriesarrayISO 3166-1 alpha-2 codes to keep. The EU-27 weighted average is EU. Empty returns all 28 country rows.
fuelsarraye95, diesel, heating_gas_oil, fuel_oil, fuel_oil_high_sulphur, lpg. Empty returns every fuel in the workbook.
seriesstringwith_taxes (default), without_taxes or duties_taxes.
weekstringlatest (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.
monitorModebooleanWhen true, rows already delivered for the same bulletin week are skipped, so a monitor run without a new bulletin delivers 0 rows.
maxResultsintegerMaximum validated records to push. Default 100.
strictSchemabooleanFail 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:

EventCharged whenPrice
apify-actor-starta run starts (synthetic, automatic)0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute)
apify-default-dataset-itema 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

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.