# Website Change Tracker - Pricing History (`datalayer/page-change-history`) Actor

When did a competitor last raise prices, rename a plan or swap analytics? The Internet Archive knows but only reports byte changes — measured on one page, 62% of those changed nothing a person would read. This extracts prices, headings and third-party scripts and diffs those instead.

- **URL**: https://apify.com/datalayer/page-change-history.md
- **Developed by:** [Datalayer](https://apify.com/datalayer) (community)
- **Categories:** SEO tools, Automation
- **Stats:** 2 total users, 1 monthly users, 77.8% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 content changes

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Website Change Tracker — Pricing & Copy History

When did your competitor last raise prices? Rename a plan? Swap analytics
providers? The Internet Archive has the answer and gives it away free. The
problem is that the archive cannot tell you when a page *changed* — only when
its bytes differed.

### The problem with archive diffs

The archive stores a content digest per capture, and `collapse=digest` drops
adjacent duplicates. It is tempting to call what survives "the times this page
changed." Measured on `stripe.com/pricing`, **January 2024 alone**:

| | |
|---|---|
| Byte-distinct captures | 9 |
| Consecutive comparisons | 8 |
| Comparisons where nothing readable changed | **5 (62%)** |
| Real content changes | 3 |

A rotated CSP nonce, a rebuilt asset filename, a new build hash or a fresh
session cookie all change the digest. None of them change the page.

This Actor extracts what a person would call the content — title, headings,
prices, rates, third-party scripts — and diffs **that**. Over the same two and
a half years, `stripe.com/pricing` has **588 byte-distinct captures** in the
archive. Sampled quarterly, 11 comparisons, every one a real change.

### What it finds

**Price changes**, with before and after:

```
2026-04-01  prices -$249.00, $349.00  +$0.02, $0.03, $0.05, $0.07
2026-08-01  prices -$0.02, $0.07      +$100,000.00
```

**Messaging shifts** — when positioning moved:

```
2025-01-01  title "Pricing & Fees | Stripe Official Site" -> "Pricing & Fees"
```

**Tech-stack switches** — the set of third-party script hosts, capture by
capture. When a competitor drops Segment for Amplitude, or adds a support-chat
vendor, it shows up here. Usually a bigger signal than the copy edit filed the
same day.

**New and retired plans** — H2 headings on a pricing page are the plan names.
`headingsAdded` catches a tier launching; `headingsRemoved` catches one being
quietly killed.

### Who uses this

- **Competitive intelligence** — a dated price history for every competitor,
  without having monitored them at the time
- **Sales and RevOps** — "they raised prices in April" is a usable talking point
- **Pricing teams** — what the market actually did, not what analysts said
- **Due diligence** — how often a company repositioned, and when
- **SEO** — when a page's title and headings were rewritten, next to your rank
  history

### Input

```json
{
  "urls": ["stripe.com/pricing", "vercel.com/pricing"],
  "startDate": "2024-01-01",
  "cadence": "monthly",
  "watchPricesOnly": true
}
```

| Field | Default | Notes |
|---|---|---|
| `urls` | `["stripe.com/pricing"]` | Bare domains or full URLs both work. |
| `startDate` | `2024-01-01` | `YYYY-MM-DD`. |
| `endDate` | — | `YYYY-MM-DD`. |
| `cadence` | `monthly` | `all` when you need the exact day a price moved. |
| `maxSnapshotsPerUrl` | `40` | Ceiling per page. |
| `changesOnly` | `true` | Off shows the comparisons that prove nothing moved. |
| `watchPricesOnly` | `false` | Narrow to currency changes only. |
| `includeSnapshots` | `false` | Emit the extracted content of every capture. |
| `includeText` | `false` | Attach visible page text to snapshot rows. |

### Output

**`change`** — one row per real change:

```json
{
  "type": "change",
  "url": "stripe.com/pricing",
  "changedBetween": "2026-01-01T14:10:14.000Z -> 2026-04-01T20:32:16.000Z",
  "changeTypes": ["pricing", "rates", "messaging"],
  "summary": "prices -$249.00, $349.00 +$0.02, $0.03; rates +3.5%; +8 heading(s); -9 heading(s)",
  "priceChanged": true,
  "pricesRemoved": ["$249.00", "$349.00"],
  "pricesAdded": ["$0.02", "$0.03", "$0.05", "$0.07"],
  "previousArchiveUrl": "https://web.archive.org/web/.../https://stripe.com/pricing",
  "archiveUrl": "https://web.archive.org/web/.../https://stripe.com/pricing"
}
```

Both archive links are on the row, so any finding can be eyeballed in one click.

**`url_summary`** — how much of the archive's churn was noise:

```json
{ "type": "url_summary", "distinctCapturesInArchive": 588,
  "snapshotsCompared": 12, "contentChanges": 11, "noiseRatio": 0 }
```

**`snapshot`** — the extracted content of each capture, when requested.

### Reliability

- Public, unauthenticated Internet Archive endpoints. Nothing is scraped from
  the live site, so no target is ever touched.
- Captures are fetched with the `id_` suffix, which returns the **original**
  archived bytes with no Wayback rewriting and no injected banner. Without it
  you extract the archive's markup instead of the site's.
- Those original bytes are frequently still gzip-compressed. Verified live: a
  Stripe capture arrives beginning `1f 8b`. Decompression is by magic-byte
  sniffing, not by trusting a header.
- The archive returns 503 under load more than most APIs and recovers in
  seconds — measured live, two empty responses then a clean 200. Retries use
  exponential backoff.
- One unreadable capture never fails the page; it lands in `RUN_SUMMARY`.

### Limits

- Only what the archive captured. Pages behind a login, geo-gated pages and
  robots-excluded pages have no history to read.
- Prices are read from the rendered HTML. A page that builds its pricing table
  in JavaScript after load will have been captured without it.
- Currency amounts and percentages are reported separately on purpose. A
  marketing page carries statistics ("326% growth") beside its real rates, so
  only currency movement is labelled `pricing`.
- Sampling picks the **first** capture in each period. Two changes inside one
  month at monthly cadence read as one. Use `all` for exact dates.
- Amounts inside `<script>` and `<style>` blocks are excluded — a JS string
  literal is not a published price.

# Actor input Schema

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

Pages to trace through history. Pricing pages, product pages and homepages work best. Paste bare domains or full URLs - stripe.com/pricing and https://stripe.com/pricing both work.

## `startDate` (type: `string`):

Earliest capture to consider, YYYY-MM-DD. Leave empty to go back as far as the archive has.

## `endDate` (type: `string`):

Latest capture to consider, YYYY-MM-DD.

## `cadence` (type: `string`):

How often to sample. A busy page can be captured thousands of times, and fetching every capture costs money without answering anything extra. Monthly suits most competitive tracking; use all when you need the exact day a price moved.

## `changesOnly` (type: `boolean`):

Skip comparisons where nothing a person would read has changed. Turn this off to see every comparison, including the ones that prove nothing moved.

## `watchPricesOnly` (type: `boolean`):

Narrow further to comparisons where a currency amount changed. Ignores copy rewrites and script changes.

## `includeSnapshots` (type: `boolean`):

Output the extracted content of every capture - title, headings, prices, third-party scripts - not just the changes between them.

## `includeText` (type: `boolean`):

Attach the page's visible text to each snapshot row, capped at 100,000 characters. Only applies when snapshot rows are on.

## `maxSnapshotsPerUrl` (type: `integer`):

Hard ceiling on how many captures are fetched for each page.

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

The Internet Archive rate-limits hard per IP, and every run on shared infrastructure looks like the same caller without a proxy. Runs go through Apify Proxy by default, drawing a fresh IP on each retry. Turn it off only when running from your own well-behaved IP.

## Actor input object example

```json
{
  "urls": [
    "stripe.com/pricing",
    "vercel.com/pricing"
  ],
  "startDate": "2024-01-01",
  "cadence": "monthly",
  "changesOnly": true,
  "watchPricesOnly": false,
  "includeSnapshots": false,
  "includeText": false,
  "maxSnapshotsPerUrl": 40,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every change to pricing, messaging or third-party scripts, with before and after.

## `summaries` (type: `string`):

How many real changes were found, and how much of the archive's churn was noise.

## `snapshots` (type: `string`):

The extracted content of each capture: title, headings, prices, vendors.

## `all` (type: `string`):

Changes, snapshots and summaries together.

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

Everything as a CSV download.

## `runSummary` (type: `string`):

Counts, plus any URL or capture that could not be read.

# 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": [
        "stripe.com/pricing",
        "vercel.com/pricing"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datalayer/page-change-history").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": [
        "stripe.com/pricing",
        "vercel.com/pricing",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("datalayer/page-change-history").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 '{
  "urls": [
    "stripe.com/pricing",
    "vercel.com/pricing"
  ]
}' |
apify call datalayer/page-change-history --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datalayer/page-change-history"
        }
    }
}

```

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/ygxwnS11HlLRfFZbk/builds/PFtZuTPo9paKEoqPd/openapi.json
