# UK Modern Slavery Statement Registry - S.54 Monitor (`stefano_seggio/uk-modern-slavery-statement-registry-monitor`) Actor

Delta-tracks the official UK Modern Slavery Statement Registry (gov.uk) - 34,000+ organisations, 23,500+ statements - for new statements, compliance-status changes, and missing mandatory disclosures across the six recommended topics. Sourced from the registry's own bulk CSV downloads. Pay-per-event.

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

## Pricing

from $20.00 / 1,000 new statements

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

## UK Modern Slavery Statement Registry — Delta Monitor

![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![Tests](https://img.shields.io/badge/tests-77%20passing-brightgreen) ![Data source](https://img.shields.io/badge/data%20source-gov.uk%20official%20bulk%20export-003439) ![Pricing](https://img.shields.io/badge/pricing-pay--per--event-orange)

Delta-tracks the official UK Modern Slavery Statement Registry — 34,000+ organisations, 23,500+
statements (real, live figures as of this actor's build date) — for new statements,
compliance-status changes, and **missing mandatory disclosures** across the six government
recommended topics. Sourced from the registry's own official yearly bulk CSV exports under the
Open Government Licence v3.0. Part of [Delta Registry](https://github.com/stefanoseggio), a
pay-per-event regulatory/compliance data fleet.

### What this actor actually does that a one-shot export can't

The registry publishes a full CSV snapshot per year — anyone can download it. What this actor adds
is the same zero-cost delta-monitoring discipline as the rest of this fleet
([ARCHITECTURE.md](ARCHITECTURE.md)): **you are billed only for a statement that is new, or whose
compliance status genuinely changed** — never for re-confirming that nothing changed. And it goes
one level deeper than most delta actors: a real HTTP `Content-MD5` check on the registry's own
files means an **unchanged year is never even downloaded**, not just never re-billed.

Zero registry competitors exist for this exact niche on Apify Store today (confirmed via a live
Store API search on this actor's build date) — the closest real analogous product found is a
Brazil-focused ESG/forced-labour supply-chain monitor (`paulovitor18/brazil-esg-supply-chain-monitor`,
a different country and data source, but the same pay-per-event delta-pricing category), which
validates real market pricing for this kind of product: $0.06–$0.90 per event depending on tier.

### Quickstart

#### cURL

```bash
curl "https://api.apify.com/v2/acts/stefano_seggio~uk-modern-slavery-statement-registry-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "years": ["2026"],
    "onlyMissingDisclosures": true,
    "maxItems": 100
  }'
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("stefano_seggio/uk-modern-slavery-statement-registry-monitor").call(run_input={
    "years": ["2026"],
    "sectorFilter": ["Private"],
    "onlyMissingDisclosures": True,
    "maxItems": 100,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f"{item['event_type']}: {item['organisation_name']} - missing: {item['missing_disclosures']}")
```

#### Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const run = await client.actor('stefano_seggio/uk-modern-slavery-statement-registry-monitor').call({
  years: ['2026'],
  sectorFilter: ['Private'],
  onlyMissingDisclosures: true,
  maxItems: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => console.log(`${item.event_type}: ${item.organisation_name} - missing: ${item.missing_disclosures.join(', ')}`));
```

### Pricing (pay-per-event)

| Event | Price | When it fires |
|---|---|---|
| `new-statement` | $0.02 | A statement (organisation + registry year) never seen before appears, after this configuration's baseline is established. |
| `statement-updated` | $0.01 | A previously-seen statement's content fingerprint changed (any real field, not just compliance status). |
| `STATEMENT_UNCHANGED` / `BASELINE_SNAPSHOT` | **Never billed** | First-run baseline observations and confirmed-unchanged statements are always free — and an unchanged registry year is never even downloaded (see Architecture). |

*Pricing above is live — this actor is published on Apify Store, and these are the exact,
currently-active Pay-Per-Event prices configured in the Apify Console's monetization settings, not
a proposal. `apify-actor-start` is retained (the first 5 seconds of platform compute is waived on
every run) and `apify-default-dataset-item` is removed (no automatic per-write dataset charge), so
the "unchanged statements cost nothing" guarantee above is enforced at both the application layer
and the Console billing layer.*

### Input reference

See [`.actor/input_schema.json`](.actor/input_schema.json) for the full, authoritative schema.

| Field | Type | Default | Notes |
|---|---|---|---|
| `years` | array | `["2026"]` | Which of the real 2020–2027 registry years to check. Every run cheaply HEAD-checks every selected year; only changed years are actually downloaded. |
| `sectorFilter` | array | `[]` (both) | `Private` / `Public` — the two real self-declared values. |
| `turnoverFilter` | array | `[]` (all) | The five real turnover brackets, exact strings as published. |
| `onlyMissingDisclosures` | boolean | `false` | Deliver only statements with at least one of the six recommended topics uncovered - a genuine compliance gap. |
| `maxItems` | integer | 50 | This actor's own per-run push cap. |
| `deltaStateName` / `resetState` / `onlyNew` | — | fleet defaults | Same convention as the rest of this fleet. |
| `webhookUrl` | string | — | Generic JSON POST, no formatting. |
| `slackWebhookUrl` | string | — | See **Alerting** below. |
| `teamsWebhookUrl` | string | — | See **Alerting** below - real 2026 caveat, not the old connector. |

### Output record

```json
{
  "@type": "schema:Organization",
  "event_id": "a9e3df21...",
  "event_type": "NEW_STATEMENT",
  "record_id": "https://modern-slavery-statement-registry.service.gov.uk/statement-summary/nEQF3gnC/2027::ALUN GRIFFITHS (CONTRACTORS) LIMITED",
  "organisation_name": "ALUN GRIFFITHS (CONTRACTORS) LIMITED",
  "company_number": "01493003",
  "address": "Waterways House Merthyr Road, Llanfoist, Abergavenny, Monmouthshire, United Kingdom, NP7 9PE",
  "parent_name": null,
  "group_submission": false,
  "sector_type": "Private",
  "organisation_sectors": ["Construction, civil engineering and building products"],
  "turnover": "£100 million to £500 million",
  "statement_url": "https://griffiths.co.uk/wp-content/uploads/2026/05/Modern-Slavery-Human-Trafficking-Policy-2026.pdf",
  "statement_summary_url": "https://modern-slavery-statement-registry.service.gov.uk/statement-summary/nEQF3gnC/2027",
  "pdf_url": "https://downloads.modern-slavery-statement-registry.service.gov.uk/pdf-published/nEQF3gnC/2027/Modern Slavery and Human Trafficking Policy.pdf",
  "statement_year": "2027",
  "includes_org_structure": true,
  "includes_policies": false,
  "includes_risk_assessment": true,
  "includes_due_diligence": true,
  "includes_training": true,
  "includes_goals": false,
  "missing_disclosures": ["Policies", "Goals"],
  "is_fully_compliant": false,
  "ilo_indicators_in_statement": false,
  "status_fingerprint": "2387e5ba...",
  "content_fingerprint": "9d24b7ea...",
  "is_new": true,
  "scraped_at": "2026-09-16T23:10:17.817Z"
}
```

This is a real record from this actor's own live verification run against the actual 2027 registry
file — not a fabricated example.

`event_id` is a SHA-1 idempotency key over `(record_id, event_type, status_fingerprint,
content_fingerprint)` — a retried delivery of the same underlying event always reproduces the same
ID, safe for downstream deduplication.

**A note on `record_id`**: it is a compound key (`statementSummaryUrl::organisationName`), not the
bare registry URL. This was a real, live-discovered necessity, not a stylistic choice — a single
group modern slavery statement can legally cover several named organisations under one shared
`statement_summary_url` (confirmed live: one real 2027 group statement names four distinct
organisations sharing one URL). Using the URL alone would have collapsed those into one tracked
entity. See [ARCHITECTURE.md section 4](ARCHITECTURE.md#4-delta-engine--same-fingerprint-discipline-as-actor-1-applied-to-a-bulk-file-source).

### Alerting — Slack and Microsoft Teams, corrected against real 2026 platform status

Both channels fire on the same trigger: a brand-new statement, or an existing statement whose
compliance-relevant fields (the six recommended topics, ILO indicators) genuinely changed - not
merely whenever those fields happen to be populated.

#### Slack

Create the webhook via a **Slack App** (Slack workspace → Apps → create/select an app → Features →
Incoming Webhooks → Add New Webhook to Workspace). This is Slack's own current, documented method.
Do **not** use the older "legacy custom integrations" webhook path - Slack is phasing that
specific mechanism out (existing legacy URLs still deliver today, but new setups should use the
App-based path). Paste the resulting URL into `slackWebhookUrl`.

#### Microsoft Teams

**Important, and different from what you may expect**: Microsoft retired the classic Teams
"Incoming Webhook" connector - the mechanism most guides (and AI models with an older training
cutoff) still describe - in a **May 2026** cutover, after blocking new connector creation back in
August 2024. If you already have an old `outlook.office.com/webhook/...` URL, it no longer works.

The current, real mechanism: in a Teams channel, open **Workflows**, search for the **"When a
Teams webhook request is received"** template, and complete the setup - Power Automate gives you a
new webhook URL. Paste that into `teamsWebhookUrl`. This actor posts a standard **Adaptive Card**
(the modern Teams card format) wrapped in a message attachment. Honest caveat: a Power Automate
flow's exact expected trigger schema is user-configurable per flow, so if your flow's "Parse JSON"
step expects a different shape, adjust it to match this actor's payload (documented in
[`src/notifier.ts`](src/notifier.ts)) - this was not independently confirmed against a live Teams
tenant during this build, since none was available to test against.

### Architecture

Full spec in [ARCHITECTURE.md](ARCHITECTURE.md). Summary:

```
  Actor input ──▶ src/main.ts (migrating/aborting-safe state flush)
                        │
                        ▼
              src/routes.ts: for each selected registry year
                        │
                        ▼
         src/csvSource.ts: HEAD request → real Content-MD5
                        │
              ┌─────────┴─────────┐
              ▼                   ▼
      unchanged since        changed / never
      last run → SKIP        seen → GET + parse
      (no download at        (csv-parse, RFC 4180 -
       all)                   handles real embedded
                               newlines in quoted fields)
                        │
                        ▼
         src/deltaEngine.ts: normalize → canonicalize
         → SHA-256 → classify (NEW_STATEMENT /
         STATEMENT_UPDATED / STATEMENT_UNCHANGED /
         BASELINE_SNAPSHOT)
                        │
                        ▼
         src/state.ts: Key-Value Store persistence
         (per-statement fingerprints + per-year
          content-hash cache)
                        │
                        ▼
    Apify Dataset (pay-per-event push)
         + src/notifier.ts (webhook / Slack / Teams,
           each independently failure-isolated)
```

### Testing

```bash
npm test
```

77 real, passing Vitest tests across six files:

- [`tests/normalizer.test.ts`](tests/normalizer.test.ts) — CSV-row-to-statement mapping, including
  every real data-quality edge case found during this build (numeric `OrganisationName` fallback,
  stray quote characters, multi-line field joining, blank `CompanyNumber`, the compound-key
  shared-URL group-statement scenario, and the record-key fallback chain for blank identity
  fields).
- [`tests/deltaEngine.test.ts`](tests/deltaEngine.test.ts) — canonicalization determinism, SHA-256
  fingerprint stability and change-sensitivity, and the full classify/shouldDeliver state machine,
  including a regression test for the exact classify/onlyNew conflation bug found in Actor #1 and
  a regression test for the per-year `baselineComplete` scoping bug found by adversarial review in
  this actor.
- [`tests/notifier.test.ts`](tests/notifier.test.ts) — real payload-shape assertions for all three
  channels (mocked `fetch`, no live sends), failure-isolation tests confirming one channel's
  rejection never blocks another's delivery or throws out of the run, and escaping tests confirming
  registry-sourced (self-reported, untrusted) fields can't inject Slack mrkdwn or Adaptive Card
  markdown.
- [`tests/csvSource.test.ts`](tests/csvSource.test.ts) — the HTTP retry/backoff logic (5xx/429
  retried, other 4xx passed through immediately, network errors retried), the real 404-means-null
  distinction, RFC 4180 parsing of embedded newlines, and the request-timeout `AbortSignal`.
- [`tests/state.test.ts`](tests/state.test.ts) — Key-Value Store round-tripping, `resetState`
  behavior, and the mutate-by-reference contract `routes.ts` relies on.
- [`tests/routes.test.ts`](tests/routes.test.ts) — unit tests for the pure per-row/per-event logic
  (filters, event naming, high-value-change detection, output-record shaping) plus **integration
  tests that mock the CSV source and simulate a real API response mutation across sequential
  runs** to verify the delta trigger fires correctly - including regression tests for the
  maxItems-truncation caching bug and the filter/baseline-scoping bug, both found by this actor's
  own live testing and adversarial review.

### CI/CD

[`.github/workflows/deploy.yml`](.github/workflows/deploy.yml): every push and pull request runs
lint, type-check/build, and the full test suite; a push to `main` that passes all three then
authenticates with the Apify CLI (`apify login --token`) and deploys (`apify push`) using an
`APIFY_TOKEN` repository secret.

### What this actor deliberately does not do

- **No scraping, no anti-bot layer.** The registry's bulk CSV exports are official, licensed
  (Open Government Licence v3.0), and require no authentication - there's nothing to bypass.
- **No BLAKE3.** Same fleet-wide decision as Actor #1, for the same reason - see
  [ARCHITECTURE.md §4](ARCHITECTURE.md#4-delta-engine--same-fingerprint-discipline-as-actor-1-applied-to-a-bulk-file-source).
- **No classic Microsoft Teams connector support.** It's retired - see **Alerting** above.
- **No server-side filtering.** The registry's bulk export has no query mechanism; `sectorFilter`/
  `turnoverFilter`/`onlyMissingDisclosures` are applied client-side after parsing, so they narrow
  what's delivered, not what's downloaded.

***

This actor is part of **Delta Registry** — pay-per-event regulatory & compliance data
infrastructure built and operated by Stefano Seggio. For the rest of the fleet, see
[github.com/stefanoseggio](https://github.com/stefanoseggio).

# Changelog

This Actor's version history is a separate document: https://apify.com/stefano\_seggio/uk-modern-slavery-statement-registry-monitor/changelog.md

# Actor input Schema

## `years` (type: `array`):

Which registry years to check this run. The registry publishes one full CSV snapshot per year (2020-2027, confirmed live), each containing every statement submitted for that reporting year. Each run does a cheap HEAD request per selected year first and only downloads/parses a year's full file when its content-hash has actually changed since last run (see README/ARCHITECTURE.md) - so selecting more years costs little on unchanged runs, but the FIRST run against a newly-selected year always does a full baseline pass. Defaults to the current year only to keep a first/default run fast and bounded.

## `sectorFilter` (type: `array`):

Deliver only statements from organisations that self-declared this sector. Real values as published by the registry: 'Private' or 'Public'. Leave empty to deliver both. This is a client-side filter applied after parsing (the registry's bulk CSV has no server-side query/filter mechanism - see ARCHITECTURE.md section 0), so it does not reduce download size, only which normalized statements are delivered.

## `turnoverFilter` (type: `array`):

Deliver only statements from organisations that self-declared one of these turnover brackets - the exact real bracket strings used by the registry, confirmed live. Leave empty to deliver all brackets (including organisations that left this optional field blank).

## `onlyMissingDisclosures` (type: `boolean`):

When true, delivers only statements that do NOT cover at least one of the six government-recommended topics (organisational structure, policies, risk assessment, due diligence, training, goals) - i.e. a genuine compliance gap, not just any statement. When false (default), delivers all statements matching the other filters regardless of disclosure completeness.

## `maxItems` (type: `integer`):

Stops the run once this many changed/new statements have been pushed (and charged, where applicable) across all selected years - this actor's own safety valve. Statements not yet reached when the limit is hit are simply re-evaluated fresh next run. Defaults to 50 so a default/test run completes in well under Apify's automated-test window.

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

Names the persistent Key-Value Store this run's delta memory (per-statement fingerprints and per-year content-hash cache) lives in. Use a distinct name per independent schedule/filter configuration so they don't share baseline progress. Leave as 'default' for a single schedule.

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

When true, ignores all previously-remembered statements and per-year content-hashes for this deltaStateName and re-baselines from scratch (every matched statement becomes a fresh, uncharged BASELINE\_SNAPSHOT again, and every selected year is re-downloaded regardless of its content-hash). Use only to deliberately restart monitoring.

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

When true (recommended, and the only free-of-charge default), only NEW\_STATEMENT and STATEMENT\_UPDATED events are delivered - never a snapshot of an unchanged statement or a first-run baseline. When false, every matched statement is delivered every run its year is processed, including BASELINE\_SNAPSHOT (first run) and STATEMENT\_UNCHANGED rows.

## `webhookUrl` (type: `string`):

Any HTTPS endpoint that accepts a JSON POST (e.g. a Make/n8n/Zapier catch-hook, or your own service) - receives the full raw event payload with no channel-specific formatting. Independent of, and does not replace, Apify's own platform-level webhook system configurable separately in the Apify Console.

## `slackWebhookUrl` (type: `string`):

A Slack Incoming Webhook URL created via a Slack App (Slack's own currently-supported method - see 'Slack Apps' in your workspace settings, then add the 'Incoming Webhooks' feature). Do NOT use the older 'legacy custom integrations' webhook - Slack itself is phasing that specific path out, though existing legacy URLs still deliver today. Receives a formatted Slack Block Kit message on a status-tier change (award-equivalent: a mandatory-disclosure or compliance-status change).

## `teamsWebhookUrl` (type: `string`):

A Microsoft Teams 'Workflows' webhook URL (Teams' current, real mechanism as of 2026 - open a channel, 'Workflows', search the 'When a Teams webhook request is received' template). This is NOT the older Incoming Webhook 'Connector' - Microsoft fully retired Office 365 connectors in Teams in a May 2026 cutover, so a classic connector URL will not work. Receives a formatted Adaptive Card (the current Teams card format) on the same status-tier change trigger as the Slack webhook.

## Actor input object example

```json
{
  "years": [
    "2026"
  ],
  "sectorFilter": [],
  "turnoverFilter": [],
  "onlyMissingDisclosures": false,
  "maxItems": 50,
  "deltaStateName": "default",
  "resetState": false,
  "onlyNew": true
}
```

# Actor output Schema

## `statements` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/uk-modern-slavery-statement-registry-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/uk-modern-slavery-statement-registry-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 '{}' |
apify call stefano_seggio/uk-modern-slavery-statement-registry-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/uk-modern-slavery-statement-registry-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/vWDvsAd2iXgVp5aC7/builds/8XUcFORViFVhA9pqw/openapi.json
