UK Food Hygiene Prospect Finder — FSA Ratings
Pricing
from $67.00 / 1,000 establishment records
UK Food Hygiene Prospect Finder — FSA Ratings
Prospecting lists of UK food businesses from the FSA's own public ratings API: rating, scores, address and authority contact per establishment.
Pricing
from $67.00 / 1,000 establishment records
Rating
0.0
(0)
Developer
NexGen Watch
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
🔎 UK Food Hygiene Prospect Finder — FSA Ratings
Build a prospecting list of UK food businesses from the Food Standards Agency's own public API — filtered by local authority, business type, name and hygiene rating — with a link to the FSA's page for every establishment.
Built for food-safety consultants, pest control, EHO training, equipment suppliers and anyone whose buyer is a business that needs to improve its rating.
Output is one establishment row per result; billing is pay-per-event, the value event being one establishment record (a $0.02 start fee per run, then $0.10 per establishment record). Source: api.ratings.food.gov.uk.
No login, no API key and no CAPTCHA solving are involved: the source is read logged-out.
📊 Sample Output
Real rows from run bztzbXdHh5fSaREgS on build 0.1.18 (2026-09-17), the same input as the Quick start below — every value is as the source published it (emails masked, long text shortened):
| business_name | business_type | address | local_authority | local_authority_email | local_authority_website |
|---|---|---|---|---|---|
| 196 | Restaurant/Cafe/Canteen | 196 Mill Road, Cambridge, Cambridgeshire, CB1 3NF | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| 44 Russell Street (Respite Service) | Hotel/bed & breakfast/guest house | 44 Russell Street, Cambridge, Cambridgeshire, CB2 1HT | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| 5 Blends | Restaurant/Cafe/Canteen | 42 Mill Road, Cambridge, Cambridgeshire, CB1 2AD | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| 5 Chapel Street B&B | Hotel/bed & breakfast/guest house | 5 Chapel Street, Cambridge, Cambridgeshire, CB4 1DY | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| 7Bone Cambridge | Takeaway/sandwich shop | Unit 3 To 4 Restwell House Coldhams Road, Cambridge, Cambridgeshire, C | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| 92 @ Young Street | Restaurant/Cafe/Canteen | The Health Building Young Street, Cambridge, Cambridgeshire, CB1 2LZ | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| A WALLER & SON | Manufacturers/packers | 15 Victoria Avenue, Cambridge, Cambridgeshire, CB4 1EG | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
| A&M Pub Services (The Ship) | Pub/bar/nightclub | The Ship Northfield Avenue, Cambridge, Cambridgeshire, CB4 2LG | Cambridge City | [email on record] | http://www.cambridge.gov.uk |
The run finished with the status message: NORMAL: returned=10 scanned=100 server_total=1486 | establishment-result charged=10 (one per returned establishment)
✅ What you get
Each row is flat JSON with these fields (from the dataset schema and the sample run; a field the source does not publish for a given row is null):
record_type(string) — e.g.establishmentfhrs_id(integer/null) — e.g.1472773business_name(string/null) — e.g.196business_type(string/null) — e.g.Restaurant/Cafe/Canteenbusiness_type_id(integer/null) — e.g.1address(string/null) — e.g.196 Mill Road, Cambridge, Cambridgeshire, CB1 3NFpostcode(string/null) — e.g.CB1 3NFlocal_authority(string/null) — e.g.Cambridge Citylocal_authority_id— e.g.027local_authority_email(string/null) — e.g.[email on record]local_authority_website(string/null) — e.g.http://www.cambridge.gov.ukrating_value(string/null) — e.g.5rating_is_numeric(boolean/null) — e.g.Truerating_date(string/null) — e.g.2026-02-25T00:00:00scheme_type(string/null) — e.g.FHRSnew_rating_pending(boolean/null) — e.g.Falsehygiene_score(integer/null) — e.g.5structural_score(integer/null) — e.g.5management_score(integer/null) — e.g.0latitude(string/null) — e.g.52.1976012longitude(string/null) — e.g.0.1440631source_url(string/null) — e.g.https://ratings.food.gov.uk/business/en-GB/1472773source_is— e.g.the Food Standards Agency's own public ratings APIreport_id(string/null) — e.g.7100bec1c91c7b44establishments_returned(integer/null) — null in every sample rowestablishments_scanned(integer/null) — null in every sample rowestablishments_matching_the_server_side_filters(integer/null) — null in every sample rowcoverage_caveat(string/null) — null in every sample rowterminal(string/null) — null in every sample rowrefused(boolean/null) — null in every sample rowreason(string/null) — null in every sample row
Output
One record per establishment: name, business type, full address, postcode, rating and rating
date, the three component scores (hygiene, structural, management), local authority plus its
contact email and website, coordinates, and a link to the FSA's page. Plus a
search_summary naming every trap and a source_receipt.
Every run also writes a RUN_RECEIPT record to its key-value store with the source checks it made and the counts it charged — diagnostics never land in the paid dataset.
⚙️ Sample inputs
1. Quick start — the Store example (this is what the sample above came from)
{"localAuthorityName": "Cambridge City","maxResults": 10}
The sample run charged exactly: 1 × $0.02 apify-actor-start + 10 × $0.10 establishment-result = $1.02 on the Free tier — every delivered row was billed.
2. A smaller, narrowed run
{"localAuthorityName": "Cambridge City","maxResults": 5}
Caps the run at 5 rows — about $0.52 on the Free tier ($0.02 start + 5 × $0.10). localAuthorityName narrows what the source is asked for.
3. A full-size run
{"localAuthorityName": "Cambridge City","maxResults": 100}
Up to 100 rows (the schema default for maxResults) — about $10.02 on the Free tier ($0.02 start + 100 × $0.10) if the source has that many.
🧾 JSON sample record
One real record from run bztzbXdHh5fSaREgS, exactly as it lands in the dataset (emails masked, long text shortened):
{"record_type": "establishment","fhrs_id": 1472773,"business_name": "196","business_type": "Restaurant/Cafe/Canteen","business_type_id": 1,"address": "196 Mill Road, Cambridge, Cambridgeshire, CB1 3NF","postcode": "CB1 3NF","local_authority": "Cambridge City","local_authority_id": "027","local_authority_email": "[email on record]","local_authority_website": "http://www.cambridge.gov.uk","rating_value": "5","rating_is_numeric": true,"rating_date": "2026-02-25T00:00:00","scheme_type": "FHRS","new_rating_pending": false,"hygiene_score": 5,"structural_score": 5,"management_score": 0,"latitude": "52.1976012","longitude": "0.1440631","source_url": "https://ratings.food.gov.uk/business/en-GB/1472773","source_is": "the Food Standards Agency's own public ratings API","report_id": "7100bec1c91c7b44"}
🔧 How it works
Source. The actor reads api.ratings.food.gov.uk — endpoints: https://api.ratings.food.gov.uk. Public pages and feeds only; nothing behind a login.
Transport. Plain HTTPS from the Apify platform, no proxy. robots.txt is read first and a disallowed path is never fetched. Pacing: RATE_PER_SEC=2.0, MIN_INTERVAL=1.0.
Terminal states. A run ends NORMAL, CAPPED (your cap was reached), PARTIAL (something was withheld and the message says what), GENUINE_EMPTY (the source was read and truly had nothing in scope) or BLOCKED (the source refused or changed shape — the run FAILS loud and bills nothing). A zero-row run is never reported as a silent success.
Charging. Each establishment record is charged at the moment it is pushed (establishment-result); a row that fails to charge is not delivered, so the dataset count always equals the charged count.
The filter you came for does not work on the server. That is the point of this actor.
The FSA API accepts a ratingKey parameter. It is silently ignored. Measured 2026-07-31
against a control of 1,477 establishments in one authority:
| query | establishments returned |
|---|---|
| no rating filter (control) | 1,477 |
ratingKey=fhrs_5_en-gb (valid) | 1,477 |
ratingKey=fhrs_0_en-gb (valid) | 1,477 |
ratingKey=fhrs_9_en-gb (invented) | 1,477 |
ratingKey=banana (garbage) | 1,477 |
ratingKey= (empty) | 1,477 |
Every value returns the unfiltered set, and the API never says so. A tool that passed this through would answer "here are the 1-rated establishments in your area" with every establishment in the authority, five-star ones included — the exact opposite of a prospect list.
So this actor never sends it. Rating filtering runs here, over each record's own published
RatingValue, where it provably works.
An unfiltered query is refused — and the 403 is not a permission problem
GET /Establishments?pageSize=1&pageNumber=1-> 403 {"Message":"This is a CPU intensive query: please use one of thedocumented filters in your query (e.g. filter by LocalAuthority)."}
Passing that on would read as "the register is closed to you". It isn't — a filter is mandatory. The request is refused before it is sent, with the reason.
An unrecognised value returns zero, not an error
localAuthorityId=99999 -> HTTP 200, totalCount 0businessTypeId=99999 -> HTTP 200, totalCount 0
A typo therefore answers "there are no food businesses there". Every value is validated against the API's own enumerations first — 363 local authorities, 14 business types, 11 rating values — and an unknown one is refused, not guessed.
An ambiguous authority name is refused too. Cambridge is carried by three authorities —
Cambridge City, East Cambridgeshire and South Cambridgeshire. Matching on a prefix would
resolve silently to the city, and a buyer who meant the county would get a bill for the wrong
list with no way to see it happened. Names are matched as substrings and anything but a single
hit is refused, with the candidates named.
Scotland is different, and it is not fudged
Scottish establishments use FHIS, which publishes words — Pass, Improvement Required —
not numbers. ratingAtOrBelow cannot rank those, so it excludes them rather than guessing
a score, and every result reports how many were excluded that way.
Filters
| filter | what it does |
|---|---|
localAuthorityName / localAuthorityId | one of 363 authorities; ambiguous names refused |
businessTypeName / businessTypeId | one of 14 published types |
businessName | free text, matched by the FSA against establishment names |
ratingValues | keep only these ratings — applied here, not on the server |
ratingAtOrBelow | numeric ratings at or below N — the usual prospecting filter |
maxResults | how many establishments to return — also your cost ceiling |
Nothing is inferred
Every field is carried verbatim from the FSA. This actor assigns no score, ranks nobody, and states no view on any business's food safety beyond the rating the FSA itself published. A low rating is a snapshot from an inspection on a stated date, not a current condition.
Reading policy
api.ratings.food.gov.uk serves no robots.txt (404) and x-api-version: 2 is sent on every
call. The actor paces itself at 2 requests/second on an official public API. The rate is
not an input.
What is not done. No login, no cookie or CAPTCHA bypass, no private or personal-account data, no browser automation.
💰 Pricing example
| Event | Free | Bronze | Silver | Gold |
|---|---|---|---|---|
Actor Start (apify-actor-start) | $0.02 | $0.02 | $0.02 | $0.02 |
Establishment record (establishment-result) | $0.10 | $0.09 | $0.08 | $0.07 |
Worked at the live Free-tier price:
- 8 establishment records: $0.02 start + 8 × $0.10 = $0.82
- 25 establishment records: $0.02 start + 25 × $0.10 = $2.52
- 100 establishment records: $0.02 start + 100 × $0.10 = $10.02
A run that delivers zero rows charges the $0.02 start fee only. A BLOCKED run (source refused) fails loud and charges no value event. The start fee is charged once per GB of run memory; the default run memory is 512 MB.
Yield on the sample run: NORMAL: returned=10 scanned=100 server_total=1486 | establishment-result charged=10 (one per returned establishment). maxResults is a hard ceiling on what is delivered and billed, never a target.
⚖️ Legal & ToS
This actor reads public, logged-out pages and feeds published by api.ratings.food.gov.uk. It collects only what the source publishes to any visitor, keeps to the source's robots rules (checked on every run), identifies itself, and does not access accounts, private data or anything behind authentication. Use the output in line with the source's terms and your local law; the intended use is B2B research and monitoring.
❓ FAQ
Q: Do I need an API key or a login?
A: No. The source (api.ratings.food.gov.uk) is read logged-out; the input schema has no key field and the actor carries no secrets.
Q: Why did my run return 0 rows?
A: Read the run's status message. GENUINE_EMPTY means the source was read and had nothing in scope for your input; BLOCKED means the source refused and the run failed without billing a value event — retry later or narrow the input. A zero-row run bills the start fee only.
Q: How many rows can one run return?
A: Up to maxResults (default 100). Raise the cap for a bigger run; you pay per delivered row.
Q: How fresh is the data?
A: Every run reads the source live at run time; nothing is cached between runs. Put it on a schedule for a continuous feed.
Q: What formats can I export?
A: The dataset downloads as JSON, CSV, Excel, XML or RSS from the run's Dataset tab or the Apify API, and any run can push to a webhook or integration.
Q: How is this different from the other company registry checks actors?
A: Same output shape and billing model; this one covers api.ratings.food.gov.uk. The siblings under Related Actors cover the other sources or slices — run several on one schedule for a combined feed.
Q: Are there rate limits?
A: The actor paces itself against the source (RATE_PER_SEC=2.0, MIN_INTERVAL=1.0) and honours its robots rules; there is no per-buyer limit beyond your Apify plan's concurrency.
🆘 Troubleshooting
- Run FAILED with BLOCKED → the source refused the request or changed its page shape → nothing was billed beyond the start fee; retry after a while, and if it persists open an Issue with the run id.
- Status says CAPPED → your cap (
maxResults) was reached → raise it for a bigger run. - Input validation error on start → a field is outside the schema's allowed values → start from the Quick start block and change one field at a time.
- Run TIMED-OUT → a very wide request on a slow day → raise the run timeout in Run options or narrow the input; what was delivered before the timeout is still in the dataset.
🔗 Related Actors
- UK Charity Eligibility Lookup — Charity Commission register — Look up UK charities on the official Charity Commission register — one verdict per query with number, name and status. No API key; pay per answer
- UK Companies House Filing and Control Watch — Companies House filing history AND persons with significant control for the companies you name, in one cell: a new filing appearing, and a PSC being…
- UK Food Hygiene Establishment Check — Check UK establishments against the official FSA food-hygiene ratings API — one official answer per identifier (FHRSID or name): current hygiene rati…
- UK Gambling Operator Licence Check — GC Register — UK Gambling Operator Licence Check — Gambling Commission register
- UK Gazette Insolvency Notice Watch — Insolvency notices published in The Gazette — the UK's official public record: notice code, category, the subject named in the notice, publication ti…
- UK Ofsted Provider Inspection Check — Check UK education and childcare providers against the official Ofsted 'Find an inspection report' service — one official answer per URN: latest insp…
- 🏢 About NexGenData — NexGen Watch is NexGenData's fleet of 256 public monitoring and lookup actors built on official sources, pay-per-result. Browse the catalog at apify.com/nexgenwatch.
⭐ Found this useful?
If this actor saved you a manual check, a quick review on the Apify Store helps other teams find it. Feature request or a source that changed? Open it from the Issues tab — every one is read.
