Nine read-only JSON endpoints over European fuel prices, stations, tolls and twenty years of weekly national history. No SDK, no OAuth dance — plain HTTPS GET. Every response shown below was captured from the live service.
Base URL https://www.fuel-prices.eu. All endpoints are
GET and return UTF-8 JSON. All but one send
Access-Control-Allow-Origin: *, so they work straight from a browser.
| Protocol | Use HTTPS. Plain HTTP is answered rather than redirected, so a mistyped scheme will silently work unencrypted — pin https:// in your client. |
| Currency | EUR per litre, tax inclusive, in every market — local currencies are converted at the daily central-bank rate. |
| Timestamps | UTC, format YYYY-MM-DD HH:MM:SS, except board_now.php which returns a Unix epoch. |
| Numbers | Prices are rounded to 3 decimals. Most endpoints emit JSON numbers; action=brands returns its aggregates as strings — cast before you compare. |
| CORS | Access-Control-Allow-Origin: * on every endpoint below except board_now.php, which sends no CORS header and is therefore server-side only. |
| Caching | Set per endpoint: max-age=300 on live/api.php, 120 on board_now.php and geojson.php, 900 on natural-language, 3600 on history, 86400 on tolls. Apache appends a second, longer Cache-Control: max-age=3600 — treat the shorter value as authoritative. |
There is none today, and we would rather say so than pretend.
X-API-Key request header and the open endpoints will keep working at the
Free quota, so nothing you build today breaks.
Plan quotas: 1,000 requests a month on Free, 50,000 on Pro, 500,000 on Business, with burst ceilings of 60, 600 and 3,000 an hour.
Because there are no keys yet, those numbers are not machine-enforced. What is actually in place today:
/research/data/, /press/dl/): 150 or more files in
10 minutes, or more than 600 MB in an hour (or one and a half times the complete /research/ data set,
if that is larger), from one IP blocks that IP for an hour (longer if it repeats)./search/?q= answers 429 after 20 queries in 10 minutes from one IP.?format= responses and the
GeoJSON/API endpoints is logged (above 120 station or ?format= requests, or 300 GeoJSON/API
requests, in 10 minutes). Today this only counts, nothing is blocked automatically; search-engine and AI
crawlers that we can verify by IP address (not by User-Agent) are left out of the count.There is no per-minute quota on the API endpoints themselves, the natural-language endpoint included.
Cache responses for at least their max-age, set a real User-Agent that names your
product, and you will not reach any of these. For bulk or scheduled access, write to
hi@fuel-prices.eu.
Live endpoints answer with {"ok": false, "error": "..."};
the history endpoint uses a bare {"error": "..."}. Check the HTTP status,
not the shape.
| Status | When | Body |
|---|---|---|
| 200 | Success. An empty result set is still a 200 — check the array length. | {"ok":true,"data":{…}} |
| 400 | Missing or invalid parameter, or an unknown action. | {"ok":false,"error":"country parameter required"} |
| 404 | Wrong path. You get the site’s HTML 404 page, not JSON. | HTML |
| 429 | Too many requests. Today only /search/?q= answers this (20 queries in 10 minutes per IP). The data endpoints do not send 429: an IP blocked for bulk traffic sees its connections refused or timing out for an hour. | HTML |
| 500 | Database or upstream failure on our side. Retry with backoff. | {"ok":false,"error":"…"} |
Real 400, captured 8 September 2026
$ curl -s -i "https://www.fuel-prices.eu/live/api.php?action=brands" | head -1
HTTP/2 400
$ curl -s "https://www.fuel-prices.eu/live/api.php?action=brands"
{"ok":false,"error":"country parameter required"}
$ curl -s "https://www.fuel-prices.eu/live/api.php?action=nope"
{"ok":false,"error":"Unknown action: nope"}
$ curl -s "https://www.fuel-prices.eu/lang/api/country-history.php?cc=XYZ"
{"error":"Invalid country code"}
National average, minimum and maximum per fuel type, with the number of contributing stations and the freshness stamp. This is the call most integrations start with.
| Parameter | Required | Meaning |
|---|---|---|
| action | yes | summary |
Returns every live market in one document (about 19 KB). Germany is excluded: we hold
the data but do not publish it yet. last_fetched is UTC, normalised from the
database clock, and is the number to trust when you decide whether a price is stale.
Since 2026-09-15 each market also carries last_fetched_ts (Unix epoch,
seconds) and last_fetched_iso (ISO-8601 with an explicit offset) — the same
instant, in forms that carry their own time zone. last_fetched itself is
unchanged.
min is the cheapest price changed by the source in the last 72 hours
(the latest bulletin for Greece), leaving out quarantined prices and prices below 75% of the
national median for that fuel; special tax areas such as Livigno or the Canary Islands stay in.
It is the same number as the page title, the fuel cards on /live/<country>/
and the first feature of geojson.php?sort=asc. avg, max
and stations use every price of the market’s freshness window. The rule is
repeated in data.min_rule. Each market carries "live": true, except
CZ: "live": false, "status": "weekly, not live" — the weekly
national average of the Czech Statistical Office, with stations = 0.
Response (2 of 12 markets shown)
{
"ok": true,
"data": {
"countries": [
{
"country_code": "AD",
"fuels": {
"diesel": { "stations": 54, "min": 1.619, "max": 1.64, "avg": 1.629 },
"gpl": { "stations": 1, "min": 0.876, "max": 0.876, "avg": 0.876 },
"sp95": { "stations": 54, "min": 1.609, "max": 1.628, "avg": 1.616 },
"sp98": { "stations": 54, "min": 1.669, "max": 1.698, "avg": 1.678 }
},
"last_fetched": "2026-09-07 21:07:07",
"total_stations": 54,
"live": true
},
{
"country_code": "AT",
"fuels": {
"diesel": { "stations": 1722, "min": 1.799, "max": 2.449, "avg": 2.092 },
"sp95": { "stations": 1592, "min": 1.689, "max": 2.199, "avg": 1.889 }
},
"last_fetched": "2026-09-07 18:40:55",
"total_stations": 2378,
"live": true
}
]
}
}
Average, cheapest and dearest price per retail brand in one market, for one fuel. Brands with fewer than five stations carrying that fuel are left out, so a thin market can legitimately come back empty.
| Parameter | Required | Meaning |
|---|---|---|
| country | yes | ISO 3166-1 alpha-2, upper or lower case. 400 if missing. |
| fuel | no | One of the fuel codes below. Defaults to diesel. |
country=FR returns an empty brands array — that is the data,
not a fault. GB, IT, ES, PT, AT and GR are populated.Response (3 of many brands shown)
{
"ok": true,
"data": {
"country": "GB",
"fuel": "diesel",
"brands": [
{ "brand_name": "Go", "logo_file": null, "station_count": 6,
"price_min": "1.966", "price_max": "2.083", "price_avg": "2.043" },
{ "brand_name": "Thompson Fuels Ltd", "logo_file": null, "station_count": 6,
"price_min": "2.059", "price_max": "2.059", "price_avg": "2.059" },
{ "brand_name": "Costco Wholesale", "logo_file": null, "station_count": 18,
"price_min": "2.013", "price_max": "2.129", "price_avg": "2.060" }
]
}
}
Stations inside a radius, nearest first, each with every fuel price we currently hold and when the source last moved it. The workhorse of any “cheapest fuel near me” feature.
| Parameter | Required | Meaning |
|---|---|---|
| country | yes | ISO alpha-2. 400 if missing. |
| lat, lng | yes | Decimal degrees, WGS 84. Both must be non-zero or you get a 400. |
| radius | no | Kilometres. Default 10, silently capped at 50. |
| limit | no | Number of stations. Default 20, silently capped at 100. |
| fuel | no | Filters Romania only (sp95, sp98, diesel, diesel_premium, gpl); elsewhere the response carries every fuel the station sells. |
Countries with station data: AT, AU, DK, ES, FR, GR, HR, IS, IT, RO, SE, SI. RO comes
from the ANPC price monitor, like /live/romania/: price in EUR at the
ECB rate of the reading, price_ron as published, the station code as id
and the station page as url. GB answers 403: DESNZ Fuel Finder
station prices are not redistributed through this API. CZ and any country without station
data answer 404 with an explanation — never ok: true with an empty
list. An empty stations array means only that nothing is inside your radius.
Response (first station of three)
{
"ok": true,
"data": {
"country": "FR",
"center": { "lat": 48.85, "lng": 2.35 },
"radius": 10,
"stations": [
{
"id": 6808,
"name": "Station PARIS",
"brand": null,
"logo": null,
"address": "8,10,10bis Rue Bailleul",
"city": "PARIS",
"postal_code": "75001",
"lat": 48.861,
"lng": 2.341,
"is_open": true,
"distance_km": 1.4,
"prices": {
"diesel": { "price": 2.68, "ts": 1788851424, "updated": "2026-09-04T21:30:24+02:00" },
"sp95": { "price": 2.49, "ts": 1788851424, "updated": "2026-09-04T21:30:24+02:00" },
"sp98": { "price": 2.57, "ts": 1788851424, "updated": "2026-09-04T21:30:24+02:00" }
}
}
]
}
}
The smallest useful call on the service: one flat object of national petrol and diesel averages plus a server epoch, under 500 bytes. Ideal for a ticker or a widget.
Response (truncated)
{
"ok": true,
"ts": 1788814863,
"petrol": { "AD": 1.616, "AT": 1.889, "DK": 2.392, "ES": 1.823, "FR": 2.166,
"GB": 1.935, "GR": 2.091, "HR": 1.675, "IS": 1.795, "IT": 2.056, … },
"diesel": { … }
}
ts is a Unix epoch in seconds — the only endpoint that does not use a
date string.
Access-Control-Allow-Origin, so a browser fetch() from another
origin is blocked. Call it server-side, or use
live/api.php?action=summary from the browser.A ready-to-drop FeatureCollection for Leaflet, Mapbox or anything else that
eats GeoJSON: one point per station, price and opening state in the properties, plus a
meta block with the price range in view.
| Parameter | Required | Meaning |
|---|---|---|
| country | no | ISO alpha-2. Defaults to FR. |
| fuel | no | Defaults to diesel. |
| bounds | no | south,west,north,east. Set it. The default box is centred on western Europe, so a country outside it returns zero features rather than an error. |
| limit | no | Default 1500, clamped to 10–5000, then capped by the area of bounds (degrees of latitude × longitude): up to 3 → 5000, up to 15 → 1500, up to 150 → 800, larger → 400; without bounds → 1500. The effective value is in meta.limit and, when lowered, in the X-FP-Limit-Capped header. For a whole country, page through smaller boxes. |
| sort | no | asc (cheapest first, default) or desc. With asc the prices that count for the cheapest (rk = 1) come first, so the first feature is the cheapest shown on the page. |
Response (first feature)
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [16.0513559, 46.3896077] },
"properties": {
"id": 10075914,
"name": "West Oil BS Zavrč",
"brand": "West oil",
"address": "Zavrč 8c",
"city": "Zavrč",
"postal_code": "2283",
"price": 1.463,
"fuel": "diesel",
"is_open": true,
"ts": 1788959151,
"updated": "2026-09-07T14:05:51+02:00",
"oh": "Mo-Fr 08:00-18:00; Sa-Su off",
"open": "closed",
"next": "08:00"
}
}
],
"meta": { "country": "SI", "fuel": "diesel", "returned": 3, "limited": false, … }
}
oh (opening hours) is enriched from
OpenStreetMap for 8,172 stations and therefore carries
ODbL 1.0 share-alike terms. Everything else in the feature is ours. Questions:
hi@fuel-prices.eu.Station and price counts per market. For freshness, use last_fetched in
action=summary.
Response (truncated)
{
"ok": true,
"data": {
"station_counts": { "AD": 54, "AT": 2378, "ES": 12417, "FR": 10109, "GB": 8085,
"IT": 22423, … },
"price_counts": { "AD": 163, "AT": 4227, "ES": 29943, "FR": 35572, … }
}
}
station_counts lists every market we hold stations for, including ones not
yet published in summary (Germany, Sweden). A market appearing here is not a
promise that it appears in summary.
Weekly Euro 95 and diesel prices for one country, as three parallel arrays. Source is the European Commission Weekly Oil Bulletin, which is why the series is directly comparable between countries.
| Parameter | Required | Meaning |
|---|---|---|
| cc | yes | Two letters, case-insensitive. EL and GR both return Greece. Anything else gives a 400. |
Response (ends elided)
{
"cc": "DE",
"labels": ["2021-09-13", "2021-09-20", … , "2026-08-31"],
"e95": [1.629, 1.636, … , 2.236],
"diesel": [1.401, 1.42, … , 2.232],
"count": 258
}
The three arrays are index-aligned. A week with no reading for one fuel carries
0, not null — filter those out before you average.
Built for AI agents: ask in English, get structured data back with the intent it parsed,
a confidence score and full provenance. Also accepts POST with a batch of
questions.
| Parameter | Required | Meaning |
|---|---|---|
| q | yes | The question. Also works as a path segment:
/api/natural-language/Vignette+prices+in+Austria. Encode spaces as
+, not %20 — the server rejects an encoded space in the
path with a 403 before the endpoint is reached. |
| format | no | full (default), compact, or csv — CSV comes with a commented provenance header. |
| POST body | no | {"query":"…"} or {"queries":["…","…"]} for batches. |
Response (truncated)
{
"status": "ok",
"query": "cheapest diesel in Germany",
"intent": "cheapest_ranking",
"confidence": 0.95,
"parsed": { "countries": ["DE"], "fuel_type": "diesel", "time_range_weeks": 12 },
"metadata": {
"generated_at": "2026-09-07T21:07:36+00:00",
"data_date": "2026-08-31",
"source": "European Commission Weekly Oil Bulletin",
"currency": "EUR",
"unit": "per liter, tax-inclusive",
"update_frequency": "weekly (Thursday after 15:00 EET)"
},
"message": "EU countries ranked by diesel price, cheapest first (2026-08-31). #1: Malta.",
"data": {
"ranking_top3": [
{ "rank": 1, "country_code": "MT", "country_name": "Malta", "price_euro95": 1.34, "price_diesel": 1.21 },
{ "rank": 2, "country_code": "PL", "country_name": "Poland", "price_euro95": 1.525, "price_diesel": 1.669 }
]
}
}
Every tolled tunnel, bridge, pass and mainline barrier we track in Europe, with the price in euro and in local currency, plus coordinates. One document, 547 records, cached for a day.
No parameters — you get the whole set (about 210 KB) and filter client-side on
country_code or type. Human-readable notes and the licence live
at /api/toll/docs.php.
Response (first record)
{
"count": 546,
"data": [
{
"id": 1445,
"country_code": "AD",
"type": "tunnel",
"barrier_type": "mainline",
"name": "Tunel d'Envalira",
"price_eur": 7.9,
"price_local": 7.9,
"currency": "EUR",
"info": "Connects Pas de la Casa (FR border)",
"lat": 42.545,
"lng": 1.715
}
]
}
Fuel codes, ordered by how much data we hold on each.
| Code | Fuel | Notes |
|---|---|---|
| diesel | Diesel / gasoil | Every live market. |
| sp95 | Unleaded 95 (Euro 95) | Every live market except GB, where E10 has replaced it. |
| sp98 | Unleaded 98 | Most western markets. |
| e10 | Petrol, 10% ethanol | FR, GB, AU. |
| e5 | Petrol, 5% ethanol | GB, AU. |
| gpl | LPG / autogas | AD, ES, FR, GR, HR, IT, LT, PT, SI. |
| e85 | Superethanol E85 | FR. |
| cng | Compressed natural gas | IT. |
| lng | Liquefied natural gas | IT, thin coverage. |
| hvo | Hydrotreated vegetable oil | GB, very thin coverage. |
Country codes are ISO 3166-1 alpha-2 throughout. Two things to
know: Greece is GR in live data but appears as EL in
Commission-sourced history (both are accepted on the history endpoint), and Australian
markets are grouped under AU with per-state sources.
| Date | Change |
|---|---|
| 2026-10-11 | action=summary: min is the cheapest price changed in the last 72 hours (rule in data.min_rule), the same as the pages and the map; LPG in Italy is no longer cut by the petrol/diesel plausibility floor (it showed 1 station instead of about 4 500); new live flag, with CZ marked "weekly, not live". action=stations now covers Romania and answers 403 (GB) or 404 (CZ, unknown country) instead of an empty list. action=stats no longer publishes the ingest log (recent_fetches). /live/geojson.php: area-based limit caps documented, meta.limit and rk added, implausible prices no longer returned. |
| 2026-09-15 | Freshness stamps carry their time zone. ts (Unix epoch, UTC) added to /live/geojson.php and to action=stations; on both, updated changed from a zone-less YYYY-MM-DD HH:MM:SS to ISO-8601 with an offset. The old strings were the source's wall clock (Paris on FR, London on GB, Sydney on AU-NSW) or the database clock — read as UTC they were 1–3 hours out. action=summary gained last_fetched_ts and last_fetched_iso; its last_fetched is unchanged. The undocumented fetched property of /live/geojson.php was removed. |
| 2026-09-08 | First published reference. Nine endpoints documented against captured responses; OpenAPI 3 spec added at /api/openapi.json. |
| 2026-09-05 | Live prices moved behind a sanity filter: a station whose feed reports an implausible price now serves its last validated price, or nothing. Response shape unchanged. |
| 2026-07-25 | last_fetched in action=summary normalised to true UTC. Values previously read up to three hours fresher than reality. |
Breaking changes get announced by e-mail to everyone on a paid plan at least 30 days ahead. Additive fields can appear at any time — parse defensively and ignore keys you do not know.
Free needs no key: pick an endpoint above and call it. For station-level data, exports or an uptime commitment, use the contact page or email hi@fuel-prices.eu.
Researchers: download the data — the full EU bulletin since 2005 and daily statistics per market as CSV or Parquet, with how to cite, on /research/.