# SaaS Pricing Change Radar (`serendy/saas-pricing-change-radar`) Actor

Track competitor SaaS pricing with evidence-backed plans, published prices, feature and quota change signals, and private cross-run history. Export JSON, CSV and Markdown. No LLM API key needed.

- **URL**: https://apify.com/serendy/saas-pricing-change-radar.md
- **Developed by:** [Luqian Lin](https://apify.com/serendy) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 successful pricing page analyses

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

## SaaS Pricing Change Radar

**Track competitor pricing. Understand what changed.**

Monitor public SaaS pricing pages, extract evidenced plans and published prices, keep private cross-run snapshots, and spot pricing, feature, quota and trial changes. Built for SaaS founders, product managers, product marketing teams and competitive researchers.

No LLM subscription or extra API key. No separate dashboard or login. Run on demand or reuse an Apify Task with a daily or weekly Schedule.

### Quick start

1. Keep the example or add a product name and a public pricing URL.
2. Click **Start**. The first successful observation establishes a baseline.
3. Run again with the **same URLs, monitor name and rendering settings** to compare against the latest successful observation.
4. Open the JSON report, flat CSV, Markdown report or dataset in **Output**.

```json
{
  "pages": [
    {"product": "Linear", "url": "https://linear.app/pricing"},
    {"product": "Buffer", "url": "https://buffer.com/pricing"}
  ],
  "monitorName": "competitors",
  "renderMode": "browser"
}
```

Up to 20 URLs per run, processed sequentially. Exact duplicate normalized URLs are analyzed once. The default browser mode renders JavaScript, waits briefly for loading and excludes hidden elements. It observes the default English desktop state without clicking billing, currency, region or usage controls.

### What you get

- Product name, requested and resolved source URLs, UTC observation time and collection method.
- Plan names, directly observed amounts, currency when explicit, billing period, seat/unit text and annual-billing labels.
- Free and unpublished/contact-sales plans. Unknown values stay `null`, `unknown` or `not_public`; no annual conversions or inferred enterprise quotes.
- Observed card feature labels with per-item inclusion status and evidence, quota/limit text, trial and discount excerpts, contact-sales links where present.
- Per-plan source excerpts and source URLs; each offer has its own evidence. A source HTML hash makes collection drift inspectable.
- Private history across separate runs. Each monitored URL retains the latest 30 successful observations.
- JSON results, a flat CSV with one row per plan/offer, Markdown and a monitoring summary.

A dollar sign alone does **not** establish USD. The Actor leaves currency unknown unless the page identifies the relevant dollar currency. It does not convert currencies.

### Understand changes and confidence

`comparisonStatus` is `baseline`, `no_change`, `changes_detected`, `incomparable` or `unavailable`.

Published-price increases/decreases compare the same normalized plan name and offer context: currency or symbol, billing period, unit, billing model and billed cadence. Output includes the before/after offers and their evidence, previous/current observation times and confidence.

Added/removed plans, feature wording changes, quota changes, trial/discount changes and changed billing contexts are **review candidates** (`reviewRequired: true`). Layout changes, A/B tests and wording can resemble commercial changes. These candidates are not verified claims that an entitlement was removed. Whitespace and case changes are ignored. Renamed plans may appear as addition/removal candidates. Confidence values are rule-based quality indicators, not calibrated probabilities.

The time window is when a change was **observed**, not the vendor's effective change date. A baseline is never presented as historical change. Failed/empty extractions do not replace the previous successful snapshot. Changing the rendering or card selector makes the next comparison `incomparable`.

Feature names in `features` are observed card labels. `featureDetails[].inclusion` is `included` or `excluded` only when an explicit accessible text label supports it; otherwise it is **unknown**. Unlabelled check/cross icons are not interpreted as proof of an entitlement. The warning `unverified_feature_inclusion` calls this out. Review source pages for contractual feature availability.

### History and privacy

History is stored in a named Apify key-value store under the **executing account**, using authenticated user and Actor identifiers plus `monitorName`. Users cannot supply another account identity or select another history store. The Actor requests limited permissions. No embedded developer token or shared external database is used.

Different monitors within your account have separate histories. Account members with access to your storage can access that account's reports. Do not put credentials in product names or page URLs. Reports are subject to your Apify storage retention and access policies. Delete the named store shown in `SUMMARY.historyStoreId` to reset history; deleting it removes the baseline. Only the latest 30 snapshots per URL are retained, not an unlimited archive.

A server-side lock prevents concurrent updates to the same monitor. A busy monitor run fails clearly with no product events. Its lock releases on completion or expires after interruption. For best results, schedule non-overlapping runs. Resurrected runs do not reanalyze or charge again; start a fresh run to collect new evidence.

### Schedule and notifications

Save the input as an **Apify Task** and create a daily or weekly **Schedule**. Reuse its monitor name and source URLs. A schedule is created only when you enable it; installing this Actor does not start recurring costs.

Optionally set `changesWebhookUrl` to your public HTTPS endpoint. The Actor sends JSON only when it observes change signals, including review candidates. Baseline/no-change runs send nothing. The payload contains `event: "pricing.changes_observed"`, run ID, monitor name and per-page changes. Treat webhook URLs containing secrets as private input. Delivery is bounded to one attempt; inspect `WEBHOOK-RESULT` for errors and use native Apify webhooks if you need platform-managed retry delivery.

Alternatively configure a native `ACTOR.RUN.SUCCEEDED` webhook and fetch `SUMMARY` or `REPORT`, then filter `changeCount > 0`. Run success can include failed pages: check individual `status`, `error` and `warnings`.

### Pricing

The launch price is **$0.01 per successful pricing page analysis** ($10 per 1,000). Standard Store platform usage is included; there is no separate LLM or proxy fee. The live Store **Pricing** tab is authoritative. One custom `pricing-page-analysis` event corresponds to one successfully persisted, valid page analysis, including baseline and no-change observations. Multiple plans or changes on that page do not create extra events.

Error pages, pages with no verifiable pricing cards, budget skips and invalid inputs do not trigger that product event. A valid analysis may contain unknown fields or partial card coverage; inspect the public examples and warnings before scheduling at scale. There is no startup event or automatic dataset-row event.

Use **Max cost per run** to limit paid results. Apify's free-plan prepaid credit can be used to test the example without a separate LLM subscription; this is platform credit, not an unlimited free tier. Execution and build costs for your own Actor development are governed by your Apify account plan. Standard Store runs include platform usage in the event price. Standby mode is not supported.

### Output and API

The default dataset has one JSON row per input page, including diagnostics. `REPORT` contains the same rows under `pages`; `REPORT-CSV` flattens plans/offers for Excel and BI; `REPORT-MARKDOWN` is the human-readable report. Nested arrays in CSV are JSON strings. JSON best preserves the evidence structure.

```javascript
import {ApifyClient} from 'apify-client';
const client = new ApifyClient({token: process.env.APIFY_TOKEN});
const run = await client.actor('Serendy/saas-pricing-change-radar').call({
  pages: [{product: 'Linear', url: 'https://linear.app/pricing'}],
  monitorName: 'competitors', renderMode: 'browser'
});
const {items} = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

`billable` means the persisted analysis qualifies for a product event. Actual charges depend on the current pricing, remaining budget and Apify counters. It does not prove a customer payment.

### Live output example

This shortened example was collected from Linear on October 19, 2026. It is an observed public page, not a fictional price-change scenario. The full output includes plan and quota evidence.

```json
{
  "product": "Linear",
  "sourceUrl": "https://linear.app/pricing",
  "collectedAt": "2026-10-19T11:54:10.141Z",
  "status": "success",
  "comparisonStatus": "no_change",
  "plans": [
    {
      "name": "Basic",
      "offers": [
        {
          "amount": 10,
          "currency": null,
          "symbol": "$",
          "period": "month",
          "unit": "per user",
          "billingModel": "per_seat",
          "billed": "yearly",
          "evidence": {
            "url": "https://linear.app/pricing",
            "quote": "$10 per user/month Billed yearly Get started"
          }
        }
      ],
      "features": [
        "All Free features +",
        "5 teams",
        "Unlimited issues",
        "Unlimited file uploads",
        "Admin roles"
      ]
    }
  ],
  "changes": [],
  "warnings": [
    "ambiguous_dollar_currency",
    "default_visible_state_only",
    "card_coverage_not_guaranteed",
    "common_plan_names_only_unless_selector_supplied"
  ]
}
```

### Scope and limitations

This is a conservative DOM-based MVP. Automatic detection accepts common plan names (for example Free, Starter, Pro, Business, Team and Enterprise) on separate cards with headings and visible prices. For unusual plan names, supply a card selector. Add-ons and comparison-table-only offers are outside automatic detection. Arbitrary pages are not guaranteed: comparison matrices, calculator-only pricing, iframe/shadow-DOM content, delayed states and unusual markup can return partial or no results. A valid extracted subset does not prove complete page coverage. Feature extraction focuses on card lists; long comparison tables are not reconstructed.

For custom card layouts, provide `cardSelector` on a page entry: a CSS selector matching each plan card containing an h2–h6 plan heading. Check the sample output before recurring use. HTTP-only mode may include CSS-hidden variants and is intended for advanced users who have checked the page structure. Automatic mode uses HTTP first and opens a browser only if no valid cards were found.

Source network errors, HTTP 4xx/5xx, access controls, oversized responses, missing cards and browser loading failures are reported. Captchas, login walls and access restrictions are not bypassed. No residential proxies, LLM or third-party enrichment services are used. Public web pages can change, vary by region, or restrict automated access. Enter only URLs you are permitted to access.

Pricing evidence is for research and review, not a vendor quote, purchasing guarantee or proof of all commercial terms. Please review source links before making a decision.

### Support

Open an issue in the Store **Issues** tab with the source URL, run ID, expected plan and a redacted input. Never share API tokens or private webhook secrets.

# Actor input Schema

## `pages` (type: `array`):

Product name, public URL, and optional CSS cardSelector for custom layouts. Exact duplicate URLs are analyzed once.

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

History namespace within your own Apify account. Keep it unchanged in scheduled runs. Different accounts are isolated.

## `renderMode` (type: `string`):

Browser waits for JavaScript and removes hidden elements. Toggles, currencies and region selectors are not clicked.

## `changesWebhookUrl` (type: `string`):

Public HTTPS endpoint receiving observed changes. Treat input as private if the URL embeds a secret. No calls on baseline or no-change runs. Delivery failure is reported without retry.

## Actor input object example

```json
{
  "pages": [
    {
      "product": "Linear",
      "url": "https://linear.app/pricing"
    },
    {
      "product": "Buffer",
      "url": "https://buffer.com/pricing"
    }
  ],
  "monitorName": "default",
  "renderMode": "browser"
}
```

# Actor output Schema

## `pages` (type: `string`):

No description

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

No description

## `markdown` (type: `string`):

No description

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

No description

## `summary` (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 = {
    "pages": [
        {
            "product": "Linear",
            "url": "https://linear.app/pricing"
        },
        {
            "product": "Buffer",
            "url": "https://buffer.com/pricing"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("serendy/saas-pricing-change-radar").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 = { "pages": [
        {
            "product": "Linear",
            "url": "https://linear.app/pricing",
        },
        {
            "product": "Buffer",
            "url": "https://buffer.com/pricing",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("serendy/saas-pricing-change-radar").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 '{
  "pages": [
    {
      "product": "Linear",
      "url": "https://linear.app/pricing"
    },
    {
      "product": "Buffer",
      "url": "https://buffer.com/pricing"
    }
  ]
}' |
apify call serendy/saas-pricing-change-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,serendy/saas-pricing-change-radar"
        }
    }
}
```

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/xrbS1Vok58lPnOWIg/builds/0SAqX6jZi9M5pMC2S/openapi.json
