# Europe Fuel Prices Scraper: Spain and Portugal Stations (`rowcraft/europe-fuel-prices-scraper`) Actor

Current fuel prices at every station in Spain and Portugal, straight from the official government sources: diesel, petrol 95 and 98, LPG and more, each with its update time. Filter by province, city or radius. No personal data, no proxies.

- **URL**: https://apify.com/rowcraft/europe-fuel-prices-scraper.md
- **Developed by:** [Rowcraft](https://apify.com/rowcraft) (community)
- **Categories:** Travel, Business, Automation
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 fuel stations

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

## Europe Fuel Prices Scraper: Spain and Portugal Stations

Get the current fuel prices of petrol stations in Spain and Portugal from the two official government services. This **Europe fuel prices scraper** reads the Spanish Ministry of Industry and Tourism and the Portuguese DGEG directly, so it does not break when a price website changes its layout and it needs no proxy, no login and no API key. Each station comes back as one row: address, postal code, city, province or district, coordinates, the brand where it is safe to show one, the price of every fuel the station lists (diesel, petrol 95 and 98, LPG, natural gas, hydrogen and more) and the time of the update, as JSON, CSV or Excel.

Filter by province, district, postal code, city, fuel or distance from a point, or take a whole country. Schedule it with **Only new rows** to get only the stations whose prices changed since the last run, and pay only for those rows.

![Results of Europe Fuel Prices Scraper: Spain and Portugal Stations in the Apify dataset view](https://api.apify.com/v2/key-value-stores/7hbQFQdGq61v2OZ2h/records/europe-fuel-prices-scraper-results.png)

### What you get

Price: $0.80 per 1,000 fuel stations on the Free plan, $0.50 on the Business plan, paid per row delivered; the $5 monthly credit of a free Apify account covers about 6,200 fuel stations. Details in Pricing below.

One row per station, the same columns for every station of both countries:

- **Station:** an ID that starts with the country (`ES-6207`, `PT-66676`), street address, postal code, city, municipality, province (Spain) or district (Portugal), the autonomous community for Spain, latitude and longitude in decimal degrees
- **Brand:** the label the station sells under (Repsol, Galp, BP, Moeve and so on), shown only when it is a recurring label (see **Privacy**), otherwise `null`
- **Prices:** `diesel`, `dieselPremium`, `petrol95`, `petrol98` and `lpg` as plain columns for quick tables, and `prices`, the full list: for every fuel the station sells, a normalised fuel code (`diesel`, `petrol_95`, `petrol_95_e10`, `cng`, `hydrogen`, `adblue` and others), the label exactly as the source writes it, the price, the unit (litre, kg or m3) and the update time
- **Availability and type:** the list of fuels that have a price, whether the Spanish station sells to the public, the type of Portuguese station (motorway, hypermarket, other), and the opening hours of Spanish stations as readable text
- **Source and time:** `priceUpdatedAt` with its time zone, the name of the publisher in `source`, the address of the official service in `sourceUrl`, and the moment the actor read it in `fetchedAt`
- **Search helpers:** `distanceKm` when you search around a point, and `changedFuels` when you use Only new rows

Prices are current prices only. Neither service publishes history, so to build one you run the actor on a schedule.

### Why this actor

- **Two official services, one table.** Spain and Portugal publish their station prices themselves. The actor reads both and returns the same columns, with fuels mapped to the same codes, so a Madrid station and a Lisbon station sit in one dataset.
- **Every price has a time and a source.** Each row names the publisher and carries the update time, so you can show where the data comes from, as the source conditions ask, and drop prices that nobody has touched for days.
- **Filters that read like a map.** Provinces by code or name (28, Madrid, A Coruña), Portuguese districts (Lisboa, Faro, 7), postal codes, cities written any way you like, fuels, and a centre point with a radius that delivers the nearest stations first.
- **Only the prices that changed.** Turn on Only new rows and a station comes back only when one of its prices is new since your last run. An hourly or daily schedule then costs you the changes, not the whole country.
- **Polite by design.** The actor reads the robots.txt of each host first, makes at most one request per second, retries with a pause when a service is busy, uses no proxy and never tries to get around a block.
- **No personal data, no surprises on the bill.** The services describe stations, not people, and the actor drops what could identify one. You pay per delivered station, with a charge cap you control.

### How to use it

1. Open the Actor and fill in **Countries** (the other fields are optional filters).
2. Set **Maximum fuel stations** to a small number for a first try (the default is a safe start).
3. Click **Start**. The status line shows progress; the run stops cleanly at your limit or at the charge limit you set in the run options.
4. Open the **Dataset** tab and download JSON, CSV, Excel, XML or HTML, or send the rows to Google Sheets, Slack, Make, n8n or Zapier with the Integrations tab.
5. To run it on a schedule, use **Schedules** in Apify Console and turn on **Only new rows since the last run** below: every run then costs only the new rows, and a webhook URL tells you when something new came in.

### Input

**Quick start.** This input is ready to run (it is also the example in the API tab):

```json
{
  "countries": [
    "ES"
  ],
  "provinces": [
    "02"
  ],
  "fuels": [
    "diesel"
  ],
  "maxItems": 100
}
```

One real row it returns (long texts shortened here):

```json
{
  "stationId": "ES-6207",
  "country": "ES",
  "brand": "REPSOL",
  "address": "AUTOVIA A-2 KM. 154",
  "postalCode": "42248",
  "city": "LODARES",
  "municipality": "Medinaceli",
  "province": "SORIA",
  "provinceCode": "42",
  "region": "Castilla y León",
  "latitude": 41.189222,
  "longitude": -2.403972,
  "stationType": null,
  "publicSale": true,
  "openingHours": "Mon 07:00-00:00; Tue-Fri 24h; Sat 00:00-21:00; Sun 08:00-22:00",
  "diesel": 1.925,
  "dieselPremium": 2.025,
  "petrol95": 1.755,
  "petrol98": 1.915,
  "lpg": 1.129,
  "fuelsAvailable": [
    "diesel",
    "diesel_premium",
    "petrol_95",
    "… 2 more"
  ],
  "prices": [
    {
      "fuel": "diesel",
      "rawFuelLabel": "Gasoleo A",
      "price": 1.925,
      "unit": "litre",
      "priceUpdatedAt": "2026-10-26T06:49:42"
    },
    {
      "fuel": "diesel_premium",
      "rawFuelLabel": "Gasoleo Premium",
      "price": 2.025,
      "unit": "litre",
      "priceUpdatedAt": "2026-10-26T06:49:42"
    },
    {
      "fuel": "petrol_95",
      "rawFuelLabel": "Gasolina 95 E5",
      "price": 1.755,
      "unit": "litre",
      "priceUpdatedAt": "2026-10-26T06:49:42"
    },
    "… 2 more"
  ],
  "currency": "EUR",
  "priceUpdatedAt": "2026-10-26T06:49:42",
  "timeZone": "Europe/Madrid",
  "source": "Ministerio de Industria y Turismo (Spain)",
  "sourceUrl": "https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/",
  "fetchedAt": "2026-10-26T05:49:40.503Z",
  "distanceKm": null,
  "changedFuels": null
}
```

| Field | What it does |
|---|---|
| `countries` | `ES` for Spain, `PT` for Portugal, or both. Default: Spain |
| `provinces` | Spanish provinces, one per line: the code 01 to 52 (`28`, `02`) or the name (`Madrid`, `Málaga`, `A Coruña`). Empty = all 52 provinces |
| `districts` | Portuguese districts, one per line: the number 1 to 18 or the name (`Lisboa`, `Porto`, `Faro`, `Évora`). Empty = all 18 districts of the mainland |
| `fuels`, `fuelsMatch` | Keep only stations that have a price for these fuels. With several fuels, `fuelsMatch` chooses between all of them (default) and at least one |
| `maxItems` | Stops after this many stations. Default 500; `0` = no limit |
| `postalCodes`, `cities` | Spain: five digits (a dropped leading zero is repaired: 2001 = 02001). Portugal: `1000-001`, or the first four digits for the whole area. City names in any capitalisation, with or without accents |
| `center`, `radiusKm` | Latitude and longitude as Google Maps shows them (`40.4168, -3.7038`) and a radius in kilometres (default 10). The nearest stations come first and each row carries `distanceKm` |
| `onlyNew`, `stateName`, `notifyUrl`, `notifyWhenEmpty` | Only the stations whose prices changed since the last run, and a webhook message at the end; see **Automation** below |

Different filters narrow each other (province and city and fuel); several values inside one filter widen it (Madrid or Barcelona). A province applies to Spain and a district to Portugal. If you select both countries and give a province but no district, Portugal is read in full, and the actor tells you so in the log and in the run summary. A value that matches nothing is reported there too, instead of silently returning an empty result.

Example: the diesel price of every station within 15 km of the centre of Madrid, nearest first.

```json
{
  "countries": ["ES"],
  "center": "40.4168, -3.7038",
  "radiusKm": 15,
  "fuels": ["diesel"],
  "maxItems": 0
}
```

Example: every station of the Lisbon and Porto districts that sells LPG, in one file.

```json
{
  "countries": ["PT"],
  "districts": ["Lisboa", "Porto"],
  "fuels": ["lpg"],
  "maxItems": 0
}
```

More recipes, all of them plain input you can paste into the form's JSON view:

```json
{
  "countries": ["ES"],
  "provinces": ["Barcelona", "Girona"],
  "fuels": ["petrol_95", "petrol_95_e10"],
  "fuelsMatch": "any",
  "maxItems": 0
}
```

The stations of two Catalan provinces that sell petrol 95 (E5) or petrol 95 E10. Use `fuelsMatch: "all"` (the default) to keep only stations that sell both.

```json
{
  "countries": ["ES", "PT"],
  "provinces": ["Badajoz", "Cáceres"],
  "districts": ["Portalegre", "Castelo Branco"],
  "maxItems": 0
}
```

Both sides of the Spanish and Portuguese border in one table, for a cross-border price comparison.

```json
{
  "countries": ["ES", "PT"],
  "fuels": ["diesel"],
  "maxItems": 0,
  "onlyNew": true,
  "stateName": "iberia-diesel",
  "notifyUrl": "https://hooks.example.com/your-webhook"
}
```

A watch over both countries for a schedule: the first run delivers every station with a diesel price, each later run delivers only the stations whose diesel price changed, and a message goes to your webhook when new rows came in.

### Output

30 fields per row. Empty values are `null`, never missing, so CSV and Excel columns are stable.

| Field | Type | Example |
|---|---|---|
| `stationId` | string | ES-12054 |
| `country` | string | ES |
| `brand` | string | REPSOL |
| `address` | string | CALLE PRINCIPE DE ASTURIAS (POLÍGONO DE ROMICA), 5 |
| `postalCode` | string | 02001 |
| `city` | string | ALBACETE |
| `municipality` | string | Albacete |
| `province` | string | ALBACETE |
| `provinceCode` | string | 02 |
| `region` | string | Castilla la Mancha |
| `latitude` | number | 39.054694 |
| `longitude` | number | -1.832 |
| `stationType` | string | motorway |
| `publicSale` | boolean | True |
| `openingHours` | string | Mon-Sun 06:00-22:00 |
| `diesel` | number | 1.949 |
| `dieselPremium` | number | 2.039 |
| `petrol95` | number | 1.765 |
| `petrol98` | number | 1.905 |
| `lpg` | number | 0.989 |
| `fuelsAvailable` | array | \["diesel", "petrol_95", "petrol_98"] |
| `prices` | array | \[{"fuel": "diesel", "rawFuelLabel": "Gasoleo A", "price": 1.949, "u… |
| `currency` | string | EUR |
| `priceUpdatedAt` | string | 2026-10-26T06:38:57 |
| `timeZone` | string | Europe/Madrid |
| `source` | string | Ministerio de Industria y Turismo (Spain) |
| `sourceUrl` | string | `https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/Pr…` |
| `fetchedAt` | string | 2026-10-26T05:38:55.771Z |
| `distanceKm` | number | 3.24 |
| `changedFuels` | array | \["diesel"] |

#### Working with the data

- **Prices are euros, as numbers, with up to three decimals.** The unit of each price is in `prices[].unit`: litre for liquid fuels, kg for natural gas, hydrogen and liquefied biogas, m3 for the Portuguese compressed natural gas price per cubic metre. Do not rank kg prices against litre prices.
- **The five plain columns follow one rule.** `diesel` is the standard diesel (Spanish Gasóleo A, Portuguese Gasóleo simples), `dieselPremium` the premium grade, `petrol95` the standard 95 petrol (Gasolina 95 E5 in Spain, Gasolina simples 95 in Portugal), `petrol98` the 98 petrol and `lpg` the autogas. E10, E25, premium petrol, renewable diesel, natural gas, hydrogen and AdBlue are in `prices`.
- **Rank by price, filter by time.** Sort the dataset on a price column and keep rows whose `priceUpdatedAt` is recent. A Portuguese price that nobody has touched for a week is common and is the first one to check when you plan a trip.
- **The `stationId` is stable.** The part after the country prefix is the station's own ID in the official service. Use it to join runs over time, to keep a price history of your own, or to match stations with another list.
- **Group on `province` or `region`** to compute averages in a spreadsheet; `provinceCode` keeps its leading zero as text, so it survives CSV and Excel.
- **Empty is `null`.** A fuel the station does not sell, a missing opening-hours text or a missing distance is `null`, never an empty string or a zero, so a filter such as "diesel is not empty" works the same in every tool.

#### Run summary and coverage

Every run writes a `RUN_SUMMARY` record to the key-value store: how many stations were read, how many matched your filters, how many were delivered, why the run stopped (all read, maximum reached, charge limit, or a refusal or outage of a source), what each country did, the errors of the sources, the filters as the actor understood them, warnings, and the memory peak. A second record, `COVERAGE`, lists every province, district, postal code and city you asked for with the number of stations that matched it on its own, so a misspelt city shows up as `not_found` instead of a silent empty dataset.

#### Example rows

Two real rows from a run (long texts shortened here, complete in the dataset):

```json
[
  {
    "stationId": "ES-6207",
    "country": "ES",
    "brand": "REPSOL",
    "address": "AUTOVIA A-2 KM. 154",
    "postalCode": "42248",
    "city": "LODARES",
    "municipality": "Medinaceli",
    "province": "SORIA",
    "provinceCode": "42",
    "region": "Castilla y León",
    "latitude": 41.189222,
    "longitude": -2.403972,
    "stationType": null,
    "publicSale": true,
    "openingHours": "Mon 07:00-00:00; Tue-Fri 24h; Sat 00:00-21:00; Sun 08:00-22:00",
    "diesel": 1.925,
    "dieselPremium": 2.025,
    "petrol95": 1.755,
    "petrol98": 1.915,
    "lpg": 1.129,
    "fuelsAvailable": [
      "diesel",
      "diesel_premium",
      "petrol_95",
      "… 2 more"
    ],
    "prices": [
      {
        "fuel": "diesel",
        "rawFuelLabel": "Gasoleo A",
        "price": 1.925,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T06:49:42"
      },
      {
        "fuel": "diesel_premium",
        "rawFuelLabel": "Gasoleo Premium",
        "price": 2.025,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T06:49:42"
      },
      {
        "fuel": "petrol_95",
        "rawFuelLabel": "Gasolina 95 E5",
        "price": 1.755,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T06:49:42"
      },
      "… 2 more"
    ],
    "currency": "EUR",
    "priceUpdatedAt": "2026-10-26T06:49:42",
    "timeZone": "Europe/Madrid",
    "source": "Ministerio de Industria y Turismo (Spain)",
    "sourceUrl": "https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/",
    "fetchedAt": "2026-10-26T05:49:40.503Z",
    "distanceKm": null,
    "changedFuels": null
  },
  {
    "stationId": "PT-66676",
    "country": "PT",
    "brand": "REPSOL",
    "address": "Rua Mestre João Serra Lt. D",
    "postalCode": "7300-069",
    "city": "Zona Industrial",
    "municipality": "Portalegre",
    "province": "Portalegre",
    "provinceCode": "12",
    "region": null,
    "latitude": 39.26658,
    "longitude": -7.43713,
    "stationType": "other",
    "publicSale": null,
    "openingHours": null,
    "diesel": 2.304,
    "dieselPremium": 2.374,
    "petrol95": 2.109,
    "petrol98": null,
    "lpg": 1.029,
    "fuelsAvailable": [
      "lpg",
      "petrol_95",
      "diesel",
      "… 1 more"
    ],
    "prices": [
      {
        "fuel": "lpg",
        "rawFuelLabel": "GPL Auto",
        "price": 1.029,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T00:00:00"
      },
      {
        "fuel": "petrol_95",
        "rawFuelLabel": "Gasolina simples 95",
        "price": 2.109,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T00:00:00"
      },
      {
        "fuel": "diesel",
        "rawFuelLabel": "Gasóleo simples",
        "price": 2.304,
        "unit": "litre",
        "priceUpdatedAt": "2026-10-26T00:00:00"
      },
      "… 1 more"
    ],
    "currency": "EUR",
    "priceUpdatedAt": "2026-10-26T00:00:00",
    "timeZone": "Europe/Lisbon",
    "source": "DGEG (Portugal)",
    "sourceUrl": "https://precoscombustiveis.dgeg.gov.pt/",
    "fetchedAt": "2026-10-26T05:49:43.647Z",
    "distanceKm": null,
    "changedFuels": null
  }
]
```

Results can be downloaded as JSON, CSV, Excel, XML or HTML from the Dataset tab, or read through the Apify API.

![Input form of Europe Fuel Prices Scraper: Spain and Portugal Stations](https://api.apify.com/v2/key-value-stores/7hbQFQdGq61v2OZ2h/records/europe-fuel-prices-scraper-input.png)

### Pricing

**Pay per result.** You pay for each fuel station delivered to your dataset, nothing else: no subscription, no minimum, no charge for rows your filters drop or for the requests behind them.

| Your Apify plan | Price per 1,000 rows | Price per row |
|---|---|---|
| Free plan | **$0.80** | $0.0008 |
| Starter plan | **$0.70** | $0.0007 |
| Scale plan | **$0.60** | $0.0006 |
| Business plan and above | **$0.50** | $0.0005 |

Example: 1,000 fuel stations cost $0.80 on the Free plan and $0.50 on the Business plan. Platform usage of the run (compute, storage) is included in that price and is not billed to you separately (pay per event: the actor covers it).
A start event of $0.00005 is charged once per run (Apify default).
**Cap your spend:** set *Maximum total charge* in the run options. The actor stops cleanly when the cap is reached and you keep everything delivered until then. The run summary (`RUN_SUMMARY` in the key-value store) says why a run stopped.

A run over the whole of Spain (about 11,400 stations, `maxItems` set to 0) costs about $9.12 at the Free plan price and $5.70 at the Business plan price. A Spanish province holds between about 10 and 900 stations, so one province costs between 1 cent and 72 cents at the Free plan price. A Portuguese district holds between about 50 and 400 stations.

### Automation: only new rows, notifications, schedules

**Only new rows since the last run.** Turn on `onlyNew` and the actor remembers every fuel station it delivered to you (in a key-value store named `rowcraft-europe-fuel-prices-scraper-state` in your Apify account). The next run with the same `stateName` skips what you already have and you pay only for new rows. The first run delivers everything that matches. Use a different `stateName` for each schedule or search so they keep separate memories.

**Notification when the run ends.** Put a webhook address in `notifyUrl` and the actor sends one JSON POST with the number of new rows, links to the dataset (JSON and CSV) and the run. It works out of the box with Slack and Discord incoming webhooks (the message is in `text` and `content`), with Make, n8n and Zapier catch hooks, and with your own endpoint. By default it is sent only when there is at least one new row; `notifyWhenEmpty` sends it every time.

**Schedules.** Create a schedule in Apify Console (hourly, daily, weekly or cron) with `onlyNew` on. A daily schedule gives you a feed of new fuel stations for the price of the new rows only.

**Integrations.** Send the results to Make, n8n, Zapier, Google Sheets, Slack, Google Drive, Airtable or any webhooks from the Integrations tab of the Actor, with no code. Apify also offers native actions for Make, n8n and Zapier.

**API, SDKs and AI agents.** Every run is available through the Apify API and the official JavaScript and Python clients; the Actor can also be called by AI agents through the Apify MCP server. The example input in the API tab is a working call.

**Use with AI agents (MCP).** Add the Apify MCP server to Claude, Cursor, VS Code or any MCP client and the agent can run this Actor as a tool:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=rowcraft/europe-fuel-prices-scraper",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

**Call it from a shell or any language.** One request runs the Actor with the input below and returns the rows as JSON:

```bash
curl -X POST "https://api.apify.com/v2/acts/rowcraft~europe-fuel-prices-scraper/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"countries": ["ES"], "provinces": ["02"], "fuels": ["diesel"], "maxItems": 100}'
```

**Python** (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("rowcraft/europe-fuel-prices-scraper").call(run_input={'countries': ['ES'], 'provinces': ['02'], 'fuels': ['diesel'], 'maxItems': 100})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

**JavaScript / Node.js** (`npm install apify-client`):

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('rowcraft/europe-fuel-prices-scraper').call({"countries": ["ES"], "provinces": ["02"], "fuels": ["diesel"], "maxItems": 100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

**What counts as new here.** The two services work differently, and so does the comparison.

- **Portugal** gives every price its own timestamp. The actor remembers each combination of station, fuel and timestamp it has delivered to you, and a station comes back when at least one of its prices carries a timestamp you do not have yet.
- **Spain** publishes one timestamp for the whole file, refreshed about every half hour, so a timestamp cannot tell which prices moved. The actor remembers each combination of station, fuel and price value instead, and a station comes back when it has a price value that was never delivered to you for that fuel. A price that returns to a value you already received is not delivered again.

In both cases the `changedFuels` column says which fuels are new, and the row still carries all current prices of the station. Stations without anything new are skipped and are not charged.

### Use cases

- **Price monitoring:** a scheduled run per province or district, a webhook into Slack or a spreadsheet, and an alert when the cheapest diesel of your area moves.
- **Fleet and logistics:** the cheapest stations around a depot or along a border crossing, with the fuel you need, ranked by distance, in two countries at once.
- **Price comparison and apps:** a fresh, normalised table of stations with coordinates for a map or a comparison page, refreshed on a schedule.
- **Journalism and research:** regional price differences, the gap between Spanish and Portuguese diesel, and the spread between standard and premium grades, from official sources you can cite.
- **Spreadsheets and dashboards:** send the dataset to Google Sheets, Airtable or a BI tool and compute your own averages per province, district or brand.

Ready-made examples, each with the input filled in (open one and press Start, or save it as your own task):

- [Diesel prices at stations within 10 km of central Madrid](https://apify.com/rowcraft/europe-fuel-prices-scraper/examples/diesel-stations-around-madrid)

### What this actor does not do

- Deliver a brand for every station. A label that only one or two stations use can be the name of a person, so it is replaced by `null`.
- Deliver station names, phone numbers, e-mail addresses or the names of operators.
- Deliver price history. It reads the current prices; use a schedule to collect your own history.
- Look up addresses or place names for the radius search. Give coordinates in `center`; the actor does not call a geocoder or any other website.
- Cover other countries, electric charging prices or heating oil. France has its own Rowcraft actor, listed below.
- Cover the Portuguese islands: the Portuguese service lists no station in the Azores or Madeira.
- Compute averages, rankings or forecasts. It delivers the rows; the calculation is yours.
- Use proxies, log in, solve CAPTCHAs or look for a way around a block.

### Limits and fair use

- **Read one province or district at a time.** Spain's service is read one province per request (the whole country in one file is 12 MB and would not fit a small run), Portugal's one district per request, with all road fuels in one go. A whole-country run is 52 requests for Spain and about 20 for Portugal, about a minute each at the default pace of one request per second. The minimum pause is 500 ms.
- **Robots.txt first.** The actor reads the robots.txt of each host before its first request and skips a country whose rules forbid the path.
- **Freshness.** Spain refreshes its file about every half hour and stamps the whole file with one time. Portugal stamps each price when the operator reports it, so some prices are days old. Filter on `priceUpdatedAt` if you need recent prices only.
- **Default of 500 stations.** Without filters the first 500 stations are delivered and reading stops there; the run summary says that more exist. Set **Maximum items** to 0 for everything that matches.
- **Spain without a province.** A city, centre point or fuel filter without a province reads all 52 provinces first, because the service cannot be searched by city. A Spanish postal code names its province, so the run reads only that one.
- **If a source is down or refuses the request,** the run ends normally, delivers nothing from that country and charges only the start event. After three failed requests in a row the actor gives that country up, and the run summary lists the errors. Run again later.
- **Memory.** The default of 256 MB is enough for both countries in full.

#### What a run does, step by step

1. It reads your input and checks the values that could cost you money if misread: an unknown country, a centre point that is not a coordinate, a radius that is not a number, a fuel it does not know. A bad value stops the run before any request is sent.
2. For each country it requests the robots.txt of the data host and skips the country if the path is disallowed.
3. It reads the provinces or districts one request after the other, at the pace you set.
4. It maps every station to a row, counts the brand labels, applies your filters and, with Only new rows, drops the stations without anything new.
5. It delivers the rows in batches of 500 and stops at your maximum or your charge limit.
6. It writes the run summary and the coverage report, and sends the webhook message if you set one.

### Data source (and licence)

The Spanish prices come from the **Precios de carburantes** REST service of the Ministerio de Industria y Turismo, at [sedeaplicaciones.minetur.gob.es](https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/EstacionesTerrestres/), which the ministry publishes with its open data (see its [open data portal](https://sede.serviciosmin.gob.es/es-ES/datosabiertos/Paginas/index.aspx)). The general reuse conditions of that portal, which follow Law 37/2007 on the reuse of public sector information, ask you to name the source, to state the date of the last update and not to alter the content or its meaning. The Portuguese prices come from the **Preços de Combustíveis** service of the Direção-Geral de Energia e Geologia (DGEG), at [precoscombustiveis.dgeg.gov.pt](https://precoscombustiveis.dgeg.gov.pt/), whose published policy lets the data be used freely, asks you to identify DGEG as the source, to give the date and time of the last update of the prices, and to reproduce the information faithfully.

The actor delivers the values as the services publish them: it converts decimal commas into numbers, writes the times in one format, adds a normalised fuel code and a unit next to the source's own label (`rawFuelLabel`) and leaves the prices and the update times unchanged. Every row carries `source` and `priceUpdatedAt`, so keep them next to the prices you publish and name the Ministerio de Industria y Turismo (Spain) or DGEG (Portugal) as the source. The Spanish file has a single update time for all its prices; the Portuguese service has one per price.

The actor reads only those two services. Rowcraft is independent and not affiliated with or endorsed by the Ministerio de Industria y Turismo, the DGEG or any other public body; the names belong to their owners.

### Privacy: no personal data

Rowcraft actors are built not to collect personal data:

- The services describe fuel stations, which are business locations: an address, prices and, for Spain, opening hours. They carry no e-mail addresses and no phone numbers, and the actor adds none.
- The Portuguese service gives each station a name, which can be the name of a person who runs it. The actor does not read it.
- A station's brand label is delivered only when the same label is used by at least five stations in the data read in the run. A label used by fewer stations may be the name of an individual operator, so it becomes `null`. The check counts the stations read in the run, so a run over one small province can show `null` for a brand that a nationwide run would show.
- The only coordinates delivered are those of the stations.

You are responsible for using the data in line with the law that applies to you.

### Other Rowcraft actors

Related Rowcraft actors. All of them read official or public sources, deliver flat rows without private contact details and support only-new runs and notifications; the full list is on the [Rowcraft profile](https://apify.com/rowcraft):

- [France Fuel Prices Scraper – Official Open Data by Station](https://apify.com/rowcraft/france-fuel-prices-scraper) — Current fuel prices at every French station, straight from the official open-data feed (Licence Ouverte 2.0): gazole, SP95, SP98, E10, E85 and GPLc, each with its update time.
- [Brazil CNPJ Companies Scraper: Receita Federal Register Data](https://apify.com/rowcraft/brazil-cnpj-companies-scraper) — Company records from the Brazilian CNPJ register for a list of CNPJ numbers or a search by state, city and CNAE activity: legal name, trade name, status, legal nature, capital, address, opening date and size.
- [Company Register Lookup Europe: Name, ID, Domain or LEI](https://apify.com/rowcraft/company-intelligence-europe) — Look up a European company by name, domain or registry number and get one unified profile from the official registers of France, Norway, Poland, the UK and the LEI index.
- [SIRENE France Company Scraper: SIREN, NAF, Revenue, Status](https://apify.com/rowcraft/france-company-register) — French companies from the official Recherche d'entreprises API (INSEE SIRENE + INPI): SIREN and SIRET, legal form, NAF activity, head-office address, employee band, revenue, net income, labels, VAT number and optional directors (no birth dates).
- [KRS Poland Companies Scraper: Register Data, PKD, Status](https://apify.com/rowcraft/krs-poland-companies) — Polish companies from the official KRS register API of the Ministry of Justice: name, KRS, NIP and REGON numbers, legal form, status, address, PKD activity codes, share capital, branches and registration dates.
- [LEI Records Scraper: Legal Entity Identifier Lookup API](https://apify.com/rowcraft/lei-records-scraper) — Legal Entity Identifier (LEI) records from the Global LEI Index, read through the public GLEIF API: legal name, legal form, register number, addresses, status, renewal date and parent companies.

### FAQ

**How fresh are the prices?** Spain refreshes its file about every half hour and the actor reads it at the start of the run, so a run shows what the service held a few minutes earlier. Portugal marks every price with the time the operator reported it, which can be days ago.

**Why is a fuel `null`?** The station does not sell that fuel, or has no price for it at the moment. `fuelsAvailable` lists the fuels that do have a price, and `prices` holds all of them with the source's own label.

**Why is the brand `null`?** The brand is shown only when at least five stations in the data read in the run use the same label. A rarer label can be the name of the person who runs the station, so the actor does not deliver it. Reading more provinces or districts in one run raises the counts.

**Why do two stations in the same place have different times?** In Spain the whole file carries one time, so all prices of a run share it. In Portugal every price has its own time, and prices of one station can differ by days.

**What does the time in `priceUpdatedAt` mean?** It is local time as the service publishes it, without a time zone: Spanish time for Spain and Portuguese time for Portugal. The `timeZone` column says which. The actor does not shift it.

**How do I find the cheapest stations near me?** Set `center` to your coordinates, `radiusKm` to the distance you accept and `fuels` to your fuel, then sort the dataset by the price column. The nearest stations are delivered first and `distanceKm` shows how far each one is. Add a province or district to make the run faster.

**What is the difference between `petrol95` and the E10 price?** `petrol95` is the standard 95 petrol, which in Spain is E5 and in Portugal is Gasolina simples 95. Spanish stations that sell E10 or E25 list those prices in `prices` under `petrol_95_e10` and `petrol_95_e25`.

**Why is Portugal's island territory missing?** The Portuguese service lists stations of the mainland only; the Azores and Madeira do not appear in it, so the actor has nothing to deliver for them.

**Can I get other countries?** Not from this actor. France is covered by its own Rowcraft actor. The actor reads only sources that publish open, official prices at station level and that allow automated access.

**Is it legal?** Yes, for the data as published. Both services publish the prices for reuse and ask for the source and the time of the update, which each row carries. The actor reads them politely: robots.txt first, at most one request per second, no proxy. How you use the data is your responsibility under the law that applies to you.

**Can I pay only for new fuel stations?** Yes. Turn on **Only new rows since the last run**: the actor remembers what it delivered to you and later runs with the same state name deliver (and charge) only the new rows.

**How do I get the results into Slack, Google Sheets, Make, n8n or Zapier?** Use the Integrations tab of the Actor for the data, and the `notifyUrl` field for a message when a run ends (Slack and Discord webhooks work directly; catch hooks of Make, n8n and Zapier too).

**Can I call it from code or from an AI agent?** Yes: through the Apify API, the JavaScript and Python clients, and the Apify MCP server. The API tab shows a working example with the input of this Actor.

**Does it collect personal data?** No. Contact persons, e-mails and phone numbers are removed and fields about people are never requested. See the Privacy section.

**The run finished with 0 rows. Did I pay?** No: with no rows delivered nothing is charged apart from the start event. Check the run summary (`RUN_SUMMARY`) for warnings, widen the filters, or run with **Only new rows** off if everything matching was already delivered to you.

### Support and reviews

**Problems, missing fields, questions:** open an issue on the **Issues** tab of this Actor. We answer within one working day and fix confirmed bugs in the next release; the changelog lists every change.

If this Actor saved you time, a short review on its page helps other users find it and helps us keep improving it.

# Changelog

This Actor's version history is a separate document: https://apify.com/rowcraft/europe-fuel-prices-scraper/changelog.md

# Actor input Schema

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

Which countries to read: Spain (ES) from the Ministry of Industry and Tourism, Portugal (PT) from DGEG. Pick one or both. Provinces apply to Spain and districts to Portugal; a country you select without its own filter is read in full.

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

Keep only stations that sell these fuels (a price must be listed). Empty = every station, including those with no price at the moment. Every row still carries all the prices the station lists.

## `maxItems` (type: `integer`):

Stop after this many stations. 0 = no limit (Spain has about 11,400 stations and mainland Portugal about 3,100). With a centre point the nearest stations come first.

## `provinces` (type: `array`):

Spanish provinces to include, one per line: the code 01 to 52 (28 = Madrid, 02 = Albacete) or the name (Madrid, Málaga, A Coruña). Empty = all 52 provinces, which takes about a minute because the service is read one province at a time.

## `districts` (type: `array`):

Portuguese districts to include, one per line: the number 1 to 18 or the name (Lisboa, Porto, Faro, Évora). Empty = all 18 districts of the mainland.

## `postalCodes` (type: `array`):

One per line. Spain: five digits (28001; a dropped leading zero is repaired, 2001 = 02001). Portugal: 1000-001, or just the first four digits (1000) for the whole area. A Spanish postal code also selects its province, so the run reads one province, not the country.

## `cities` (type: `array`):

City or municipality names, one per line. Capitals, accents and hyphens do not matter (a coruna = A Coruña). A city filter without a province reads every province, which takes about a minute.

## `fuelsMatch` (type: `string`):

With more than one fuel selected: a station must sell all of them (default) or at least one of them.

## `center` (type: `string`):

Latitude and longitude as Google Maps shows them, for example 40.4168, -3.7038. Stations within the radius below are delivered, nearest first, with their distance. Without a province or district every province is read, which takes about a minute.

## `radiusKm` (type: `number`):

Distance from the centre point, in kilometres, straight line. Used only when a centre point is set.

## `onlyNew` (type: `boolean`):

Remember the rows already delivered to you (in a key-value store in your Apify account) and deliver only rows that appeared since the last run with the same state key. The first run delivers everything that matches; later runs cost only the new rows. Ideal for schedules.

## `stateName` (type: `string`):

Name of the memory used by "Only new rows". Use a different name for each schedule or search (for example `stockholm-python`) so they do not share one memory.

## `notifyUrl` (type: `string`):

A webhook address that receives one JSON POST with the number of new rows and links to the dataset: a Slack or Discord incoming webhook, a Make, n8n or Zapier catch hook, or your own endpoint. The message is also in the `text` (Slack) and `content` (Discord) fields.

## `notifyWhenEmpty` (type: `boolean`):

By default the notification is sent only when at least one row was delivered.

## `requestIntervalMs` (type: `integer`):

Pause between two requests to the official services. Default 1000 ms (one request per second), minimum 500. A province or a district is one request.

## Actor input object example

```json
{
  "countries": [
    "ES"
  ],
  "fuels": [
    "diesel"
  ],
  "maxItems": 100,
  "provinces": [
    "02"
  ],
  "fuelsMatch": "all",
  "radiusKm": 10,
  "onlyNew": false,
  "stateName": "default",
  "notifyWhenEmpty": false,
  "requestIntervalMs": 1000
}
```

# Actor output Schema

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

All delivered rows in the default dataset (JSON, CSV, Excel and other formats via the dataset API).

## `runSummary` (type: `string`):

Counts, warnings and why the run stopped (RUN_SUMMARY record in the default key-value store).

# 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"
    ],
    "fuels": [
        "diesel"
    ],
    "maxItems": 100,
    "provinces": [
        "02"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rowcraft/europe-fuel-prices-scraper").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"],
    "fuels": ["diesel"],
    "maxItems": 100,
    "provinces": ["02"],
}

# Run the Actor and wait for it to finish
run = client.actor("rowcraft/europe-fuel-prices-scraper").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"
  ],
  "fuels": [
    "diesel"
  ],
  "maxItems": 100,
  "provinces": [
    "02"
  ]
}' |
apify call rowcraft/europe-fuel-prices-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rowcraft/europe-fuel-prices-scraper"
        }
    }
}
```

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/dSJ8vxPoW7hlf0lEx/builds/qulWSeDdYAOjqom6Z/openapi.json
