# UK Food Business First-Seen Leads (`afkdigital/uk-food-business-first-seen-leads`) Actor

Track FHRS food businesses first observed after your prior complete scan. Filter by council, region, business type and rating. No opening-date claims or private contacts.

- **URL**: https://apify.com/afkdigital/uk-food-business-first-seen-leads.md
- **Developed by:** [AFK Digital](https://apify.com/afkdigital) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 83.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.50 / 1,000 first observed food businesses

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## API24 — UK Food Business First-Seen Leads

**Source:** Netcup `adf-netcup-01`, `/opt/APIFY/API24`. **Apify Actor:** `afkdigital/uk-food-business-first-seen-leads`. Full cloud validation and pricing are described below.

An independent Apify Actor that monitors the **Food Standards Agency Food Hygiene Rating Scheme (FHRS)** public register. Designed for food suppliers, POS providers, packaging distributors, and commercial-kitchen service firms looking for **businesses first observed in their own monitoring scope**.

> **Accuracy notice:** An FHRS entry first seen by this Actor is **not proof of a newly opened, newly incorporated, or newly registered business**. The FSA Search API does not expose a dependable per-establishment registration/publishing timestamp. `RatingDate` is the **hygiene inspection/rating date**; collection `meta.extractDate` is an **API data extraction time**, not a business publication date.

### Public source, licence, and access

- Official FSA API v2: https://api.ratings.food.gov.uk/help
- Official establishments search spec: https://api.ratings.food.gov.uk/Help/Api/GET-Establishments_name_address_longitude_latitude_maxDistanceLimit_businessTypeId_schemeTypeKey_ratingKey_ratingOperatorKey_localAuthorityId_countryId_sortOptionKey_pageNumber_pageSize
- API v2 requests include header `x-api-version: 2`, JSON `Accept`, and English `Accept-Language`.
- Official FAQ on throttling: https://api.ratings.food.gov.uk/Help/FAQs
- Dataset: https://data.food.gov.uk/catalog/datasets/38dd8d6a-5ab1-4f50-b753-ab33288e3200
- FSA open data and licence: https://www.food.gov.uk/our-data
- Open Government Licence v3.0: https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/
- FSA status notices (including official FHRSID reassignment warnings): https://api.ratings.food.gov.uk/Help/Status

FSA currently permits anonymous read access; there are no credentials to request or protected endpoints to bypass. FHRS is public-sector information under **OGL v3**; attribution is displayed on every result and in the run summary. Commercial use of the *public data* is allowed subject to licence conditions; that does not authorize using or exposing personal contact details, making unsupported opening-date claims, or ignoring relevant marketing/privacy law.

FSA throttles requests but publishes **no fixed request-per-second rate**. This Actor uses sequential calls, a default **1,250 ms minimum gap**, and short bounded retries on HTTP 403/429/5xx. Exhaustion fails safely rather than circumventing restrictions. The Actor does not scrape web pages, rotate proxies, or call unofficial endpoints.

### What counts as a lead?

1. **First full scan:** Enumerate every page of every selected local authority and apply all filters. The result is a free, portable **baseline**. There are **no paid leads** on this run.
2. **Subsequent full scans:** Compare FHRS IDs against the baseline and *all previously accepted observations*; produce only qualifying unseen IDs. Compare a conservative hashed business-name/address fingerprint to suppress likely FHRSID reassignments.
3. An observation is recorded and eligible for the custom charge **only once successfully emitted**. Excess candidates beyond `resultLimit` are retained as pending observations (with their original `firstObservedAt` timestamps).
4. Repeated existing FHRS IDs, filtered-out businesses, suspected ID reassignments, baseline records, API errors, and incomplete scans are never charged as new leads.
5. Source collection inconsistencies, mismatched page totals, repeated IDs across pages, safety-ceiling overruns, and source failures **stop the scan before any new leads are emitted**.

**Scope safety:** A new monitor must have `localAuthorityId`, exact `localAuthority` name, or exact FSA `region`. For existing snapshots, filters must be identical; changing a filter requires a fresh baseline. Excessively large scans fail rather than making a claim based on partial results.

### Inputs

| Field | Function |
|---|---|
| `localAuthorityId` / `localAuthority` / `region` | At least one required; names match the FSA authority catalogue case-insensitively |
| `businessTypeId` | Optional exact FSA business type ID, sent to FSA |
| `businessType` | Optional substring of official FSA business type |
| `keyword` | Optional substring of official *business name*, not emails/phones |
| `dateFrom`, `dateTo` | Optional inclusive **inspection date** (`RatingDate`) range |
| `ratingMin`, `ratingMax` | Numeric FHRS rating 0–5, inclusive; pending/non-numeric FHIS results excluded when bounded |
| `resultLimit` | Max new billable results per run (default 100) |
| `monitorId` | Stable unique logical monitor ID; enables persistence across separate Apify runs |
| `previousSnapshot` | Optional exported `SNAPSHOT` JSON to resume without a persistent monitor; takes precedence |
| `pageSize` | Requested FSA page size; actual maximum is server-capped |
| `maxPages` / `maxAuthorities` | Fail-safe completion ceilings (defaults 150 and 40) |
| `requestDelayMs` | Sequential FSA pacing, minimum 1,000 ms |

For a repeatable, automatic monitor use the **same** `monitorId` on later runs or a stable Apify Task (the task ID is used if no explicit `monitorId` is provided). A named KV store is created under a one-way hash of that ID combined with Apify's initiating `APIFY_USER_ID` when available, to avoid cross-user state collisions. Filters are additionally keyed with a SHA-256 scope hash.

When neither `monitorId` nor an Apify Task ID is present, use the prior run's **SNAPSHOT** value as `previousSnapshot` on the next run. Without a previous snapshot, the next run intentionally becomes a new baseline.

**Concurrency and anti-repeat safety:** A persistent per-user/per-monitor/per-scope Apify Request Queue provides an atomic unique-key claim for each new FHRSID, before the custom paid dataset event. If concurrent runs detect the same new FHRSID, only the first claimant attempts to deliver and charge it; subsequent attempts are suppressed, including attempts with stale snapshots. For best delivery reliability schedule non-overlapping tasks. A hard process crash after a successful claim but before delivery can cause an **unbilled skipped observation**: the safety model deliberately prefers not charging twice over claiming guaranteed delivery. Refer to RUN-SUMMARY claim fields and the release report.

### Output fields

The default Dataset contains two explicitly typed records: `recordType: "first-seen-business"` for a qualifying **paid lead**, or a free `recordType: "monitor-status"` receipt when a successful complete scan finds no billable new business. The latter carries `monitorId`, `status`, `checkedAt`, `authoritiesChecked`, `establishmentsScanned`, `filterMatches`, and `resultsEmitted: 0` and **does not claim a business opened or was newly registered**. Integrations that expect leads only should filter `recordType == "first-seen-business"`. Paid lead records contain these public/derived fields:

`fhrsId`, `businessName`, `addressLine1`–`addressLine4`, `postCode`, `localAuthority`, `region`, `businessType`, `businessTypeId`, `ratingValue`, `ratingNumeric`, `inspectionDate`, `sourceExtractAt`, `firstObservedAt`, `observationStatus`, `sourceUrl`, `matchReason`, `dataAttribution`.

The source URL points directly to the documented official API detail resource: `https://api.ratings.food.gov.uk/Establishments/{FHRSID}`. No inferred company registration date or publication/update date is fabricated. **Phone numbers, personal email addresses, contact details, and local-authority emails are explicitly excluded** from the output and snapshot.

Default KV records:

- `RUN-SUMMARY`: status, complete scan metrics, counts of newly observed/migrated/pending rows, charging and source information, or safe failure reason.
- `SNAPSHOT`: portable state (`format`, `scopeHash`, `seenIds`, **hashed** name/address identities, `pending` timestamps, coverage counts).

### Local installation, testing and repeat runs

Requires Node.js 22+.

```bash
cd /opt/APIFY/API24
npm ci --no-audit --no-fund
npm test
npm run smoke
mkdir -p storage/key_value_stores/default
cp examples/baseline.json storage/key_value_stores/default/INPUT.json
API24_DISABLE_CHARGING=1 npm start
API24_DISABLE_CHARGING=1 npm start
cat storage/key_value_stores/default/RUN-SUMMARY.json
```

The example monitors FSA local authority 321 (**Isles of Scilly**), filtering business type `Restaurant`. First run: `baseline-created`, one free monitor-status record and zero chargeable leads. Second unchanged run: `completed`, one free monitor-status record and zero chargeable leads. The test command `npm run smoke` uses genuine FHRS records and a **controlled omitted-record baseline** to demonstrate a genuine-ID first-seen diff without claiming the establishment really just opened.

For an Apify task, paste e.g.:

```json
{
  "localAuthorityId": 321,
  "monitorId": "my-scilly-food-leads",
  "businessType": "Restaurant",
  "resultLimit": 50,
  "maxPages": 10
}
```

For a different region, see `examples/region.json`. Exact region names are returned by the FSA Authorities endpoint; scanning too many pages/authorities will fail with a clear message and no partial billable results.

### Monetisation and release gating

**Price: $0.0025 USD per qualifying first-seen FHRS record ($2.50 per 1,000).** The Actor uses exactly one Apify pay-per-event event, `first-seen-business`, for a qualifying new observation delivered to the Dataset. The free first-run baseline, unchanged repeats, filtered results and source failures are not charged. Spend caps are checked before claiming a record. The run summary counts only the configured custom event using the ChargingManager, excluding any synthetic free dataset event.

**Verified Apify monetization:** The Actor has a `PAY_PER_EVENT` pricing configuration with only `first-seen-business` as the configured paid event at $0.0025. The private cloud baseline and identical-repeat runs each charged zero events; a controlled omission test based on a genuine published FHRS ID charged one event, and replaying the old snapshot charged zero. This is **not proof that the test business recently opened.** Consult the API24 release report for the latest build and concurrency checks. Avoid simultaneous scheduling for the same monitor to reduce missing-delivery risk following abrupt process interruptions.

Local tests disable monetisation with `API24_DISABLE_CHARGING=1`. This environment variable is **not** for a monetized production Actor run.

### Limitations and integrity

- FSA data may be delayed or corrected. Missing records may be withdrawn or moved, not necessarily closed. Newly observed records may be old businesses absent from an earlier extract.
- A change of FSA `FHRSID` can reflect a council system migration; the fingerprint check reduces, but cannot eliminate, reassignment false positives.
- Scotland FHIS ratings are not generally numeric 0–5; choose an unbounded numeric rating filter to include those categories.
- A bad/old snapshot, altered scope, partial pages, HTTP 403/429 or source schema drift causes a safe failure. No scraping or rate-limit evasions are used.
- **No automated collection of personal emails or phone numbers.** Users must comply with UK GDPR and electronic-marketing rules when contacting businesses via independently obtained, lawful channels.
- This is a read-only public registry monitor, not a company formation, restaurant permit, new-opening or commercial-contacts database.

### Files

`src/fhrs.js` — official v2 HTTP API, pagination, pacing, error handling

`src/monitor.js` — input validation, scope, FHRS matching, portable snapshots and lead selection

`src/main.js` — Apify lifecycle, named snapshot state, output and custom PPE charging

`.actor/` — Apify Actor definition, input/dataset/output schema

`test/` — mocked boundary/error tests and live FSA test script

`reports/` — local evidence; deliberately omitted from Docker/Actor uploads

### Store QA repair (2026-10-15)

A small official `localAuthorityId: 321` scope and stable demo `monitorId` are prefilled in the input form. Valid first scans and unchanged repeat scans return a free status receipt, allowing Apify automated QA to verify a non-empty dataset without pretending a lead exists or charging for one. The API still requires an explicit authority/region if called without the UI prefill.

# Actor input Schema

## `localAuthorityId` (type: `integer`):

Preferred exact local authority identifier, e.g. 321 Isles of Scilly.

## `localAuthority` (type: `string`):

Exact case-insensitive official local authority name. Alternative to numeric ID.

## `region` (type: `string`):

Exact official region, e.g. London, South West, Scotland. May cover many authorities.

## `businessTypeId` (type: `integer`):

Optional API business-type filter.

## `businessType` (type: `string`):

Optional case-insensitive substring of FSA BusinessType.

## `keyword` (type: `string`):

Optional case-insensitive keyword within the business name, NOT an email/phone search.

## `dateFrom` (type: `string`):

Optional RatingDate lower bound, NOT registration/opening date.

## `dateTo` (type: `string`):

Optional RatingDate upper bound, NOT registration/opening date.

## `ratingMin` (type: `integer`):

Optional inclusive 0–5 FHRS rating; pending, exempt and FHIS ratings are not numeric.

## `ratingMax` (type: `integer`):

Optional inclusive 0–5 FHRS rating.

## `resultLimit` (type: `integer`):

Other new leads are retained as pending in the snapshot; full scan is still required.

## `monitorId` (type: `string`):

Unique ID (3–64 letters, digits, \_ or -) to persist observations in an isolated named Apify KV store. Without this, use an Actor task or previousSnapshot.

## `previousSnapshot` (type: `string`):

Optional entire SNAPSHOT JSON returned by previous successful run with exactly the same matching filters. Overrides saved monitor state.

## `pageSize` (type: `integer`):

Controls FHRS scanning: pageSize

## `maxPages` (type: `integer`):

Safely fail incomplete scans; do not charge or update state.

## `maxAuthorities` (type: `integer`):

Controls FHRS scanning: maxAuthorities

## `requestDelayMs` (type: `integer`):

Single-threaded API requests. FSA does not publish a fixed throttle limit.

## Actor input object example

```json
{
  "localAuthorityId": 321,
  "resultLimit": 100,
  "monitorId": "fhrs-qa-demo-scilly",
  "pageSize": 100,
  "maxPages": 150,
  "maxAuthorities": 40,
  "requestDelayMs": 1250
}
```

# Actor output Schema

## `leads` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `snapshot` (type: `string`):

No description

# 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 = {
    "localAuthorityId": 321,
    "monitorId": "fhrs-qa-demo-scilly"
};

// Run the Actor and wait for it to finish
const run = await client.actor("afkdigital/uk-food-business-first-seen-leads").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 = {
    "localAuthorityId": 321,
    "monitorId": "fhrs-qa-demo-scilly",
}

# Run the Actor and wait for it to finish
run = client.actor("afkdigital/uk-food-business-first-seen-leads").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 '{
  "localAuthorityId": 321,
  "monitorId": "fhrs-qa-demo-scilly"
}' |
apify call afkdigital/uk-food-business-first-seen-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,afkdigital/uk-food-business-first-seen-leads"
        }
    }
}
```

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/OTeUhqq4xCdJidVBo/builds/Bwf6t2xpJ637ANZN9/openapi.json
