# UK HSE Enforcement Monitor - Prosecutions & Notices Scraper (`stefano_seggio/uk-hse-enforcement-monitor`) Actor

Scrape and monitor the UK Health and Safety Executive registers of convictions (prosecutions, fines, breaches, courts) and enforcement notices (Improvement, Prohibition). Filter by company name, region, industry, fine, notice type, dates. Delta mode with new and updated events. Pay per record.

- **URL**: https://apify.com/stefano\_seggio/uk-hse-enforcement-monitor.md
- **Developed by:** [Stefano Seggio](https://apify.com/stefano_seggio) (community)
- **Categories:** Business, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 record (full detail)s

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## UK HSE Enforcement Monitor — Prosecutions & Notices Scraper

**Store URL:** https://apify.com/stefano\_seggio/uk-hse-enforcement-monitor
**Actor ID:** `jV35qppM82fjyjsle`
**Version:** 2.0 (latest changelog entry: 2.0.1, 2026-09-08)

***

### Executive Summary & Business Use Case

UK HSE Enforcement Monitor extracts structured records from the two public enforcement registers published by the UK Health and Safety Executive at **resources.hse.gov.uk**: the **register of convictions** (every successful HSE prosecution — defendant, offence, fine, costs, breaches, court, hearing date — which HSE keeps for five years, roughly 210 live records) and the **register of enforcement notices** (Improvement and Prohibition notices — recipient, notice type, served and compliance dates, result, legislation breached — which HSE keeps for ten years, 30,000+ records). Neither register has an official API; the HSE site is a 2013-era classic-ASP search wizard with ten rows per page, no CSV export for notices, no "last updated" field, and a default sort that hides late-published cases. This actor is a maintained, pay-per-record API layer over both registers, with a delta engine that can put either register — or a filtered slice of it — under continuous monitoring.

The data supports several concrete commercial use cases, all grounded in fields that actually exist in the output. **Contractor pre-qualification and supply-chain risk teams** (CHAS/SSIP/Constructionline-style buyers, principal contractors, procurement teams) use `nameContains`, `event_type`, `noticeCategory`, `isImmediate` and `partyHseReference` to check whether a subcontractor has picked up a notice or conviction since they were approved, and `isRepeatOffender` plus `partyConvictionCount`/`partyNoticeCount` to flag repeat offenders at onboarding or renewal. **Employers'-liability and public-liability insurance underwriters, MGAs and brokers** use `totalFineGbp`, `resultingFromFatality`, `hasCustodialSentence` and the party-level conviction/notice counts to price, load, survey or decline a risk, and to catch anything new before a renewal date. **KYB, adverse-media and ESG data vendors**, and **H\&S consultancies doing business development**, pull the whole register keyed on `partyStatus`/`partyAddress`/`partyPostcode` plus the daily delta (via `contentHash` and `firstSeenAt`) to attach a "UK H\&S enforcement" attribute to company profiles or to prospect companies freshly served an Improvement Notice with a near-term `effectiveComplianceDateIso`.

Because every filter in the input schema is applied server-side by the HSE register itself, narrow runs (a single company name, a single region/industry combination, a fine floor) touch only a handful of pages and cost accordingly — the actor is priced and designed for exactly this kind of targeted, recurring lookup rather than only bulk extraction.

### Technical Features & V2 Architecture Highlights

**Delta engine with named, cross-run key-value-store persistence.** When `onlyNew` is enabled, the actor writes its memory of every previously delivered record — one content hash per record, keyed by register — into a private, named key-value store called `uk-hse-enforcement-monitor-state-<deltaStateName>` (the name defaults to a fingerprint of the active filter set, so unrelated schedules never collide unless `deltaStateName` is set to the same value on purpose). Later runs read that store back, walk the register in **entry order** (case/notice number descending) rather than the site's own offence/issue-date order — which the README documents as necessary because HSE enters convictions months after the hearing and notices weeks after service, so date-sorted listings hide late-published records — and deliver only records that are new or whose page's content hash changed. Convictions (~210 records) are walked in full every run; the notices walk stops once it hits two consecutive already-known pages, so a routine scheduled run only touches a few pages.

**Real event types: `SANCTION`, `NEW_LISTING`, `UPDATED` — no `CLOSED`.** Per `dataset_schema.json` and `input_schema.json`, this actor's `event_type` enum is domain-specific and asymmetric by design, not the generic new/updated/closed set some other actors in this developer's fleet use: `SANCTION` marks a conviction never delivered before, `NEW_LISTING` marks a notice never delivered before, and `UPDATED` marks a previously delivered record whose case/notice page has since changed (a notice complied with, a revised compliance date, a breach or hearing added). There is deliberately no `CLOSED` event — the HSE registers have no concept of a record being withdrawn or closed in a way the site exposes cleanly (a withdrawn record just starts returning an HTTP 500 "unknown id" page, handled separately — see below), so the engine does not manufacture a closure event for it.

**`onlyNew` behaviour, verbatim from the actor's own schema.** `input_schema.json` documents `onlyNew` as: *"Remembers every record it has delivered (per filter set, in a named key-value store) and returns only records that are new or whose page changed since. Convictions (~210 records) are always walked in full; notices stop at the first two pages of already-known records, so a scheduled run costs a few pages."* In plain terms: with `onlyNew: true`, a record the register hasn't changed since the last run is never emitted into the dataset at all — it is not delivered, not counted, and (per the pricing model below) not billed. Turning `onlyNew` off after having used it does not fork the delta memory — filter values, not the flag itself, determine which named state a run reads and writes.

**No evidence of a generic 18-field "UMS" in this actor's own schema.** What the actor's own files do confirm is a smaller, explicitly-named **6-field integration envelope** — `record_id`, `event_type`, `scraped_at`, `is_new`, `source_url`, `data_source` — which the README states in as many words is *"identical across all of this developer's public-register Actors, so one webhook parser serves them all."* Everything past those six fields is domain-specific to the HSE registers (conviction fields, notice fields, breach arrays, location fields, party fields, provenance fields), documented field-by-field in `dataset_schema.json`.

**Content-hash change detection with a baseline floor and a walk watermark (added/fixed in 2.0.0/2.0.1).** Because the HSE registers have no "last updated" timestamp, the delta engine hashes each fetched page (`contentHash`) as its sole change-detection key. `CHANGELOG.md` documents two architectural safeguards added specifically to keep a capped, scheduled run honest over time: (1) a **baseline floor** — on a cold delta memory's first run, if `maxItemsPerDataset` cuts the walk short, the oldest record actually delivered is persisted as that register's baseline *before* anything is delivered, and every later delta run treats unseen records below that floor as history — never delivered, never charged, never a reason to keep walking; and (2) a **walk watermark** for every later capped run — the record number a capped walk stopped at is persisted so the next run resumes the backlog from that point instead of prematurely treating "two known pages" as the end of new material. The 2.0.1 patch specifically fixed a case where a small cap (below ~20, i.e. under two full listing pages) broke the baseline promise and caused older records to be silently re-delivered and re-charged run after run.

**Recheck window for `UPDATED` detection.** `recheckDays` (default 180, 0–3650) controls how long known **open** records — Improvement Notices with `result` = "Ongoing", and every conviction — keep being re-read after first delivery (or after their register date, whichever is later) to catch an `UPDATED` event; up to 2,000 records are re-checked per run. Prohibition notices carry no `result` value on the register at all and are never re-checked.

**Three independent detail-fetch toggles gate both data completeness and price tier.** `fetchDetail` opens the case/notice page itself (description, dates, fine, costs, result, address, industry, HSE admin fields, plus the per-case breach list for convictions); `fetchBreachDetail` opens each breach's own page (court, Act section/Regulation paragraph, per-breach fine, hearing date) for convictions, or the breach list for notices; `fetchPartyDetail` opens the defendant/recipient's own page and cross-register history (three extra requests, no extra charge). Turning `fetchDetail` or `fetchBreachDetail` off drops a record to the cheaper `result-summary` price tier (see Pricing below) and also disables `UPDATED` detection for that record, since there is no case/notice page to hash.

**Five ready-made dataset views** ship in `dataset_schema.json`: **Overview** (both registers side by side), **Convictions & fines**, **Enforcement notices**, **Compliance tracker** (ongoing/overdue/complied notices with `daysUntilCompliance`), and **Defendants & recipients** (party-level roll-up with repeat-offender counts) — usable directly from the Output tab or via the dataset API's `view` parameter.

### Input Schema & JSON Configuration Example

All descriptions below are copied verbatim from `.actor/input_schema.json`.

| Field | Type | Default | Description |
|---|---|---|---|
| `datasets` | array (enum: `convictions`, `notices`) | `["convictions", "notices"]` | Convictions = successful prosecutions (defendant, fine, costs, breaches, court, hearing date; HSE keeps 5 years). Notices = Improvement and Prohibition enforcement notices (recipient, type, served/compliance dates, result; HSE keeps 10 years). |
| `nameContains` | string | — | Substring match on the defendant's (convictions) or recipient's (notices) name, e.g. 'Skanska' or 'Balfour Beatty'. The contractor-vetting filter. Quotes are treated as single-character wildcards. |
| `descriptionContains` | string | — | Full-text substring over the case description (convictions) or notice summary (notices), e.g. 'asbestos', 'scaffold', 'silica'. |
| `localAuthorityContains` | string | — | Local authority where the offence occurred, e.g. 'Bradford', 'Cardiff UA'. |
| `mainActivityContains` | string | — | A numeric value matches the SIC 2007 code of the offence location; any other text matches the activity description ('ROOFING', 'MACHINING'). The site keeps the two in different columns, so a code never matches as text and vice versa. The code match is a CONTAINS match on the register: use a full 5-digit code for one class ('43910' = roofing) or a 4-digit group ('4391'); 2-3 digit values also match every code that merely contains those digits ('43' returns 14310, 24310, 46430 ...) - use the industry filter for broad selections. |
| `region` | string (select, enum `""`,`1`–`7`) | `""` | HSE's seven UK regions. (`1` Wales & South West, `2` East & South East, `3` North West, `4` Yorks & North East, `5` Midlands, `6` London, `7` Scotland) |
| `country` | string (select, enum `""`,`8`–`11`) | `""` | Country of the offence location. (`8` England, `9` Scotland, `10` Wales, `11` Jersey) |
| `industry` | string (select, enum `""`,`12`–`16`) | `""` | HSE's five industry groups. (`12` Agriculture/hunting/forestry/fishing, `13` Construction, `14` Extractive & utility supply, `15` Manufacturing, `16` Total service industries) |
| `hseDivision` | string (select, enum `""`,`17`–`27`) | `""` | HSE's operational division (finer than region). |
| `dateFrom` | string (datepicker, absolute or relative) | — | Offence date (convictions) / date the notice was served (notices). Absolute (2026-01-01) or relative ('30 days', '6 months', '1 year' back from today, UK calendar). Note: both dates lag publication on the register by weeks to years - for 'what is new on the register' use delta mode instead. |
| `dateTo` | string (datepicker, absolute or relative) | — | Absolute or relative upper bound of the same date. Leave empty for 'today'. |
| `hseReference` | string (pattern `^\s*\d+\s*$`) | — | Everything about one party: the numeric HSE Reference shown on a defendant or recipient page (the same id is used on both registers), e.g. 4392330. |
| `recordNumber` | string (pattern `^\s*\d+\s*$`) | — | Look up one record by its case number (convictions) or notice number (notices), e.g. 4883993 or 316005113. |
| `defendantStatus` | string (select, enum `""`,`1980`,`1972`,`1981`,`1974`,`1976`,`1978`,`1977`,`1973`,`1975`,`1979`) | `""` | Legal form of the defendant. Convictions only. |
| `resultingFromFatality` | string (enum `any`,`yes`,`no`) | `"any"` | Only cases that resulted from the investigation of a (RIDDOR-reportable) fatality. Convictions only. |
| `minTotalFineGbp` | integer (min 0) | — | Only cases with a total fine of at least this amount. Tip: 100000 leaves about 50 of the 210 cases. |
| `maxTotalFineGbp` | integer (min 0) | — | Only cases with a total fine of at most this amount. |
| `noticeTypes` | array (enum `01`–`09`) | `[]` | HSE's nine notice-type codes (labels verified against the register, 2026-09-07). '03' is the ordinary Improvement Notice (22,000+); '01' and '02' are the rare Crown / FEPA variants. Select several to get their union. Notices only. |
| `act` | string (select, 43-code enum) | `""` | Primary legislation of the breach. Notices only (labels are the register's own, some truncated by the site). |
| `eventTypes` | array (enum `SANCTION`,`NEW_LISTING`,`UPDATED`) | `["SANCTION","NEW_LISTING","UPDATED"]` | SANCTION = a conviction not delivered before; NEW\_LISTING = a notice not delivered before; UPDATED = a record you already received whose case/notice page changed (a notice complied with, a revised compliance date, a breach or hearing added) - delta mode only. Applied by the delta engine, not by the site: changing it later does not fork the delta memory. |
| `onlyNew` | boolean | `false` | Remembers every record it has delivered (per filter set, in a named key-value store) and returns only records that are new or whose page changed since. Convictions (~210 records) are always walked in full; notices stop at the first two pages of already-known records, so a scheduled run costs a few pages. |
| `recheckDays` | integer (0–3650) | `180` | In delta mode, known records that are still open (Improvement Notices with Result 'Ongoing', and every conviction) are re-read for this many days after they were first delivered (or after their register date, whichever is later) to detect UPDATED events - the register has no 'last updated' field, so this is the only way to see a notice being complied with or a hearing being added. 0 disables re-checks. Up to 2,000 records are re-checked per run. |
| `deltaStateName` | string (pattern `^[A-Za-z0-9-]{1,30}$`) | fingerprint of filters | Optional label for the memory of a monitoring task. Defaults to a fingerprint of your filters, so different schedules never interfere. Set the same name on two tasks to make them share one memory. |
| `resetState` | boolean | `false` | Forget every previously delivered record for this delta state and re-baseline (the run then returns up to 'Max records per register' most recently entered records). |
| `maxItemsPerDataset` | integer (1–100000) | `100` | Hard cap on delivered records per selected register (and therefore on cost: you pay per record). On the very first delta run the cap defines the baseline: the oldest record that run delivers is remembered per register, and records entered before it are history - later delta runs never deliver or charge for them, whatever the cap (run once with onlyNew=false, or set a high cap on the first run, if you want the history). On every later delta run the overflow is a backlog: the walk remembers where it stopped (a per-register watermark in the delta memory) and the next run walks down to that point instead of stopping at the records already delivered. |
| `fetchDetail` | boolean | `true` | Opens each record's own page for the description, dates, fine and costs, result, address, industry and HSE admin fields (plus, for convictions, the per-case breach list). Off = listing-row-only records (name, date, local authority, main activity, notice type) at the cheaper 'result-summary' price; no UPDATED detection is possible without it. |
| `fetchBreachDetail` | boolean | `true` | Convictions: opens each breach's page for the court, Act section / Regulation paragraph, hearing date, result and per-breach fine. Notices: fetches the breach list (Act / regulation / paragraph per breach). Off = the cheaper 'result-summary' price. |
| `fetchPartyDetail` | boolean | `true` | Opens the party's page (registered address, Status such as 'Private Company' or 'Individual', HSE Reference) and its case and notice lists on both registers - the repeat-offender signal (3 extra requests per record, no extra charge). |
| `maxConcurrency` | integer (1–10) | `5` | Run-wide cap on simultaneous HTTP requests (listing, detail, breach and party pages together - never more than this in flight). The register is a classic-ASP site that answered 17 concurrent requests without throttling; 5 keeps a 100-record run around a minute. |
| `dateRange` | string (hidden, pattern `^(24h\|7d\|30d)$`) | — | Kept for tasks created with version 1: '24h', '7d' or '30d' are interpreted as 'Date from'. |

#### Valid input example

```json
{
    "datasets": ["notices"],
    "nameContains": "Skanska",
    "noticeTypes": ["08"],
    "industry": "13",
    "region": "3",
    "dateFrom": "90 days",
    "eventTypes": ["NEW_LISTING", "UPDATED"],
    "onlyNew": true,
    "recheckDays": 180,
    "deltaStateName": "skanska-nw-construction",
    "maxItemsPerDataset": 500,
    "fetchDetail": true,
    "fetchBreachDetail": true,
    "fetchPartyDetail": true,
    "maxConcurrency": 5
}
```

This asks for Immediate Prohibition Notices (`noticeTypes: ["08"]`) served on or after 90 days ago, in Construction (`industry: "13"`), North West England (`region: "3"`), where the recipient name contains "Skanska", delivering new and updated records only (`onlyNew: true`, `eventTypes` excludes `SANCTION` since no `convictions` dataset was requested), remembered under the delta state `skanska-nw-construction`, capped at 500 records, with full case, breach and party detail fetched.

### Output Dataset Sample & Data Dictionary

Fields, types and descriptions below come from `.actor/dataset_schema.json`. `additionalProperties: true` in that schema means individual records may occasionally carry a couple of extra provenance-style fields not enumerated here, but every field named below is declared explicitly in the schema.

**Integration envelope** (shared shape across this developer's public-register actors):

| Field | Type | Description |
|---|---|---|
| `record_id` | string | `caseNumber` or `noticeNumber` - stable across runs. |
| `event_type` | string (`SANCTION`|`NEW_LISTING`|`UPDATED`) | SANCTION (conviction not delivered before), NEW\_LISTING (notice not delivered before) or UPDATED (a previously delivered record whose case/notice page changed). |
| `scraped_at` | string | ISO-8601 UTC timestamp of this extraction. |
| `is_new` | boolean | true when this record was never delivered by a previous run of this delta state. |
| `source_url` | string | The official HSE register page. |
| `data_source` | string | Open Government Licence v3.0 attribution string. |

**Record identity and classification:**

| Field | Type | Description |
|---|---|---|
| `recordType` | string (`conviction`|`notice`) | Which register the record came from. |
| `caseNumber` | string | null | Convictions only. |
| `noticeNumber` | string | null | Notices only. |
| `defendantName` | string | null | Convictions only. |
| `defendantId` | string | null | Defendant ID (HSE reference). |
| `recipientName` | string | null | Notices only. |
| `recipientId` | string | null | Recipient ID (HSE reference). |
| `description` | string | null | Case or notice description. |

**Conviction fields:**

| Field | Type | Description |
|---|---|---|
| `offenceDate` | string | null | Offence date, raw site string (example `27/07/2021`). |
| `offenceDateIso` | string | null | Offence date, `YYYY-MM-DD`. |
| `hearingDate` | string | null | Latest hearing date, raw site string. |
| `hearingDateIso` | string | null | Newest Date of Hearing across the case's breaches - the closest thing to a publication date. |
| `totalFine` | string | null | Total fine, raw site string (example `£400,000.00`). |
| `totalFineGbp` | number | null | Total fine, GBP. |
| `totalCosts` | string | null | Costs awarded to HSE, raw site string. |
| `totalCostsGbp` | number | null | Costs awarded to HSE, GBP. |
| `totalPenaltyGbp` | number | null | Fine + costs, GBP. |
| `resultingFromFatality` | boolean | null | true when the case page states it resulted from the investigation of a fatality. |
| `hasCustodialSentence` | boolean | null | true when any breach result is a prison sentence (including suspended). |
| `breachCount` | integer | null | Number of breaches. |
| `legislationBreached` | array\<string> | null | Legislation breached. |
| `courtLevel` | string | null | Court level (example `Magistrates Court`). |
| `breaches` | array\<object> | Convictions: `breachId`, `court`, `courtLevel`, `act`, `regulation`, `dateOfHearing(Iso)`, `result`, `resultListing`, `resultCategory`, `isCustodial`, `fine`, `fineGbp`, `actName`/`actSection`/`actSubSection`, `regulationName`/`regulationNumber`/`regulationParagraph`, `legislation`/`provision`/`paragraph`, `source_url`. Notices: `breachId`, `actOrRegulation`, `legislation`, `provision`, `paragraph`. |

**Notice fields:**

| Field | Type | Description |
|---|---|---|
| `noticeType` | string | null | Example `Immediate Prohibition Notice`. |
| `noticeTypeListing` | string | null | Notice type, listing wording. |
| `noticeCategory` | string | null (`Improvement`|`Prohibition`|null) | Notice category. |
| `isProhibition` | boolean | null | Is prohibition notice. |
| `isImprovement` | boolean | null | Is improvement notice. |
| `isImmediate` | boolean | null | Is immediate prohibition. |
| `isDeferred` | boolean | null | Is deferred prohibition. |
| `isCrown` | boolean | null | Crown notice. |
| `isComah` | boolean | null | COMAH notice. |
| `isFepa` | boolean | null | FEPA notice. |
| `servedDate` | string | null | Served on, raw site string. |
| `servedDateIso` | string | null | Served on, ISO date. |
| `complianceDate` | string | null | From the notice page, or from the listing when an Improvement notice type is selected (8-column listing). |
| `complianceDateIso` | string | null | Compliance date, ISO. |
| `revisedComplianceDate` | string | null | Revised compliance date, raw site string. |
| `revisedComplianceDateIso` | string | null | Revised compliance date, ISO. |
| `effectiveComplianceDateIso` | string | null | Revised date when one exists, else the original. |
| `daysToComply` | integer | null | Days given to comply. |
| `daysUntilCompliance` | integer | null | Negative once the effective compliance date has passed. |
| `hasRevisedComplianceDate` | boolean | null | Has revised compliance date. |
| `result` | string | null | Example `Ongoing`. Ongoing / Complied with (Improvement Notices only). From the notice page, or from the listing when an Improvement notice type is selected. |
| `isOngoing` | boolean | null | Is ongoing. |
| `isCompliedWith` | boolean | null | Complied with. |
| `isOverdue` | boolean | null | Ongoing and past its effective compliance date. |
| `descriptionItemIds` | array\<string> | null | Per-item notice numbers embedded in multi-item descriptions. |

**Location of offence:**

| Field | Type | Description |
|---|---|---|
| `address` | string | null | Offence address. |
| `postcode` | string | null | Postcode. |
| `country` | string | null | Country. |
| `region` | string | null | UK region. |
| `localAuthority` | string | null | Local authority. |
| `industry` | string | null | Industry. |
| `mainActivity` | string | null | Raw site string (example `43910 - ROOFING ACTIVITIES`). |
| `sicCode` | string | null | SIC code. |
| `sicDescription` | string | null | SIC description. |
| `typeOfLocation` | string | null | Type of location. |
| `hseGroup` | string | null | HSE group. |
| `hseDirectorate` | string | null | HSE directorate. |
| `hseArea` | string | null | Always empty on the live register; kept for compatibility. |
| `hseDivision` | string | null | HSE division. |

**Party (`fetchPartyDetail`):**

| Field | Type | Description |
|---|---|---|
| `partyStatus` | string | null | Example `Private Company`. |
| `partyEntityType` | string | null (`company`|`individual`|`partnership`|`public_body`|`other`|null) | Party entity type. |
| `partyAddress` | string | null | Party registered address. |
| `partyPostcode` | string | null | Party postcode. |
| `partyHseReference` | string | null | Party HSE reference. |
| `partyUrl` | string | null | Party page. |
| `partyConvictionCount` | integer | null | Party convictions (all time on register). |
| `partyNoticeCount` | integer | null | Party notices (all time on register). |
| `partyOtherCaseNumbers` | array\<string> | null | Party's other case numbers. |
| `partyOtherNoticeNumbers` | array\<string> | null | Party's other notice numbers. |
| `isRepeatOffender` | boolean | null | true when the party has more than one conviction or notice on the registers. |

**Provenance:**

| Field | Type | Description |
|---|---|---|
| `detailFetched` | boolean | Detail fetched. |
| `detailError` | string | null | `NOT_FOUND` when the page was withdrawn between listing and detail. |
| `breachDetailFetched` | boolean | Breach detail fetched. |
| `partyDetailFetched` | boolean | Party detail fetched. |
| `partyDetailError` | string | null | Party detail error. |
| `contentHash` | string | null | The delta engine's change key for this record. |
| `firstSeenAt` | string | null | `YYYY-MM-DD`, from the delta memory. |

#### Sample dataset record

This is a real notice record produced by the actor (per the actor's own README):

```json
{
    "record_id": "314719061",
    "event_type": "NEW_LISTING",
    "scraped_at": "2026-09-07T21:10:39.804Z",
    "is_new": true,
    "source_url": "https://resources.hse.gov.uk/notices/notices/notice_details.asp?SF=CN&SV=314719061",
    "data_source": "Contains public sector information published by the Health and Safety Executive and licensed under the Open Government Licence v3.0 (HSE public registers of convictions and enforcement notices, resources.hse.gov.uk)",
    "recordType": "notice",
    "noticeNumber": "314719061",
    "recipientName": "Llanelec Precision Engineering Company Limited",
    "recipientId": "1108773",
    "noticeType": "Improvement Notice",
    "noticeCategory": "Improvement",
    "isProhibition": false,
    "isImmediate": false,
    "servedDate": "05/12/2024",
    "servedDateIso": "2024-12-05",
    "complianceDateIso": "2025-03-03",
    "revisedComplianceDateIso": "2025-03-31",
    "effectiveComplianceDateIso": "2025-03-31",
    "daysToComply": 88,
    "daysUntilCompliance": -525,
    "hasRevisedComplianceDate": true,
    "result": "Complied with",
    "isOngoing": false,
    "isCompliedWith": true,
    "isOverdue": false,
    "breachCount": 2,
    "legislationBreached": [
        "Health and Safety At Work Act 1974",
        "Management of Health & Safety at Work Regulations 1999"
    ],
    "breaches": [
        {
            "breachId": "001",
            "actOrRegulation": "Health and Safety At Work Act 1974 / 2 / 1",
            "legislation": "Health and Safety At Work Act 1974",
            "provision": "2",
            "paragraph": "1"
        },
        {
            "breachId": "002",
            "actOrRegulation": "Management of Health & Safety at Work Regulations 1999 / 3 / 1",
            "legislation": "Management of Health & Safety at Work Regulations 1999",
            "provision": "3",
            "paragraph": "1"
        }
    ],
    "address": "Llanelec Prec. Eng. Co. Ltd./L, Llanelec Precision Engineering, Nidum House, Neath Abbey Business Park, NEATH, West Glamorgan, SA10 7DR, Wales",
    "postcode": "SA10 7DR",
    "country": "Wales",
    "region": "Wales & South West",
    "localAuthority": "Neath & Port Talbot UA",
    "industry": "Manufacturing",
    "mainActivity": "25620 - MACHINING",
    "sicCode": "25620",
    "sicDescription": "MACHINING",
    "hseDivision": "Wales",
    "partyStatus": "Private Company",
    "partyEntityType": "company",
    "partyAddress": "Nidum House, Neath Abbey Business Park, NEATH, West Glamorgan, SA10 7DR",
    "partyPostcode": "SA10 7DR",
    "partyHseReference": "1108773",
    "partyConvictionCount": 0,
    "partyNoticeCount": 22,
    "isRepeatOffender": true,
    "detailFetched": true,
    "breachDetailFetched": true,
    "partyDetailFetched": true,
    "contentHash": "e20670777ba0369e",
    "firstSeenAt": "2026-09-07"
}
```

A conviction record has the same envelope plus `"recordType": "conviction"`, `"event_type": "SANCTION"`, `caseNumber`, `defendantName`, `offenceDateIso`, `hearingDateIso`, `totalFineGbp`, `totalCostsGbp`, `totalPenaltyGbp`, `resultingFromFatality`, `hasCustodialSentence`, `courtLevel`, and a `breaches[]` array carrying `court`, `courtLevel`, `dateOfHearingIso`, `result`, `resultCategory`, `isCustodial`, `fineGbp`, `actName`/`actSection`/`actSubSection`, `regulationName`/`regulationNumber`/`regulationParagraph` per breach.

### Multi-language Integration Snippets

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/stefano_seggio~uk-hse-enforcement-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "datasets": ["notices"],
    "noticeTypes": ["08"],
    "industry": "13",
    "region": "3",
    "onlyNew": true,
    "maxItemsPerDataset": 500
  }'
```

#### Python (apify-client)

```python
from apify_client import ApifyClient

client = ApifyClient(token="YOUR_TOKEN")

run = client.actor("stefano_seggio/uk-hse-enforcement-monitor").call(
    run_input={
        "datasets": ["notices"],
        "noticeTypes": ["08"],
        "industry": "13",
        "region": "3",
        "onlyNew": True,
        "maxItemsPerDataset": 500,
    }
)

for record in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(
        record["event_type"],
        record["recordType"],
        record.get("recipientName") or record.get("defendantName"),
        record.get("noticeType"),
        record["source_url"],
    )
```

#### Node.js (apify-client)

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('stefano_seggio/uk-hse-enforcement-monitor').call({
    datasets: ['notices'],
    noticeTypes: ['08'],
    industry: '13',
    region: '3',
    onlyNew: true,
    maxItemsPerDataset: 500,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems({ desc: true });
for (const record of items) {
    console.log(
        record.event_type,
        record.recordType,
        record.recipientName ?? record.defendantName,
        record.noticeType,
        record.source_url,
    );
}
```

### Pricing Model Explanation

The actor is billed per pay-per-event pricing with three named events (confirmed live via the Apify API):

| Event | Price | When it applies to this actor |
|---|---|---|
| `result` | **$0.003** per record | A record delivered with the case/notice page fetched **and** breach detail fetched — i.e. `fetchDetail: true` and `fetchBreachDetail: true` for that record, giving court, Act section, and regulation paragraph alongside the full case/notice fields. |
| `result-summary` | **$0.001** per record | Any lighter record: `fetchDetail: false` (listing-row-only — name, date, local authority, main activity, notice type), `fetchBreachDetail: false` (case/notice page fetched but no breach page), or a record whose detail page could not be fetched at all (delivered as a listing-only stub, e.g. `detailError: "NOT_FOUND"`). |
| Actor start (`apify-actor-start`) | **$0.00005** | Once per run, regardless of how many records — including a delta run that finds nothing new. |

The two-tier `result`/`result-summary` split in this actor is specifically about **how much of the record was fetched**, not about "new vs. old" or any other generic distinction: it is driven directly by the `fetchDetail` and `fetchBreachDetail` input flags. A run with both flags on pays full price per record for the case/notice page plus the per-breach legislation detail; turning either off (or hitting a page that can't be read) drops that record to the summary price. `fetchPartyDetail`, by contrast, adds three extra HTTP requests per record (the party's own page and its case/notice history) at **no extra charge** — it does not change which pricing tier a record falls into.

Because this actor supports delta monitoring (`onlyNew: true`), enabling it changes what gets billed, not the price of what gets billed: a record the HSE register hasn't added or changed since the last run is never delivered into the dataset at all, so it is never a `result` or `result-summary` event — it simply doesn't exist as a chargeable event that run, rather than being billed at $0. Per the README's own worked example, a quiet daily monitoring run that finds nothing new costs only the $0.00005 actor-start fee. A daily monitor of both registers that turns up 20 new notices and one conviction (all with full detail) costs about $0.06 for that run; a one-off full pull of the ~210-case convictions register costs about $0.63; the entire 30,000-record notices register with full detail is about $91, or about $30 if pulled as listing rows only (`fetchDetail: false`).

# Actor input Schema

## `datasets` (type: `array`):

Convictions = successful prosecutions (defendant, fine, costs, breaches, court, hearing date; HSE keeps 5 years). Notices = Improvement and Prohibition enforcement notices (recipient, type, served/compliance dates, result; HSE keeps 10 years).

## `nameContains` (type: `string`):

Substring match on the defendant's (convictions) or recipient's (notices) name, e.g. 'Skanska' or 'Balfour Beatty'. The contractor-vetting filter. Quotes are treated as single-character wildcards.

## `descriptionContains` (type: `string`):

Full-text substring over the case description (convictions) or notice summary (notices), e.g. 'asbestos', 'scaffold', 'silica'.

## `localAuthorityContains` (type: `string`):

Local authority where the offence occurred, e.g. 'Bradford', 'Cardiff UA'.

## `mainActivityContains` (type: `string`):

A numeric value matches the SIC 2007 code of the offence location; any other text matches the activity description ('ROOFING', 'MACHINING'). The site keeps the two in different columns, so a code never matches as text and vice versa. The code match is a CONTAINS match on the register: use a full 5-digit code for one class ('43910' = roofing) or a 4-digit group ('4391'); 2-3 digit values also match every code that merely contains those digits ('43' returns 14310, 24310, 46430 ...) - use the industry filter for broad selections.

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

HSE's seven UK regions.

## `country` (type: `string`):

Country of the offence location.

## `industry` (type: `string`):

HSE's five industry groups.

## `hseDivision` (type: `string`):

HSE's operational division (finer than region).

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

Offence date (convictions) / date the notice was served (notices). Absolute (2026-01-01) or relative ('30 days', '6 months', '1 year' back from today, UK calendar). Note: both dates lag publication on the register by weeks to years - for 'what is new on the register' use delta mode instead.

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

Absolute or relative upper bound of the same date. Leave empty for 'today'.

## `hseReference` (type: `string`):

Everything about one party: the numeric HSE Reference shown on a defendant or recipient page (the same id is used on both registers), e.g. 4392330.

## `recordNumber` (type: `string`):

Look up one record by its case number (convictions) or notice number (notices), e.g. 4883993 or 316005113.

## `defendantStatus` (type: `string`):

Legal form of the defendant. Convictions only.

## `resultingFromFatality` (type: `string`):

Only cases that resulted from the investigation of a (RIDDOR-reportable) fatality. Convictions only.

## `minTotalFineGbp` (type: `integer`):

Only cases with a total fine of at least this amount. Tip: 100000 leaves about 50 of the 210 cases.

## `maxTotalFineGbp` (type: `integer`):

Only cases with a total fine of at most this amount.

## `noticeTypes` (type: `array`):

HSE's nine notice-type codes (labels verified against the register, 2026-09-07). '03' is the ordinary Improvement Notice (22,000+); '01' and '02' are the rare Crown / FEPA variants. Select several to get their union. Notices only.

## `act` (type: `string`):

Primary legislation of the breach. Notices only (labels are the register's own, some truncated by the site).

## `eventTypes` (type: `array`):

SANCTION = a conviction not delivered before; NEW\_LISTING = a notice not delivered before; UPDATED = a record you already received whose case/notice page changed (a notice complied with, a revised compliance date, a breach or hearing added) - delta mode only. Applied by the delta engine, not by the site: changing it later does not fork the delta memory.

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

Remembers every record it has delivered (per filter set, in a named key-value store) and returns only records that are new or whose page changed since. Convictions (~210 records) are always walked in full; notices stop at the first two pages of already-known records, so a scheduled run costs a few pages.

## `recheckDays` (type: `integer`):

In delta mode, known records that are still open (Improvement Notices with Result 'Ongoing', and every conviction) are re-read for this many days after they were first delivered (or after their register date, whichever is later) to detect UPDATED events - the register has no 'last updated' field, so this is the only way to see a notice being complied with or a hearing being added. 0 disables re-checks. Up to 2,000 records are re-checked per run.

## `deltaStateName` (type: `string`):

Optional label for the memory of a monitoring task. Defaults to a fingerprint of your filters, so different schedules never interfere. Set the same name on two tasks to make them share one memory.

## `resetState` (type: `boolean`):

Forget every previously delivered record for this delta state and re-baseline (the run then returns up to 'Max records per register' most recently entered records).

## `maxItemsPerDataset` (type: `integer`):

Hard cap on delivered records per selected register (and therefore on cost: you pay per record). On the very first delta run the cap defines the baseline: the oldest record that run delivers is remembered per register, and records entered before it are history - later delta runs never deliver or charge for them, whatever the cap (run once with onlyNew=false, or set a high cap on the first run, if you want the history). On every later delta run the overflow is a backlog: the walk remembers where it stopped (a per-register watermark in the delta memory) and the next run walks down to that point instead of stopping at the records already delivered.

## `fetchDetail` (type: `boolean`):

Opens each record's own page for the description, dates, fine and costs, result, address, industry and HSE admin fields (plus, for convictions, the per-case breach list). Off = listing-row-only records (name, date, local authority, main activity, notice type) at the cheaper 'result-summary' price; no UPDATED detection is possible without it.

## `fetchBreachDetail` (type: `boolean`):

Convictions: opens each breach's page for the court, Act section / Regulation paragraph, hearing date, result and per-breach fine. Notices: fetches the breach list (Act / regulation / paragraph per breach). Off = the cheaper 'result-summary' price.

## `fetchPartyDetail` (type: `boolean`):

Opens the party's page (registered address, Status such as 'Private Company' or 'Individual', HSE Reference) and its case and notice lists on both registers - the repeat-offender signal (3 extra requests per record, no extra charge).

## `maxConcurrency` (type: `integer`):

Run-wide cap on simultaneous HTTP requests (listing, detail, breach and party pages together - never more than this in flight). The register is a classic-ASP site that answered 17 concurrent requests without throttling; 5 keeps a 100-record run around a minute.

## `dateRange` (type: `string`):

Kept for tasks created with version 1: '24h', '7d' or '30d' are interpreted as 'Date from'.

## Actor input object example

```json
{
  "datasets": [
    "convictions",
    "notices"
  ],
  "region": "",
  "country": "",
  "industry": "",
  "hseDivision": "",
  "defendantStatus": "",
  "resultingFromFatality": "any",
  "noticeTypes": [],
  "act": "",
  "eventTypes": [
    "SANCTION",
    "NEW_LISTING",
    "UPDATED"
  ],
  "onlyNew": true,
  "recheckDays": 180,
  "resetState": false,
  "maxItemsPerDataset": 100,
  "fetchDetail": true,
  "fetchBreachDetail": true,
  "fetchPartyDetail": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

No description

## `resultsNewestFirst` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `excel` (type: `string`):

No description

## `overview` (type: `string`):

No description

## `convictions` (type: `string`):

No description

## `notices` (type: `string`):

No description

## `compliance` (type: `string`):

No description

## `parties` (type: `string`):

No description

## `runSummary` (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 = {
    "onlyNew": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/uk-hse-enforcement-monitor").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 = { "onlyNew": True }

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/uk-hse-enforcement-monitor").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 '{
  "onlyNew": true
}' |
apify call stefano_seggio/uk-hse-enforcement-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/uk-hse-enforcement-monitor"
        }
    }
}

```

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/jV35qppM82fjyjsle/builds/mUclbEreHEu5G64Mb/openapi.json
