# SaaS Pricing Monitor — Plans, Change Alerts & Battlecard (`bovi/saas-pricing-monitor`) Actor

**Stateful SaaS pricing-page monitor.** Structured plan cards (name ↔ price ↔ period ↔ features), typed change events (`price_changed`, `plan_added`, `trial_changed`…) via KV diffing, plus a competitive battlecard. Keyword mode auto-discovers competitor pricing pages — no URL paste needed.

- **URL**: https://apify.com/bovi/saas-pricing-monitor.md
- **Developed by:** [Vitalii Bondarev](https://apify.com/bovi) (community)
- **Categories:** Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $47.53 / 1,000 pricing page snapshots

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## SaaS Pricing Monitor — Plans, Change Alerts & Battlecard

Monitor competitor SaaS pricing pages and get **structured plans**, **typed change alerts**, and a **decision-ready battlecard** — not a wall of page text.

Point it at pricing URLs, or just give it a **keyword** ("project management software") and it auto-discovers the top competitor pricing pages itself.

### Why this monitor

Most pricing trackers regex the page text and dump it. You get junk plan names, prices with no plan attached, and "changes" that are just copy edits. This actor extracts the actual **plan cards**:

```json
{
  "name": "Pro",
  "amount": 8.75,
  "currency": "USD",
  "period": "month",
  "per_seat": true,
  "is_free": false,
  "is_custom_price": false,
  "features": ["Unlimited message history", "Group video calls", "..."],
  "cta": "Get Started"
}
````

…and diffs them statefully between runs, so you get **typed events**, each one answerable with an action:

| Event | Fires when |
|---|---|
| `price_changed` | a plan's price moved — with old, new, % and direction |
| `plan_added` / `plan_removed` | the plan lineup changed |
| `trial_changed` | free trial appeared, disappeared, or changed length |
| `annual_discount_changed` | annual-billing discount appeared or changed |
| `billing_period_changed` | a plan switched monthly ↔ annual billing |
| `features_changed` | a plan's feature list materially changed |
| `page_changed_unparsed` | page changed but nothing structured moved (honest signal, never silent) |

The first run on a URL bootstraps a baseline (no change events, no surprises). Every scheduled run after that emits **only what changed**.

### Keyword auto-discovery (no URL paste)

Set `keyword` to your category and the actor finds the top competitor pricing pages via search (Google SERP when your account has the `GOOGLE_SERP` proxy group, otherwise Bing/DuckDuckGo), filters out review sites and "best X software" listicles, verifies each candidate is a live pricing page, and monitors them.

You can mix modes: pin your must-watch competitors in `pricingPageUrls` and let discovery fill the remaining slots up to `maxCompetitors`.

### Battlecard

Each run can emit one competitive battlecard across all monitored pages:

- cheapest & priciest paid entry points, median entry price
- free-tier and trial matrix per competitor
- full plan matrix (name, price, period, per-seat)
- everything that changed this run, summarized
- positioning notes ("X has no trial and no free tier — friction opportunity")

Pipe it to Slack or your CRM via a webhook on the run, or schedule the actor weekly and read the battlecard as your Monday pricing brief.

### Input

| Field | Type | Notes |
|---|---|---|
| `keyword` | string | SaaS category for auto-discovery. Optional if URLs given. |
| `pricingPageUrls` | array | Public pricing page URLs. Optional if keyword given. |
| `maxCompetitors` | integer | Cap on pages per run (default 10, max 25). |
| `emitBattlecard` | boolean | One battlecard per run (default true). |
| `eventTypes` | array | Filter which change events to emit. Empty = all. |
| `kvStoreName` | string | Named KV store for state. Keep it stable across runs. |
| `forceBrowser` | boolean | Force stealth-browser rendering (rarely needed — escalation is automatic). |
| `proxyConfiguration` | object | Apify Proxy; defaults to residential. |

### Output records

All records land in the default dataset with a `record_type` discriminator:

- `snapshot` — one per monitored page per run: plans, trial/discount signals, `parse_confidence`, `extraction_method`
- `change_event` — typed events as in the table above
- `battlecard` — one per run

Dataset views (Snapshots / Change events / Battlecard) are pre-configured in the Apify console.

### How it parses (and why it's reliable)

1. Fast static fetch with TLS impersonation; JS-shell pages **escalate automatically** to a stealth Camoufox browser render.
2. Plan cards are found **structurally** — repeated sibling card patterns in the DOM, not class-name guessing, not page-text regex. JSON-LD offers enrich the result when present.
3. Junk grids (FAQ accordions, feature-comparison tables) are rejected by per-grid quality scoring.
4. Every snapshot carries `parse_confidence` and `extraction_method`, and a degraded parse **never** fabricates plan-removal events — you get an honest `page_changed_unparsed` instead.

### Pricing events

| Event | Charged |
|---|---|
| `pricing-snapshot` | per pricing page successfully extracted |
| `pricing-change-event` | per typed change event emitted |
| `battlecard-report` | per battlecard generated |

Failed pages are never charged. Baselines are charged as snapshots only — a first run never bills change events.

### Scheduling

Typical setup: a weekly (or daily) schedule with your competitor set, a stable `kvStoreName`, and a webhook or integration reading the dataset. The actor is stateless between runs except for the named KV store, so you can fork configurations per product line by changing `kvStoreName`.

### Legal

Only public pricing pages — no login, no paywall, no personal data. You own your runs and the extracted data. Monitoring public competitor pricing is standard competitive-intelligence practice; this actor reads nothing a regular visitor's browser wouldn't.

# Actor input Schema

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

Describe the SaaS category and the actor finds the top competitor pricing pages itself — no URL paste needed. Example: 'project management software' or 'email marketing platform'. Combine with explicit URLs below; discovery fills the remaining slots up to Max competitors.

## `pricingPageUrls` (type: `array`):

Public SaaS pricing page URLs to monitor, e.g. \['https://slack.com/pricing', 'https://www.notion.com/pricing']. Optional when a keyword is set.

## `maxCompetitors` (type: `integer`):

Cap on the number of pricing pages monitored in one run (explicit URLs + auto-discovered). Each successfully extracted page = one pricing-snapshot charge.

## `emitBattlecard` (type: `boolean`):

Aggregate all monitored pages into one decision-ready battlecard record: cheapest paid entry, price ranges, free-tier/trial matrix, plan matrix, and this run's changes. One battlecard-report charge per run.

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

Filter which typed change events are emitted (and charged). Leave empty for ALL. Valid: price\_changed, plan\_added, plan\_removed, trial\_changed, annual\_discount\_changed, billing\_period\_changed, features\_changed, page\_changed\_unparsed.

## `kvStoreName` (type: `string`):

Named Apify Key-Value store that persists per-URL pricing state between runs. Keep the same name across scheduled runs to enable change detection — the first run on a URL bootstraps a baseline and emits no change events.

## `forceBrowser` (type: `boolean`):

Skip the fast static fetch and render every page in a stealth browser (Camoufox). Slower; use only if a target page returns empty plans via the default path. The actor normally escalates to the browser automatically when needed.

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

Apify Proxy settings. Defaults to US residential proxies — pinning one country keeps geo-localized pricing pages stable between runs (a rotating exit country flips currencies and re-baselines your monitors).

## Actor input object example

```json
{
  "keyword": "project management software",
  "maxCompetitors": 10,
  "emitBattlecard": true,
  "kvStoreName": "saas-pricing-monitor-state",
  "forceBrowser": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

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

Dataset containing Saas Pricing Monitor records (record\_type, domain, url, plan\_count, plans, has\_free\_tier, has\_free\_trial, trial\_days, has\_annual\_discount, annual\_discount\_pct, has\_usage\_based, has\_enterprise, currency, parse\_confidence, extraction\_method, extracted\_at, event\_type, plan\_name).

# 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 = {
    "keyword": "project management software",
    "maxCompetitors": 10,
    "kvStoreName": "saas-pricing-monitor-state",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bovi/saas-pricing-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 = {
    "keyword": "project management software",
    "maxCompetitors": 10,
    "kvStoreName": "saas-pricing-monitor-state",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("bovi/saas-pricing-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 '{
  "keyword": "project management software",
  "maxCompetitors": 10,
  "kvStoreName": "saas-pricing-monitor-state",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call bovi/saas-pricing-monitor --silent --output-dataset

```

## MCP server setup

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

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "SaaS Pricing Monitor — Plans, Change Alerts & Battlecard",
        "description": "**Stateful SaaS pricing-page monitor.** Structured plan cards (name ↔ price ↔ period ↔ features), typed change events (`price_changed`, `plan_added`, `trial_changed`…) via KV diffing, plus a competitive battlecard. Keyword mode auto-discovers competitor pricing pages — no URL paste needed.",
        "version": "0.1",
        "x-build-id": "BHjzNs3dnoEqIpfWK"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/bovi~saas-pricing-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-bovi-saas-pricing-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/bovi~saas-pricing-monitor/runs": {
            "post": {
                "operationId": "runs-sync-bovi-saas-pricing-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/bovi~saas-pricing-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-bovi-saas-pricing-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",
                "properties": {
                    "keyword": {
                        "title": "Keyword (auto-discover competitors)",
                        "type": "string",
                        "description": "Describe the SaaS category and the actor finds the top competitor pricing pages itself — no URL paste needed. Example: 'project management software' or 'email marketing platform'. Combine with explicit URLs below; discovery fills the remaining slots up to Max competitors."
                    },
                    "pricingPageUrls": {
                        "title": "Pricing page URLs",
                        "type": "array",
                        "description": "Public SaaS pricing page URLs to monitor, e.g. ['https://slack.com/pricing', 'https://www.notion.com/pricing']. Optional when a keyword is set.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "maxCompetitors": {
                        "title": "Max competitors per run",
                        "minimum": 1,
                        "maximum": 25,
                        "type": "integer",
                        "description": "Cap on the number of pricing pages monitored in one run (explicit URLs + auto-discovered). Each successfully extracted page = one pricing-snapshot charge.",
                        "default": 10
                    },
                    "emitBattlecard": {
                        "title": "Emit competitive battlecard",
                        "type": "boolean",
                        "description": "Aggregate all monitored pages into one decision-ready battlecard record: cheapest paid entry, price ranges, free-tier/trial matrix, plan matrix, and this run's changes. One battlecard-report charge per run.",
                        "default": true
                    },
                    "eventTypes": {
                        "title": "Change event types to emit",
                        "type": "array",
                        "description": "Filter which typed change events are emitted (and charged). Leave empty for ALL. Valid: price_changed, plan_added, plan_removed, trial_changed, annual_discount_changed, billing_period_changed, features_changed, page_changed_unparsed.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "kvStoreName": {
                        "title": "KV store name for state persistence",
                        "type": "string",
                        "description": "Named Apify Key-Value store that persists per-URL pricing state between runs. Keep the same name across scheduled runs to enable change detection — the first run on a URL bootstraps a baseline and emits no change events.",
                        "default": "saas-pricing-monitor-state"
                    },
                    "forceBrowser": {
                        "title": "Force browser rendering",
                        "type": "boolean",
                        "description": "Skip the fast static fetch and render every page in a stealth browser (Camoufox). Slower; use only if a target page returns empty plans via the default path. The actor normally escalates to the browser automatically when needed.",
                        "default": false
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Apify Proxy settings. Defaults to US residential proxies — pinning one country keeps geo-localized pricing pages stable between runs (a rotating exit country flips currencies and re-baselines your monitors).",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ],
                            "apifyProxyCountry": "US"
                        }
                    }
                }
            },
            "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
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
