# GDPR Subprocessor List Monitor (DPA) · SiteVerdict (`siteverdict/siteverdict-subprocessor-list-monitor`) Actor

Watch SaaS vendors' published subprocessor lists. Parses tables, lists, JSON and CSV, then alerts your DPO to new, removed or renamed subprocessors, location and purpose changes and new transfers outside the EEA/UK. History, Slack/webhook alerts, optional AI note.

- **URL**: https://apify.com/siteverdict/siteverdict-subprocessor-list-monitor.md
- **Developed by:** [Aman Thakran](https://apify.com/siteverdict) (community)
- **Categories:** Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## SiteVerdict — Subprocessor List Monitor (GDPR / DPA)

**Know the day a vendor adds a new subprocessor, moves your data to a new country, or quietly renames one — before your DPA objection window closes.**

Paste the subprocessor-list URLs your SaaS vendors publish (Atlassian, GitHub, your CRM, your payroll tool…). Schedule this Actor weekly. The first run stores a baseline; every later run tells you exactly what changed, flags new transfers outside the EEA / UK / adequacy countries, and can ping Slack or any webhook with a DPO-ready message.

> **Not legal advice.** Parsing is heuristic and transfer classifications are indicative. Always verify against the vendor's official notice and your DPA.

### What you get

For every vendor list, each run:

1. **Fetches the page politely** (honest user agent, robots.txt respected, 1.5 s spacing per host, no bot-protection bypass).
2. **Parses the list** — HTML tables (any header wording: "Sub-processor", "Entity name", "Third party", "Location of processing", "Purpose", …), tables with `rowspan`/`colspan` and section rows, responsive tables that repeat labels in each cell, bullet lists ("Name – purpose – location"), definition lists, JSON exports and CSV exports.
3. **Normalises** names (`Twilio Inc.` = `TWILIO, INC`) and locations (`USA`, `U.S.`, `Northern Virginia`, `Frankfurt`, `EEA (Sweden, Ireland)` → ISO codes) and classifies each country: **EEA**, **UK**, **adequacy decision**, **US — check DPF certification**, **third country**.
4. **Compares** with the previous snapshot and reports:
   - **added** and **removed** subprocessors,
   - **likely renames** (same website domain or near-identical name — not reported as add + remove),
   - **location changes** (countries added / removed),
   - **purpose changes**,
   - **new third-country transfers** (new subprocessor or new location outside EEA / UK / adequate countries).
5. **Never fakes a clean result**: if a page can't be parsed, is blocked, or the list suddenly disappears, you get a `parse-empty` / `blocked` / `error` row with a clear warning — the stored baseline is kept.

### Use cases

1. **DPO / privacy team** — monitor all vendors in your Record of Processing Activities; respond to new subprocessors inside the contractual objection period.
2. **Procurement & vendor-risk** — keep an always-current inventory of fourth parties and where they process data (ENTRIES.csv).
3. **Consultancies / agencies** — run one monitor per client (`stateStoreName`) and forward Slack/webhook alerts.

### Before / after (sample mode output)

Run 1 (baseline) — *Acme Analytics* lists AWS (Ireland), Cloudy Hosting GmbH (Germany), OldMail Ltd (UK), Stripe (Ireland).
Run 2 — the Actor reports:

```json
{
  "vendor": "Acme Analytics (sample)",
  "status": "changed",
  "added": ["Nimbus AI Labs Pvt Ltd"],
  "removed": ["OldMail Ltd"],
  "locationChanges": [{ "name": "Cloudy Hosting GmbH", "from": "Germany", "to": "Germany, United States", "countriesAdded": ["US"], "countriesRemoved": [] }],
  "newThirdCountryTransfers": [
    { "name": "Nimbus AI Labs Pvt Ltd", "country": "IN", "classification": "third-country", "note": "Third country: SCCs / IDTA / other safeguard needed", "reason": "new-subprocessor" },
    { "name": "Cloudy Hosting GmbH", "country": "US", "classification": "US-DPF-check", "note": "US: check EU-US DPF / UK Extension certification, otherwise SCCs / IDTA needed", "reason": "new-location" }
  ]
}
```

Slack message: *"SiteVerdict: subprocessor list changed — Acme Analytics: 1 added (Nimbus AI Labs Pvt Ltd); 1 removed (OldMail Ltd); 1 location change(s); 2 new transfer(s) outside the EEA/UK/adequate countries … Reminder: check your DPA's objection period."*

Try it free: enable **Sample mode** (no network, nothing charged).

### Input example

```json
{
  "targets": [
    { "url": "https://www.atlassian.com/legal/sub-processors", "vendor": "Atlassian" },
    { "url": "https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors", "vendor": "GitHub" },
    { "url": "https://vendor.example/subprocessors.json", "vendor": "Vendor X" },
    { "url": "https://vendor.example/trust", "vendor": "Vendor Y", "selector": "#subprocessors table" }
  ],
  "stateStoreName": "client-acme",
  "slackWebhookUrl": "https://hooks.slack.com/services/…",
  "onlyChanged": true
}
```

Then create a **Schedule** in Apify Console (weekly is typical).

### Output

- **Dataset** (one row per vendor list) with views **Overview**, **Changes** (one row per change) and **Entries** (full lists, when `includeEntries` is on).
- **Key-value store**:
  - `REPORT` — HTML report for the run,
  - `SUMMARY` — agent-ready JSON (changed vendors, problems, disclaimer),
  - `CHANGES.csv`, `ENTRIES.csv`,
  - `HISTORY-<vendor>` — last N checks with their changes.
- The named store (`stateStoreName`) keeps the latest snapshot per vendor between runs.

Statuses: `baseline`, `unchanged`, `changed`, `parse-empty`, `blocked`, `robots-disallowed`, `error`.

### Alerts and the optional AI note

- **Webhook** (JSON) and **Slack** incoming webhook. HTTPS only; both are secret inputs, never logged or written to the dataset. Optional alerts when a list cannot be checked (`alertOnErrors`).
- **AI impact note** (off by default): with **your own** Anthropic or OpenAI key, each changed vendor gets a short DPO note (what changed, what to ask the vendor). The page content is passed as data with instructions to ignore any instructions inside it. Your provider bills the tokens; we charge the `ai-summary` event only when a note is produced.

### Pricing (pay per event)

| Event | When | Price |
|---|---|---|
| `list-checked` | a vendor list was fetched and parsed (baseline, unchanged or changed) | $0.01 |
| `change-alert` | a vendor list changed (once per vendor per run) | $0.03 |
| `ai-summary` | an AI impact note was produced with your own key | $0.02 |

Errors, blocked pages, robots-disallowed pages, unparseable pages and sample mode are **never charged**. Examples: 50 vendors checked weekly ≈ **$2.15/month** + $0.03 per actual change; 200 vendors monthly ≈ $2/month. The run stops cleanly at your "max total charge".

### FAQ

**The vendor's list is a PDF.** Not supported yet — the row says so (not charged). Ask the vendor for an HTML/JSON/CSV copy or a trust-centre page.

**The page uses cards / divs, not a table.** Set `selector` to the element that wraps the list. If nothing can be parsed you get `parse-empty` with a hint — never a false "no subprocessors".

**The page is behind a login, bot protection or robots.txt disallows it.** We do not bypass any of these. You get `blocked` / `robots-disallowed` (not charged).

**Does it understand subscription e-mails ("we're adding a new subprocessor in 30 days")?** No — it monitors the published list. Many vendors update the list at the start of the notice period.

**Is the US an "adequate" country?** Only organisations certified under the EU-US Data Privacy Framework (and the UK Extension). The Actor flags US locations as "check DPF certification" — it does not look certification up.

### Limits

- Max 200 lists per run, 8 MB per page, 2,000 entries per list.
- No JavaScript rendering: lists injected by client-side JavaScript after load may parse as empty (you'll be told). Most vendors publish server-rendered lists or JSON.
- Country detection uses names, ISO codes, common data-centre cities and US states; unusual wording may be missed (the raw `location` text is always kept).
- Rename detection is a heuristic (same website domain or ≥ 82 % name similarity).

### Sources & compliance

- **Sources:** only the public pages / files whose URLs you supply. Checked 11 Oct 2026: robots.txt is fetched and obeyed for every URL and redirect (RFC 9309); requests carry the user agent `SiteVerdict/subprocessor-list-monitor (+aman.thakran@bharatfare.com)`; at least 1.5 s between requests to the same host; no proxies, no fingerprinting, no CAPTCHA or login bypass. Respect each vendor's website terms for your use.
- **Security:** user-supplied URLs are checked against private, loopback, link-local and cloud-metadata addresses on every hop and at connect time (SSRF guard); response size and time limits apply; secrets are masked from logs and output.
- **Personal data (UK GDPR / PECR):** subprocessor lists name companies. Contact columns and e-mail addresses are removed by default (`includeContactDetails` off). If you enable them, you are the controller for that data; keep a lawful basis and minimise retention. The Actor sends no marketing messages.
- **Adequacy reference** (EU Commission decisions, as checked 11 Oct 2026): Andorra, Argentina, Canada (commercial organisations), Faroe Islands, Guernsey, Israel, Isle of Man, Japan, Jersey, New Zealand, Republic of Korea, Switzerland, United Kingdom, Uruguay, and the US for DPF-certified organisations only. Decisions change; verify before relying on them.
- **Disclaimer:** technical monitoring information only, not legal advice; no warranty that a vendor's list is complete or that a transfer is lawful.

Built by SiteVerdict (BharatFare Travel and Technologies Ltd, UK). Questions: aman.thakran@bharatfare.com.

# Changelog

This Actor's version history is a separate document: https://apify.com/siteverdict/siteverdict-subprocessor-list-monitor/changelog.md

# Actor input Schema

## `targets` (type: `array`):

One object per vendor: <code>{"url": "https://vendor.com/legal/subprocessors", "vendor": "Vendor name", "selector": "optional CSS selector of the table/list"}</code>. Plain URL strings also work. HTML pages (tables, bullet lists, definition lists), JSON and CSV exports are supported. Max 200 per run.

## `urls` (type: `array`):

Alternative to the JSON above: one URL per line. The vendor name defaults to the host name.

## `stateStoreName` (type: `string`):

Named key-value store that keeps the last snapshot and change history per vendor between runs. Use a different name per client or portfolio.

## `historySize` (type: `integer`):

How many past checks (with their changes) to keep per vendor.

## `onlyChanged` (type: `boolean`):

Skip dataset rows for baseline/unchanged vendors. Errors, blocked pages and unparseable pages are always output.

## `includeEntries` (type: `boolean`):

Adds every parsed subprocessor (name, purpose, location, countries, transfer class) to each row (see the 'Entries' view). ENTRIES.csv in the key-value store always has them.

## `includeContactDetails` (type: `boolean`):

Off by default: contact columns and e-mail addresses found on lists are removed (UK GDPR data minimisation). Turn on only if you need them and have a lawful basis.

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

Optional HTTPS endpoint that receives a JSON payload for every changed vendor (Zapier, Make, n8n, your SIEM...). Stored as a secret.

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

Optional Slack incoming-webhook URL for a DPO-friendly change message. Stored as a secret.

## `alertOnErrors` (type: `boolean`):

Send an alert when a page errors, is blocked or can no longer be parsed (useful when vendors move their list).

## `aiProvider` (type: `string`):

Off by default. With your OWN API key, writes a short DPO impact note for each changed vendor (charged as 'ai-summary' only when it succeeds; your provider bills the tokens).

## `aiApiKey` (type: `string`):

Your Anthropic or OpenAI API key. Stored as a secret, never logged or output.

## `aiModel` (type: `string`):

Model id. Defaults: Anthropic claude-sonnet-5-5, OpenAI gpt-4.1-mini.

## `timeoutSecs` (type: `integer`):

Request timeout (seconds).

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

Requests to the same host are always spaced at least 1.5 s apart.

## `sampleMode` (type: `boolean`):

Runs on two bundled fictional vendor lists (baseline + changed version) to show the output: one new subprocessor in India, one removal and one location change. Nothing is charged.

## Actor input object example

```json
{
  "targets": [
    {
      "url": "https://www.atlassian.com/legal/sub-processors",
      "vendor": "Atlassian"
    },
    {
      "url": "https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors",
      "vendor": "GitHub"
    }
  ],
  "stateStoreName": "siteverdict-subprocessor-monitor",
  "historySize": 20,
  "onlyChanged": false,
  "includeEntries": false,
  "includeContactDetails": false,
  "alertOnErrors": false,
  "aiProvider": "none",
  "timeoutSecs": 30,
  "maxConcurrency": 3,
  "sampleMode": false
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

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

No description

## `files` (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 = {
    "targets": [
        {
            "url": "https://www.atlassian.com/legal/sub-processors",
            "vendor": "Atlassian"
        },
        {
            "url": "https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors",
            "vendor": "GitHub"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("siteverdict/siteverdict-subprocessor-list-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 = { "targets": [
        {
            "url": "https://www.atlassian.com/legal/sub-processors",
            "vendor": "Atlassian",
        },
        {
            "url": "https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors",
            "vendor": "GitHub",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("siteverdict/siteverdict-subprocessor-list-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 '{
  "targets": [
    {
      "url": "https://www.atlassian.com/legal/sub-processors",
      "vendor": "Atlassian"
    },
    {
      "url": "https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors",
      "vendor": "GitHub"
    }
  ]
}' |
apify call siteverdict/siteverdict-subprocessor-list-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,siteverdict/siteverdict-subprocessor-list-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/4jgFmfSRQAGQaREfc/builds/SuK5FVXE4edoABe3c/openapi.json
