# Subprocessor Change Monitor (`virtual-constructs/subprocessor-change-monitor`) Actor

Watch vendor subprocessor pages and get alerted when subprocessors are added, removed, or changed.

- **URL**: https://apify.com/virtual-constructs/subprocessor-change-monitor.md
- **Developed by:** [Virtual Constructs](https://apify.com/virtual-constructs) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$50.00 / 1,000 checks

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## Subprocessor Change Monitor

Watch any vendor's subprocessor page and get alerted the moment a subprocessor
is added, removed, or changed. This is the recurring GDPR Article 28 chore, the
one where you are supposed to review each vendor's sub-processor list for
changes, turned into a scheduled, pay-per-use monitor that runs itself.

You supply the URLs (you know your own vendors); the monitor fetches each page,
parses the subprocessor list, and diffs it against the last run. Every run
returns only what changed. Output is a stable, documented JSON schema, easy to
wire into alerts, tickets, compliance workflows, and AI agents (MCP-friendly).

This is a monitor, not a platform: one dumb reliable pipe. No dashboard, no
posture score, no policy templates. It watches pages and tells you what moved.
Continuous sub-processor assurance across a 20-vendor list runs about $30 a
month (see costs below), far less than doing the review by hand or than
learning late that a vendor changed who processes your data.

### What does it return?

One dataset item per URL per run:

- `changed` and `changeType` (`list`, `content`, or `none`)
- `added[]` and `removed[]`: subprocessors that appeared or disappeared, each
  with `name`, `purpose`, `location`, and the raw row text
- `changedEntries[]`: subprocessors present in both runs whose purpose,
  location, or text changed (`before` / `after`)
- `currentList[]`: the full normalized subprocessor list parsed this run
- `contentHash`, `snapshotRef`, `checkedAt`, and `ok` / `error`

### How to monitor subprocessor changes

1. Paste the vendor subprocessor page **URLs** you want to watch, e.g.
   `https://stripe.com/legal/service-providers`.
2. Set a **Monitor name** and put the actor on a schedule (daily or weekly is
   typical). The first run per URL establishes a baseline; every later run
   returns only what changed.
3. Results land in the dataset as JSON, CSV, or Excel. Wire a webhook or a
   Slack/email integration to the run to get alerted on `changed: true`.

That is the whole product: point it at the pages, schedule it, get diffs.

#### Input

| Field                | Type    | Default            | Notes                                                                             |
| -------------------- | ------- | ------------------ | --------------------------------------------------------------------------------- |
| `urls`               | array   | required           | Vendor subprocessor page URLs to watch (you supply them).                         |
| `mode`               | string  | `auto`             | `auto` parses a structured list when found, else text. `list` / `text` force it.  |
| `notifyOn`           | string  | `list-change-only` | `list-change-only` ignores page-chrome noise; `any-change` flags any text change. |
| `selectorByUrl`      | object  | `{}`               | Optional `{ "url": "cssSelector" }` to pin the exact content region.              |
| `monitorName`        | string  | `default`          | Names this watchlist's saved state; enables run-to-run diffing.                   |
| `maxConcurrency`     | integer | 5                  | Pages fetched in parallel (kept polite).                                          |
| `proxyConfiguration` | object  | Apify Proxy on     | Spreads requests across IPs; improves reach on rate-limited hosts.                |

#### Managed monitoring: set a name, schedule it, done

Set `monitorName` (e.g. `"my-vendors"`) and the actor keeps a small snapshot
per URL in a key-value store in your Apify account. Every scheduled run diffs
each page against the last run:

- First run for a URL: establishes the baseline (`baseline: true`, no change
  reported).
- Every later run: reports `added` / `removed` / `changedEntries` if the list
  moved, otherwise `changed: false`.
- Changing a URL's `selectorByUrl` or the diff `mode` re-baselines that URL
  (the content region is no longer comparable).

Use different monitor names for independent watchlists. No cursor management,
no state of your own.

#### Output example

```json
{
	"url": "https://acme.com/legal/subprocessors",
	"checkedAt": "2026-07-13T16:00:00.000Z",
	"ok": true,
	"changed": true,
	"changeType": "list",
	"baseline": false,
	"added": [
		{
			"name": "Twilio, Inc.",
			"purpose": "SMS delivery",
			"location": "US",
			"raw": "Twilio, Inc. | SMS delivery | US"
		}
	],
	"removed": [],
	"changedEntries": [],
	"currentList": [
		{
			"name": "Amazon Web Services, Inc.",
			"purpose": "Hosting",
			"location": "US",
			"raw": "Amazon Web Services, Inc. | Hosting | US"
		}
	],
	"entryCount": 24,
	"path": "list",
	"contentHash": "9f2c1a5b7d3e4c6a8b0f2d1e3c5a7b9d0e2f4a6c",
	"snapshotRef": "my-vendors--3a1f8c2b9d4e6f0a",
	"error": null,
	"source": "subprocessor-monitor"
}
````

Every field is always present; missing data is `null`, never an absent key.
New fields may be added in future versions; existing fields will not change
shape.

#### Result status and error codes

Every record has an `ok` flag and an `error` field. When `ok` is `true`, the
page was fetched and diffed and `error` is `null`. When a URL cannot be
checked, `ok` is `false`, `error` says why, and that URL is not charged. The
full set of error codes:

| `error`             | What it means                                                                                                                               | Charged? |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `null`              | Success: the page was fetched and diffed.                                                                                                   | Yes      |
| `js-rendered`       | The page is a JavaScript-only shell (SafeBase, Vanta-hosted trust center, etc.); the list is not in the server HTML.                        | No       |
| `no-content-region` | The page loaded with real content, but no usable subprocessor region was found (unusual markup, or a `selectorByUrl` that matched nothing). | No       |
| `non-html`          | The response was a PDF or other non-HTML document.                                                                                          | No       |
| `http-error`        | The server returned a 4xx/5xx status (blocked, moved, rate-limited, or down).                                                               | No       |
| `fetch-failed`      | The request failed to complete (network error, DNS, or timeout).                                                                            | No       |
| `charge-limit`      | Your run's spending limit was hit before this URL. It is left unchecked and picked up on the next run.                                      | No       |

Only `null` (a successful check) is billed. Everything else is free, so a run
where nothing could be checked costs nothing.

### Who is this for?

Privacy and DPO teams meeting GDPR Article 28 sub-processor review duties,
GRC and vendor-management teams tracking their supply chain, procurement and
security reviewers, and AI agents that need vendor subprocessor changes as
clean structured JSON instead of scraping HTML tables themselves. It also
works for competitive and market research: watch what vendors your competitors
add.

If your GRC platform already tracks the subprocessors of tools it integrates
with, this covers the vendors it does not: any company with a public
subprocessor page, a la carte, pay-per-use, callable from a pipeline or agent.

### Why monitor subprocessor changes?

Under GDPR Article 28, a data processor must keep a current list of its
sub-processors and give customers advance notice and a chance to object before
it adds or replaces one. Your Data Processing Agreement (DPA) with each vendor
usually points at that subprocessor page as the source of truth. The catch is
that the notice does not always reach you: pages change quietly, update emails
land in an unwatched inbox, and whoever owns vendor risk often finds out at
audit time instead of when it happened.

Watching each vendor's sub-processor list is also a recurring control in the
frameworks your customers ask about: SOC 2 and ISO 27001 vendor and
third-party risk management, CCPA/CPRA service-provider disclosures, and
general supply-chain due diligence. A new sub-processor can mean a new country
of data processing, a new AI vendor touching customer data, or a fourth party
you never assessed. Catching the change on the day it ships, with a structured
`added` / `removed` diff, is the difference between a proactive review and an
awkward finding.

This actor turns that standing chore into a scheduled job: a subprocessor
change monitor and alert across your whole vendor watchlist, with no
subscription and no platform to adopt.

### Monitor your own subprocessor page too

You can point it at your own company's subprocessor / sub-processor page, not
just your vendors'. That catches your published list drifting from reality: a
hosting migration your team shipped, an analytics or AI vendor added without
updating the page, or an edit that should have triggered customer notice under
your own DPA. It is a cheap standing check that your public sub-processor
disclosure still matches what you actually run, which is exactly what an
auditor or a security-conscious prospect will look at.

### How much does it cost?

Pay per event: $0.05 per URL checked, and nothing else. No subscription, no
seat licences, no charge for pages that could not be checked, no charge for
empty runs.

What that buys, in practice:

- **A 20-vendor watchlist, checked daily: about $30 a month** (roughly 600
  checks). That is continuous GDPR Article 28 sub-processor monitoring across
  your whole vendor list, running itself, for less than an hour of a privacy
  analyst's time.
- Ten vendors, weekly: around $2 a month.
- A one-off audit of 50 vendors: $2.50 for the run.

Compare that to doing it by hand (someone opening each vendor's page on a
calendar reminder and eyeballing the table for changes), or to the price of
finding out late that a vendor quietly added a sub-processor you never got to
object to. The check is the product: you are paying to have the pages watched
on a schedule, not only for the runs that happen to surface a change.

`maxConcurrency` affects speed, not price; the price is per URL per run, and
pages that come back `ok: false` are free.

### Using it from code and agents

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("virtual-constructs/subprocessor-change-monitor").call(run_input={
    "urls": [
        "https://stripe.com/legal/service-providers",
        "https://www.atlassian.com/legal/sub-processors",
    ],
    "monitorName": "my-vendors",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["changed"]:
        print(item["url"], item["added"], item["removed"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('virtual-constructs/subprocessor-change-monitor').call({
	urls: ['https://stripe.com/legal/service-providers'],
	monitorName: 'my-vendors',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

AI agents can call this actor as an MCP tool via Apify's MCP server
(mcp.apify.com). The input and output schemas are stable and documented, so a
call like "tell me if any of these vendors changed their subprocessors" works
without the agent knowing anything about page markup.

### Limits, stated plainly

- **Static pages only, in this version.** The monitor reads server-rendered
  HTML with a fast HTTP fetch. Subprocessor lists hosted on JavaScript-only
  trust centers (SafeBase, Vanta-hosted trust pages, and similar) come back as
  `ok: false` with `error: "js-rendered"` rather than a wrong answer. In a
  sample of 27 real vendor pages, about 70% were readable this way; the rest
  were JS-rendered, PDF-only, or bot-protected. Browser rendering for the
  JS-only pages is planned as a later, separately-priced tier.
- **A few otherwise-static pages inject the list with JavaScript.** If the page
  has enough static text to look real but the subprocessor list itself is
  rendered client-side (Twilio is one example), the monitor falls back to
  watching the page text and reports `path: "text"`, so `entryCount` will be 0
  and you will not get per-vendor `added`/`removed`. Check `path` on a new URL:
  if it is `"text"` when you expected a list, that page needs the browser tier.
- **You supply the URLs.** The actor does not discover a company's
  subprocessor page for you. This is deliberate: it removes the fuzziest,
  most brittle part of the problem and keeps the monitor reliable. You know
  your vendors; paste their pages.
- **Change detection, not legal judgement.** It tells you a subprocessor was
  added, removed, or edited. It does not assess whether that change matters
  for your compliance posture. That is your call (and deliberately not ours,
  for accuracy and liability reasons).
- **PDF and non-HTML pages** are reported as `ok: false` with `error:
  "non-html"` in this version.
- **Freshness:** data is as of the `checkedAt` timestamp on each record. This
  is a poll on your schedule, not a push feed.
- **Best effort on messy pages.** `list-change-only` (the default) and date
  masking suppress most false positives from page chrome; pin the exact region
  with `selectorByUrl` if a particular page is still noisy.

### FAQ

**Why not just check the pages myself?**
For one or two vendors you can. The paid value is doing it for a whole
watchlist on a schedule: fetching, parsing tables and lists into a normalized
per-vendor diff, ignoring page-chrome and date noise, remembering the last
state so you only see changes, and handing an agent or a no-code schedule a
stable tool. Many vendors offer their own "subscribe to subprocessor updates"
email, but that is one vendor at a time and email-only; this is one watchlist
across any vendor, as structured data.

**What is a subprocessor page?**
A public page where a SaaS vendor lists the third parties (sub-processors) it
shares customer data with, e.g. AWS for hosting or Stripe for payments. GDPR
Article 28 requires processors to disclose these and give customers a chance to
object to changes, so most vendors keep such a page.

**How does it avoid false alarms when a page just tweaks its date?**
The default `notifyOn: list-change-only` reports a change only when the
subprocessor list itself changes, and date literals and "last updated" lines
are masked before comparison. Corporate-suffix-only edits (`Inc.` vs `Inc`) do
not count as a change either.

**A vendor's page did not work. Why?**
Most likely it is a JavaScript-only trust center (`error: "js-rendered"`), a
PDF (`error: "non-html"`), or it blocked the request (`error: "http-error"`).
The first two are honest current limits; for blocking, enabling Apify Proxy
usually helps.

**Can I watch part of a page?**
Yes. Put a CSS selector in `selectorByUrl` for that URL to pin the exact table
or list and ignore the rest of the page.

**Which vendors can I monitor?**
Any company with a public subprocessor page. It works today on the static
sub-processor and DPA pages of vendors like AWS, Google Cloud, Stripe, Twilio,
Datadog, Atlassian, HubSpot, Cloudflare, MongoDB, Figma, and Zendesk, among
many others. You are not limited to a fixed list: paste any vendor's URL and
the monitor watches it.

**How do I get a Slack, email, or webhook alert when a subprocessor changes?**
Attach an Apify integration or a webhook to the run and filter on
`changed: true` (or on a non-empty `added` / `removed`). That turns the actor
into a subprocessor change alert into Slack, email, a ticket, or your own
pipeline, without polling anything yourself.

**Does it work with Vanta, SafeBase, or OneTrust trust centers?**
Many of those are JavaScript-rendered, so in this version they come back as
`error: "js-rendered"` rather than a wrong answer (browser rendering for them
is a planned tier). If the same vendor also publishes a static legal or DPA
subprocessor page, point the monitor at that URL instead and it works today.

**How often should I check vendor subprocessor pages?**
Daily or weekly is typical. Because you only pay per check, a daily schedule on
a small watchlist is inexpensive, and a weekly one is cheaper still. Pick the
cadence that matches your notice-and-objection window.

**Does this help with SOC 2, ISO 27001, or CCPA, not just GDPR?**
Yes. Sub-processor and third-party vendor monitoring is a recurring control in
SOC 2 and ISO 27001, and CCPA/CPRA has its own service-provider disclosure
obligations. The same `added` / `removed` diff is useful evidence for all of
them.

**Can AI agents monitor subprocessors automatically?**
Yes. The actor is MCP-friendly and callable as a tool from an agent via Apify's
MCP server, with a stable, documented schema, so an agent can watch a vendor
list and act on changes without knowing anything about page markup.

**Can I export to CSV or Excel?**
Yes. The dataset exports as CSV, Excel, JSON, NDJSON, or XML.

**Is this legal?**
These are public pages that vendors publish specifically to disclose their
subprocessors. The monitor reads them as published, on a polite schedule. It
does not log in, bypass access controls, or touch anything non-public.

# Actor input Schema

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

The vendor subprocessor / sub-processor pages to watch, e.g. https://stripe.com/legal/service-providers. You supply the URLs (you know your own vendors); the monitor watches exactly what you give it. Static server-rendered pages work best; JavaScript-only trust centers (SafeBase, Vanta-hosted) are reported as ok=false with error "js-rendered" in v1.

## `mode` (type: `string`):

How to compare a page. auto (default) parses a structured subprocessor list when one is found and falls back to whole-region text otherwise. list forces structured list parsing. text compares normalized page text only (no per-vendor added/removed).

## `notifyOn` (type: `string`):

list-change-only (default) reports a change only when the subprocessor list itself changes, ignoring page-chrome noise like a refreshed date. any-change also flags text changes elsewhere in the watched region. Only affects pages where a structured list is found.

## `selectorByUrl` (type: `object`):

Optional map of { "url": "cssSelector" } to pin the exact content region on a page, e.g. { "https://acme.com/subprocessors": "#subprocessor-table" }. Cuts false positives from surrounding page chrome. Changing a URL's selector re-establishes its baseline.

## `monitorName` (type: `string`):

Names this watchlist's saved state, held in a small key-value store in your account. Every run with the same name diffs each URL against the last run; the first run per URL establishes a baseline. Use different names for independent watchlists. Defaults to "default".

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

How many pages to fetch in parallel. Kept modest for politeness; raise only for large watchlists you own or have permission to scan.

## `proxyConfiguration` (type: `object`):

Recommended. Spreads requests across IPs and improves reach on rate-limited hosts.

## Actor input object example

```json
{
  "urls": [
    "https://stripe.com/legal/service-providers",
    "https://www.atlassian.com/legal/sub-processors"
  ],
  "mode": "auto",
  "notifyOn": "list-change-only",
  "selectorByUrl": {},
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `changes` (type: `string`):

All per-URL results as JSON. Append ?format=csv or ?format=ndjson to change the export format.

# 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 = {
    "urls": [
        "https://stripe.com/legal/service-providers",
        "https://www.atlassian.com/legal/sub-processors"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("virtual-constructs/subprocessor-change-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 = { "urls": [
        "https://stripe.com/legal/service-providers",
        "https://www.atlassian.com/legal/sub-processors",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("virtual-constructs/subprocessor-change-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://stripe.com/legal/service-providers",
    "https://www.atlassian.com/legal/sub-processors"
  ]
}' |
apify call virtual-constructs/subprocessor-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=virtual-constructs/subprocessor-change-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Subprocessor Change Monitor",
        "description": "Watch vendor subprocessor pages and get alerted when subprocessors are added, removed, or changed.",
        "version": "0.0",
        "x-build-id": "P2DBjN2SXZ7VjfKwJ"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/virtual-constructs~subprocessor-change-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-virtual-constructs-subprocessor-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/virtual-constructs~subprocessor-change-monitor/runs": {
            "post": {
                "operationId": "runs-sync-virtual-constructs-subprocessor-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/virtual-constructs~subprocessor-change-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-virtual-constructs-subprocessor-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "urls"
                ],
                "properties": {
                    "urls": {
                        "title": "Subprocessor page URLs",
                        "type": "array",
                        "description": "The vendor subprocessor / sub-processor pages to watch, e.g. https://stripe.com/legal/service-providers. You supply the URLs (you know your own vendors); the monitor watches exactly what you give it. Static server-rendered pages work best; JavaScript-only trust centers (SafeBase, Vanta-hosted) are reported as ok=false with error \"js-rendered\" in v1.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "mode": {
                        "title": "Diff mode",
                        "enum": [
                            "auto",
                            "list",
                            "text"
                        ],
                        "type": "string",
                        "description": "How to compare a page. auto (default) parses a structured subprocessor list when one is found and falls back to whole-region text otherwise. list forces structured list parsing. text compares normalized page text only (no per-vendor added/removed).",
                        "default": "auto"
                    },
                    "notifyOn": {
                        "title": "Notify on",
                        "enum": [
                            "list-change-only",
                            "any-change"
                        ],
                        "type": "string",
                        "description": "list-change-only (default) reports a change only when the subprocessor list itself changes, ignoring page-chrome noise like a refreshed date. any-change also flags text changes elsewhere in the watched region. Only affects pages where a structured list is found.",
                        "default": "list-change-only"
                    },
                    "selectorByUrl": {
                        "title": "Content selector per URL",
                        "type": "object",
                        "description": "Optional map of { \"url\": \"cssSelector\" } to pin the exact content region on a page, e.g. { \"https://acme.com/subprocessors\": \"#subprocessor-table\" }. Cuts false positives from surrounding page chrome. Changing a URL's selector re-establishes its baseline.",
                        "default": {}
                    },
                    "monitorName": {
                        "title": "Monitor name (managed state)",
                        "type": "string",
                        "description": "Names this watchlist's saved state, held in a small key-value store in your account. Every run with the same name diffs each URL against the last run; the first run per URL establishes a baseline. Use different names for independent watchlists. Defaults to \"default\"."
                    },
                    "maxConcurrency": {
                        "title": "Max concurrency",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "How many pages to fetch in parallel. Kept modest for politeness; raise only for large watchlists you own or have permission to scan.",
                        "default": 5
                    },
                    "proxyConfiguration": {
                        "title": "Proxy",
                        "type": "object",
                        "description": "Recommended. Spreads requests across IPs and improves reach on rate-limited hosts.",
                        "default": {
                            "useApifyProxy": true
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
