# SaaS Pricing & Changelog Monitor (`mmdincer/saas-pricing-changelog-monitor`) Actor

Reads public SaaS pricing and changelog pages into plans, prices and release notes, then reports what changed since the last run. Not a sitemap diff. Obeys robots.txt. JavaScript-only tables are flagged, not guessed.

- **URL**: https://apify.com/mmdincer/saas-pricing-changelog-monitor.md
- **Developed by:** [Muhammet Mustafa Dincer](https://apify.com/mmdincer) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.90 / 1,000 page checkeds

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/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 & Changelog Monitor

Reads a public **pricing page** or **changelog** into structured plans and release notes, then, on the next run, reports what changed: a new plan, a price move, a feature added or removed, a new changelog entry.

A sitemap monitor tells you a URL was added. This Actor tells you the **Pro plan went from $20/month to $25/month**, or that a changelog gained "Lifecycle triggers for automations" on 2026-11-04.

### What you get

| | |
|---|---|
| **Does** | Reads plans (name, price, currency, period, features) and changelog entries (date, title, summary). Compares them with the previous run |
| **Input** | Pricing, changelog or features URLs, or a bare domain. Up to 25 |
| **Output** | One page row with the plans and entries. One change row per difference |
| **Change types** | `plan_added`, `plan_removed`, `price_changed`, `features_changed`, `changelog_added`, `changelog_removed`, `feature_added`, `feature_removed` |
| **Needs** | No login, no browser. Public https pages |
| **Pricing** | Pay per event. $0.0002 to start, $0.004 per page checked, $0.015 per reported change (free plan). A baseline run has no change charge. This file is the intended price; it is not applied until the Actor is published |
| **Does not** | Read a sitemap, crawl the whole site, log in, or guess prices from a JavaScript-only shell |

### Quick start

Leave the prefilled pages (`https://buttondown.com/pricing` and `https://ghost.org/changelog`) and click **Start**. Changes mode is on and **Only changed pages** is off. The first run returns one page row per URL with status `baseline_saved` and the plans or changelog entries. Run the same input again. A page that still matches comes back as status `unchanged`, with those plans or entries and no change row. Turn **Only changed pages** on to omit that unchanged page. A real change is its own row either way, with a one-line `summary`.

A bare domain such as `example.com` checks only `/pricing`, `/plans`, `/price`, `/changelog`, `/changes` and `/whats-new` (and `/features` if you turn that page type on). If you know the page, paste the URL. The Actor does not open `sitemap.xml`.

### How a pricing page is read

1. **Embedded product data** (`schema.org` `Offer` with a plan name and a price). This is the strongest reading. An `AggregateOffer` price range is ignored.
2. **Plan cards** in the HTML: a short price (`$20`, `€9`, `Free`, `Custom`) in the same block as a heading, plus the list items under that heading.
3. **A comparison table** whose header cells are plan names and whose cells contain prices, when the cards are not a clear group.

The method is stored on `extractionMethods` (`json-ld` or `html-plans`) and `extractionConfidence` is `high`, `medium` or `none`.

A page whose path is `/changelog` is not mined for stray prices. A page whose path is `/pricing` is not mined for stray dates.

### How a changelog is read

Entries come from a `<time>` date plus the nearest heading, newest first, up to **Max changelog entries** (default 25). A changelog page that has fewer than two entries in the HTML, and that links its own RSS or Atom feed, is read from that feed instead. The feed has to live on the same site and the link has to be a changelog.

Entries older than the oldest entry still in the window are the window sliding forward. They are not reported as removed.

### What the first run stores

Changes mode saves a baseline in a named key-value store, `saas-pricing-changelog-monitor-state`, under the **Watchlist name**. The same name on the next run compares against that baseline. Snapshot mode returns the current plans and entries and saves nothing.

The baseline stores plan names, amounts, currencies, periods and feature lists, plus changelog dates and titles. It does not store the raw HTML.

### When the page cannot be read

Pricing tables are often drawn by JavaScript. A plain request then returns an empty shell. The Actor does **not** invent plans from that shell. The row status is `javascript_shell` and `extractionWarning` explains it. Paste a server-rendered URL, or a changelog that publishes RSS.

If a later run cannot read a page that previously had a good baseline (a redesign, a challenge page that still returned HTML, a much smaller and different set of plans), the status is `extraction_failed`. The previous plans are **kept**. They are not reported as removed. `extraction_failed` is not a change charge.

Layout reading (`html-plans`) can break when the site redesigns. JSON-LD offers survive a visual redesign. The warning on an `html-plans` row says so.

### Changes

A scheduled watchlist that should stay quiet until something moves sets `onlyChanged` to true. That is not the quick-start prefill. The prefill leaves it off so a repeat still returns the page.

```json
{
    "pages": ["https://example.com/pricing", "https://example.com/changelog"],
    "pageTypes": ["pricing", "changelog"],
    "mode": "changes",
    "onlyChanged": true,
    "monitorName": "competitors"
}
```

A price change row looks like this:

```json
{
    "recordType": "change",
    "changeType": "price_changed",
    "url": "https://example.com/pricing",
    "planName": "Pro",
    "previousPriceText": "$20",
    "priceText": "$25",
    "previousAmount": 20,
    "amount": 25,
    "currency": "USD",
    "summary": "Pro price changed from $20 to $25."
}
```

A page row carries the full `plans` and `changelogEntries` arrays. Change rows carry only the difference.

`onlyChanged` set to true hides pages that match the baseline. It does not hide a page that could not be read. Left off, an unchanged page is still a page row: status `unchanged`, the plans or changelog entries from this run, `changed` false, and no change row. The saved baseline is the same either way. An omitted `onlyChanged` stays true, so an existing call that never set the field keeps the quiet watchlist.

Change types you turn off are not charged. The new reading is still saved, so a left-out change is not reported on a later run either. `extraction_failed` is the exception: the baseline is not replaced.

### Features pages

Turn on the **Features** page type for a marketing page that lists capabilities and has no prices. Headings become `features`. Later runs report `feature_added` and `feature_removed`. A pricing page's own feature bullets stay on the plan, as `features_changed`.

### Politeness

The Actor sends a named user agent, stays on the site you named (redirects off the site are dropped), and obeys `robots.txt` unless you turn that off. A disallowed page is not fetched and not charged. `Sitemap:` lines in robots.txt are ignored. Requests to a private, local or IP address are rejected. There is no browser and no login.

### Pricing

| Event | Free plan |
|---|---|
| Actor start | $0.0002 |
| Page checked | $0.004 |
| Change reported | $0.015 |

A page that is blocked, missing, or disallowed by robots.txt is not charged. A baseline run charges a page check and no changes. Checking two pages every day and getting one price change a week is about $0.26 in page checks plus $0.015 for the change, before the start event.

### Local sample

On 5 November 2026 a local run read three public pages (`maxChangelogEntries` 8, watchlist `live-demo`, **Only changed pages** on). Each came back `baseline_saved`. A second run of the same input compared the saved baseline and reported all three `unchanged`, with no new dataset rows and no change charge. That empty second dataset is the only-changed setting. The prefilled Start leaves it off, so those same unchanged pages would each be a page row.

Buttondown pricing came from embedded product data (`json-ld`, confidence `high`):

| Plan | Price |
|---|---|
| Free | Free (up to 100 subscribers) |
| Basic | $9 (up to 1,000 subscribers) |
| Standard | $29 (up to 5,000 subscribers) |
| Professional | $79 (up to 10,000 subscribers) |
| Advanced | $139 (up to 20,000 subscribers) |

Ghost's changelog came from the HTML (`html-changelog`, confidence `high`). The newest of the eight kept entries were 2026-11-04 "Lifecycle triggers for automations", 2026-10-21 "Stripe Checkout customization" and 2026-10-15 "Tier-specific welcome sequences".

Umami pricing came from the plan cards (`html-plans`). Hobby Free/month, Pro $20/month, Business $200/month, Enterprise Custom, each with its feature list. That row carries the layout warning: a redesign can break the reading, and a later failed read keeps the previous baseline instead of reporting every plan as removed.

```json
{
    "recordType": "page",
    "url": "https://umami.is/pricing",
    "pageType": "pricing",
    "status": "baseline_saved",
    "summary": "Baseline saved: 4 plans.",
    "extractionMethods": ["html-plans"],
    "extractionConfidence": "high",
    "plans": [
        { "name": "Hobby", "priceText": "Free/month", "amount": 0, "currency": "USD", "period": "month" },
        { "name": "Pro", "priceText": "$20/month", "amount": 20, "currency": "USD", "period": "month" },
        { "name": "Business", "priceText": "$200/month", "amount": 200, "currency": "USD", "period": "month" },
        { "name": "Enterprise", "priceText": "Custom", "amount": null, "currency": null, "period": null }
    ]
}
```

# Changelog

This Actor's version history is a separate document: https://apify.com/mmdincer/saas-pricing-changelog-monitor/changelog.md

# Actor input Schema

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

Public pricing, changelog or features pages, one per line. A full URL such as <code>https://buttondown.com/pricing</code> is fetched as that page. A domain such as <code>buttondown.com</code> checks only the usual paths for the page types you selected. Up to 25 per run.

## `pageTypes` (type: `array`):

What to read. <b>Pricing</b> is plans and prices. <b>Changelog</b> is dated release notes. <b>Features</b> is a feature list on a page that has no plan table. A URL whose path is <code>/pricing</code> is not also mined for stray changelog dates, and a <code>/changelog</code> path is not mined for stray prices.

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

<b>Changes</b> compares each page with the previous run of the same watchlist. The first run only saves a baseline. <b>Snapshot</b> returns the plans and changelog entries as they are now and does not save anything.

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

In changes mode, skip dataset rows for pages that match the previous run. The first run still returns the baseline. Pages that cannot be read are still returned. The quick start leaves this off, so a repeat returns the page with status unchanged and the plans or changelog that were read, and no change row. Turn it on when a scheduled watchlist should stay empty until something changes.

## `changeTypes` (type: `array`):

Which changes become rows. Changes you leave out are not charged. The new reading is still saved, so a left-out change is not reported on a later run either. <b>extraction_failed</b> means the page could not be read and the previous baseline was kept.

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

Name of the saved baseline (changes mode). Runs with the same name compare against each other. Use different names to keep separate watchlists, for example <code>competitors</code> and <code>ours</code>.

## `maxChangelogEntries` (type: `integer`):

How many of the newest changelog entries to keep per page. Older entries that slide out of this window are not reported as removed.

## `respectRobotsTxt` (type: `boolean`):

Read each site's <code>robots.txt</code> and skip pages it disallows. Recommended. Skipped pages are not charged. Sitemap addresses inside robots.txt are ignored.

## `requestsPerSecond` (type: `number`):

How fast pages are requested from one site. The Actor also waits for a crawl-delay of up to 10 seconds.

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

Optional. Direct requests work for most public marketing pages. Turn on Apify Proxy only if a site refuses requests from the Apify platform. Proxy traffic is billed by Apify separately.

## Actor input object example

```json
{
  "pages": [
    "https://buttondown.com/pricing",
    "https://ghost.org/changelog"
  ],
  "pageTypes": [
    "pricing",
    "changelog"
  ],
  "mode": "changes",
  "onlyChanged": false,
  "changeTypes": [
    "plan_added",
    "plan_removed",
    "price_changed",
    "features_changed",
    "changelog_added",
    "changelog_removed",
    "feature_added",
    "feature_removed",
    "extraction_failed"
  ],
  "monitorName": "default",
  "maxChangelogEntries": 25,
  "respectRobotsTxt": true,
  "requestsPerSecond": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Plan, price, feature and changelog differences since the previous run.

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

One row per checked page: plans, prices and changelog entries, or why the page was skipped.

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

Pages checked, changes reported, per-page status, and whether the run stopped at the spending limit.

# 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": [
        "https://buttondown.com/pricing",
        "https://ghost.org/changelog"
    ],
    "pageTypes": [
        "pricing",
        "changelog"
    ],
    "mode": "changes",
    "onlyChanged": false,
    "changeTypes": [
        "plan_added",
        "plan_removed",
        "price_changed",
        "features_changed",
        "changelog_added",
        "changelog_removed",
        "feature_added",
        "feature_removed",
        "extraction_failed"
    ],
    "monitorName": "default",
    "maxChangelogEntries": 25,
    "respectRobotsTxt": true,
    "requestsPerSecond": 1,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("mmdincer/saas-pricing-changelog-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 = {
    "pages": [
        "https://buttondown.com/pricing",
        "https://ghost.org/changelog",
    ],
    "pageTypes": [
        "pricing",
        "changelog",
    ],
    "mode": "changes",
    "onlyChanged": False,
    "changeTypes": [
        "plan_added",
        "plan_removed",
        "price_changed",
        "features_changed",
        "changelog_added",
        "changelog_removed",
        "feature_added",
        "feature_removed",
        "extraction_failed",
    ],
    "monitorName": "default",
    "maxChangelogEntries": 25,
    "respectRobotsTxt": True,
    "requestsPerSecond": 1,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("mmdincer/saas-pricing-changelog-monitor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "pages": [
    "https://buttondown.com/pricing",
    "https://ghost.org/changelog"
  ],
  "pageTypes": [
    "pricing",
    "changelog"
  ],
  "mode": "changes",
  "onlyChanged": false,
  "changeTypes": [
    "plan_added",
    "plan_removed",
    "price_changed",
    "features_changed",
    "changelog_added",
    "changelog_removed",
    "feature_added",
    "feature_removed",
    "extraction_failed"
  ],
  "monitorName": "default",
  "maxChangelogEntries": 25,
  "respectRobotsTxt": true,
  "requestsPerSecond": 1,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call mmdincer/saas-pricing-changelog-monitor --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/KUOgUIIhxP2jxY1Wp/builds/II2bcqJCxNEdfIAa4/openapi.json
