Fuel Finder API: how to actually use it
Working notes on the UK government's Fuel Finder service. Some of this is in the official docs. The rest we could not find documented anywhere, so we worked it out by calling the API.
Fuel Finder publishes near live pump prices for all 8,000 or so UK forecourts. We pull it twice a day for FuelHunter, so these notes come from running against the API rather than from reading the portal.
API last checked 3 September 2026.
You need a UK IP address
The API only answers requests from a UK address. From anywhere else it returns 403 before it looks at your credentials, so a valid token makes no difference.
The 403 carries server: CloudFront,
x-cache: Error from cloudfront and
content-length: 0. There is no JSON error and no message in the
body, which makes it easy to mistake for a network problem at your end.
We tested from a UK connection and from two cloud data centres, on the
same credentials with plain curl:
| Request from | Root | Authenticated pull |
|---|---|---|
| UK | 301 | Full pull, 500 records a batch |
| AWS, US | 403, empty body | Not reached |
| Hetzner, Germany | 403, empty body | Not reached |
It applies to the whole host rather than to particular routes.
/, /api/v1/pfs, /robots.txt and a path
that does not exist all return the same 403. TLS completes normally and you
get a full HTTP/2 response back, so there is nothing to fix in your client.
Changing the user agent does not help.
If you are planning to run this from a cloud region outside the UK, plan for it now. Either host in the UK, or fetch in the UK and move the result to wherever the rest of your stack lives.
Getting a token
OAuth 2.0, client credentials. Tokens last an hour. Credentials come from the developer portal, signing in with GOV.UK One Login.
The base URL is:
https://www.fuel-finder.service.gov.uk/api/v1
Worth setting that against the portal's worked examples, which use
api.fuelfinder.service.gov.uk with no hyphen. That host does not
resolve, so use the one above.
POST https://www.fuel-finder.service.gov.uk/api/v1/oauth/generate_access_token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
client_id=<id>
client_secret=<secret>
scope=fuelfinder.read
The token comes back nested under data, at
data.access_token, so reading access_token from the
top of the response gives you nothing and every later call returns 401. Send
it as Authorization: Bearer <token>.
Endpoints and paging
| Endpoint | Returns |
|---|---|
GET /pfs?batch-number=N | Forecourt info: name, brand, location, amenities, opening times |
GET /pfs/fuel-prices?batch-number=N | Prices per grade, with submission and effective timestamps |
Both page at 500 records, starting at batch-number=1. The UK
needs 17 batches per endpoint at the moment.
Paging ends with a 404. An empty 200 is not the end of the data, it is a blip, and treating it as the end will silently cut your pull short. That has happened to us twice. Once prices stopped at batch 11 and we shipped 5,000 of about 7,980 records, which looked like a lot of stations simply having no price. Once batch 1 came back empty, an all blank file passed a row count check, and the site showed no prices at all.
So retry an empty page the way you would retry a 500, and check coverage before you let a pull replace data you already have. Sporadic 500, 503 and 504 responses and dropped connections turn up mid run as well, historically in something like one run out of three. Back off and retry.
Rate limits
| Limit | Value |
|---|---|
| Requests per minute, per client | 100, live and test alike |
| Concurrent requests | 1 |
| Over the limit | 429 |
The portal also suggests caching station data for an hour, prices for 15 minutes and search results for 5 minutes.
A full pull is 35 requests, 17 batches from each endpoint plus the token, and takes about three and a half minutes single threaded with a half second gap between batches. That works out at roughly 10 requests a minute against a limit of 100. Polling every 30 minutes, which matches how often the service publishes, uses about 12% of the allowance.
The API publishes within 30 minutes of a change. The twice daily figure you may have seen applies to the manual CSV download from the portal.
We have not found a delta or changed-since parameter. If one exists, a poll drops from 35 requests to a handful.
The CSV columns
68 columns, in six groups:
| Group | Columns | Prefix |
|---|---|---|
| Forecourt | 10 | forecourts. |
| Location | 8 | forecourts.location. |
| Prices | 18 | forecourts.fuel_price. plus two timestamp families |
| Opening hours | 21 | forecourts.opening_times.usual_days.<day>. |
| Bank holiday | 3 | forecourts.opening_times.bank_holiday.standard. |
| Amenities | 8 | forecourts.amenities. |
Six fuel grades, E5, E10, B7S
(standard diesel), B7P (premium diesel), B10 and
HVO. Each has three columns:
forecourts.fuel_price.<GRADE>
forecourts.price_submission_timestamp.<GRADE>
forecourts.price_change_effective_timestamp.<GRADE>
Opening hours are seven blocks of three, with the day in the column name rather than in the row order:
forecourts.opening_times.usual_days.monday.open_time
forecourts.opening_times.usual_days.monday.close_time
forecourts.opening_times.usual_days.monday.is_24_hours
The full list, with a type and group against each column, is in the companion repository, along with a short Python client that handles the token nesting, the 404 and the empty page retry.
Where the CSV guide differs from the file
The CSV guide lists the columns in a shortened display form. The header in the file uses the full nested path, so the names in the guide will not match what you read:
| Shown in the guide | In the file |
|---|---|
forecourts.postcode | forecourts.location.postcode |
forecourts.latitude | forecourts.location.latitude |
forecourts.car_wash | forecourts.amenities.vehicle_services.car_wash |
The whole of the location, amenities and opening hours blocks are affected. Match on the short names and your parser still runs. It picks up the station level fields and returns nothing for the rest, so the data looks absent rather than unmatched. Worth checking first if you are seeing empty columns.
The portal's REST API examples have the same problem with the host name, covered above.
Reading the values
Prices arrive in mixed units. Most retailers submit pence per litre, so
138.9, and some submit pounds per litre, so 1.389.
Nothing in the row tells you which. Treating anything under 10 as pounds and
multiplying by 100 has worked on every file we have seen.
There are two timestamp formats. Price and forecourt update timestamps come in a JavaScript style:
Sat Mar 07 2026 15:34:42 GMT+0000 (Coordinated Universal Time)
Closure dates come in something closer to ISO:
2025-09-05 14:30:00
Parse both, and keep the raw string when neither matches.
The price submission timestamp records the last time the price changed, not the last time it was confirmed. Retailers are told to submit changes only, and not to resend an unchanged price with a new timestamp. Our own history bears that out: of 438,254 consecutive price pairs, 1.8% repeat the previous value. A station showing a two week old timestamp has usually held its price for two weeks, so anything you build on that field measures price stability rather than data freshness.
Two fields worth not trusting.
is_supermarket_service_station disagrees with reality often
enough that deriving supermarket status from brand_name against a
known list is more accurate. country is not written consistently,
so map it against known values rather than reading it straight.
Booleans are the uppercase strings TRUE and FALSE.
Missing values are empty cells throughout, so parse leniently.
Attribution
The data is Crown copyright under the Open Government Licence v3.0. The developer guidelines ask recipients not to pass the raw feed on, so there is no price data on this page or in the repository, only the schema and the notes. Our data sources page covers how FuelHunter uses it.