# SERP Rank Change Detector (`ceddl/serp-rank-change-detector`) Actor

Compare baseline and current search-result exports to find rank gains, losses, entries, exits, URL swaps, and duplicate result rows without running another search scrape.

- **URL**: https://apify.com/ceddl/serp-rank-change-detector.md
- **Developed by:** [Cedric Günther](https://apify.com/ceddl) (community)
- **Categories:** SEO tools, Automation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 serp snapshots compareds

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

## SERP Rank Change Detector

Compares two supplied search-result snapshots and returns deterministic rank gains, losses, entries, exits, URL swaps, and duplicate-result evidence without querying a search engine. It is built for SEO operations teams, search-data pipeline owners, agencies comparing controlled SERP exports. The main result is structured, deterministic evidence that can be consumed from the default dataset or an Apify automation.

### When to use this Actor

- Monitor ranking gains and losses between consistently collected snapshots.
- Detect new entries, exits, and unambiguous title-based URL swaps.
- Find duplicate canonical URLs before storing or reporting SERP data.

### How it works

- Validate that both bounded snapshots share an explicit query, engine, locale, and device context.
- Canonicalize HTTP(S) URLs and compare exact result identities and ranks.
- Emit deterministically sorted change, duplicate, and summary records before the completion event is charged.

The Actor validates only the declared product contract. It does not infer facts outside the supplied data or claim outcomes that the source material cannot prove.

### Quick start

1. Open the Actor's **Input** tab or create a Task from one of the public examples.
2. Paste or adapt this bounded example.
3. Click **Start** and inspect the default dataset plus the output links shown on the run page.

```json
{
  "comparisonId": "apify-actors-weekly",
  "query": "apify actors",
  "engine": "google",
  "locale": "en-US",
  "device": "desktop",
  "baseline": [
    {
      "rank": 1,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    },
    {
      "rank": 2,
      "url": "https://example.org/store",
      "title": "Actor Store"
    }
  ],
  "current": [
    {
      "rank": 1,
      "url": "https://example.org/store",
      "title": "Actor Store"
    },
    {
      "rank": 3,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    }
  ]
}
```

Expected result: rank-change records for the gain and loss plus one serp-summary record.

### Input

The quick-start example is intentionally small. These are the material controls; the Input tab remains authoritative for the complete current schema.

| Field | Purpose and format | Default | Important bounds or interaction |
|---|---|---|---|
| `comparisonId` | Stable caller-defined label copied into every result for routing and repeat-run reconciliation. | No implicit default | minimum length 1; maximum length 120 |
| `query` | Exact search query represented by both supplied snapshots; the Actor does not execute the search. | No implicit default | minimum length 1; maximum length 500 |
| `engine` | Search engine label that must match the provenance of both snapshots. | No implicit default | minimum length 1; maximum length 80 |
| `locale` | Locale context shared by both snapshots so rankings from unlike markets are not silently compared. | No implicit default | minimum length 1; maximum length 80 |
| `device` | Device class shared by both snapshots, such as desktop or mobile according to the schema enum. | No implicit default | minimum length 1; maximum length 80 |
| `baseline` | Earlier ordered SERP rows with explicit ranks and URLs. | No implicit default | maximum items 1000 |
| `current` | Later ordered SERP rows compared with baseline under the same query context. | No implicit default | maximum items 1000 |
| `maxResultsPerSnapshot` | Optional lower cap applied independently to each snapshot before comparison. | No implicit default | minimum 1; maximum 1000 |

Unknown top-level fields and invalid field combinations fail validation rather than being guessed.

### Output

The default dataset contains typed records. The run's Output tab links the dataset and any key-value-store reports declared by the current output schema.

| Field | Meaning |
|---|---|
| `recordType` | Discriminates rank-change, duplicate-result, and serp-summary records. |
| `comparisonId` | Current dataset field. |
| `changeType` | Current dataset field. |
| `url` | Current dataset field. |
| `previousUrl` | Current dataset field. |
| `title` | Current dataset field. |
| `baselineRank` | Current dataset field. |
| `currentRank` | Current dataset field. |
| `delta` | Signed rank movement under the Actor's documented ordering semantics. |
| `snapshot` | Current dataset field. |
| `canonicalUrl` | Current dataset field. |
| `ranks` | All source ranks participating in a duplicate canonical-URL group. |
| `engineVersion` | Current dataset field. |

Representative current-schema dataset item:

```json
{
  "recordType": "rank-change",
  "comparisonId": "apify-actors-weekly",
  "changeType": "GAINED",
  "url": "https://example.org/store",
  "title": "Actor Store",
  "baselineRank": 2,
  "currentRank": 1,
  "delta": 1,
  "engineVersion": "1.0.0"
}
```

Equivalent valid snapshots succeed with a serp-summary record and zero rank-change records.

### Pricing and billing

This Actor uses `PAY_PER_EVENT`; platform usage is included in event prices. A charge is eligible only after the billable unit described below is durably completed. Validation failures and the non-billable failure classes in the product contract do not emit the custom completion event. The current live policy uses the same event price at every Store tier; no tier discount is active. The Apify **Pricing** tab is authoritative if a later approved pricing change takes effect.

| Event | What triggers it | FREE | BRONZE | SILVER | GOLD | PLATINUM | DIAMOND |
|---|---|---:|---:|---:|---:|---:|---:|
| `serp-snapshots-compared` | One baseline/current SERP snapshot pair converted into durable rank-change evidence. | $0.02000000 | $0.02000000 | $0.02000000 | $0.02000000 | $0.02000000 | $0.02000000 |
| `apify-actor-start` | Platform-managed Actor start event. | $0.00005000 | $0.00005000 | $0.00005000 | $0.00005000 | $0.00005000 | $0.00005000 |

The Actor does not have Task-specific prices: public Tasks use this same live Actor pricing. Third-party costs are not implied; see the data and security section for external services actually contacted.

### Limits and bounds

- Each snapshot is capped at 1,000 rows; maxResultsPerSnapshot may select a lower bound.
- Only supplied snapshots are compared; no search engine, proxy, browser, or SERP API is called.
- URL swaps require an unambiguous title match and are not guessed when candidates conflict.

These are product-facing limits, not targets. Use smaller inputs when you need faster feedback or simpler evidence.

### Failure and edge-case behavior

- Invalid context, ranks, URLs, duplicate rank positions, or over-limit input fails closed before the custom event.
- An empty supplied snapshot is valid and can produce truthful entries or exits.
- Duplicate canonical URLs are represented explicitly instead of silently selecting one rank.

Operationally:

- Collect both snapshots with comparable engine, locale, device, personalization, and timing controls.
- A rank change is observation evidence only and does not imply traffic, conversion, or revenue impact.

### Use with Tasks and automation

Public Tasks provide reusable saved inputs for distinct supported workflows. Start with the closest Example Task, review its visible fields and scope caveat, then save your own Task for schedules or repeated runs. Do not treat an Example Task as evidence that unsupported behavior exists.

- [Compare two SERP ranking exports](https://apify.com/ceddl/serp-rank-change-detector/examples/compare-serp-rankings): Measure entries, exits, gains, losses, URL swaps, and duplicate rows between comparable safe snapshots.
- [Monitor SERP rank gains and losses](https://apify.com/ceddl/serp-rank-change-detector/examples/monitor-serp-rank-gains-and-losses): Measure ranking gains and losses for URLs between two exported SERP snapshots.
- [Detect SERP entries, exits, and URL swaps](https://apify.com/ceddl/serp-rank-change-detector/examples/detect-serp-entries-exits-and-url-swaps): Detect URLs entering or leaving an exported SERP and identify rank-slot URL swaps.

### Integration and API usage

Every saved Task can be started manually, through the Apify API, or from an Apify schedule. Run-completion webhooks can notify a downstream system after output is durable. Actor-to-Actor calls should consume the typed dataset/output links instead of scraping the Store page.

- Schedule a saved comparison Task after upstream SERP collection completes.
- Use recordType, changeType, and canonicalUrl for downstream dashboards or alerts.

No third-party integration is claimed unless it is named above and supported by the current product contract.

### Data, privacy, and security

- Queries, URLs, titles, and evidence are stored in the run's Apify storages according to account retention settings.
- No external service is contacted and no search credentials are required.

Set Apify storage retention and access according to the sensitivity of your inputs and outputs. This documentation does not create legal, privacy, compliance, or security certification.

### Support and known limitations

- The Actor does not collect live SERPs, forecast traffic, or explain why a ranking changed.
- Different collection methods or personalization can make snapshot comparisons misleading even when the input is structurally valid.

For support, use the [Actor Issues page](https://apify.com/ceddl/serp-rank-change-detector/issues). Include the run ID, a minimal reproducible input with sensitive values removed, the failing record or error code, and what you expected. Do not post credentials, private source files, customer data, or full confidential payloads.

# Actor input Schema

## `comparisonId` (type: `string`):

Input field comparisonId.

## `query` (type: `string`):

Input field query.

## `engine` (type: `string`):

Input field engine.

## `locale` (type: `string`):

Input field locale.

## `device` (type: `string`):

Input field device.

## `baseline` (type: `array`):

Input field baseline.

## `current` (type: `array`):

Input field current.

## `maxResultsPerSnapshot` (type: `integer`):

Input field maxResultsPerSnapshot.

## Actor input object example

```json
{
  "comparisonId": "sample-serp",
  "query": "apify actors",
  "engine": "google",
  "locale": "en-US",
  "device": "desktop",
  "baseline": [
    {
      "rank": 1,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    },
    {
      "rank": 2,
      "url": "https://example.org/store",
      "title": "Actor Store"
    }
  ],
  "current": [
    {
      "rank": 1,
      "url": "https://example.org/store",
      "title": "Actor Store"
    },
    {
      "rank": 3,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    }
  ]
}
```

# Actor output Schema

## `evidence` (type: `string`):

No description

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

No description

## `reports` (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 = {
    "comparisonId": "sample-serp",
    "query": "apify actors",
    "engine": "google",
    "locale": "en-US",
    "device": "desktop",
    "baseline": [
        {
            "rank": 1,
            "url": "https://example.com/guide",
            "title": "Actor Guide"
        },
        {
            "rank": 2,
            "url": "https://example.org/store",
            "title": "Actor Store"
        }
    ],
    "current": [
        {
            "rank": 1,
            "url": "https://example.org/store",
            "title": "Actor Store"
        },
        {
            "rank": 3,
            "url": "https://example.com/guide",
            "title": "Actor Guide"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ceddl/serp-rank-change-detector").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 = {
    "comparisonId": "sample-serp",
    "query": "apify actors",
    "engine": "google",
    "locale": "en-US",
    "device": "desktop",
    "baseline": [
        {
            "rank": 1,
            "url": "https://example.com/guide",
            "title": "Actor Guide",
        },
        {
            "rank": 2,
            "url": "https://example.org/store",
            "title": "Actor Store",
        },
    ],
    "current": [
        {
            "rank": 1,
            "url": "https://example.org/store",
            "title": "Actor Store",
        },
        {
            "rank": 3,
            "url": "https://example.com/guide",
            "title": "Actor Guide",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("ceddl/serp-rank-change-detector").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 '{
  "comparisonId": "sample-serp",
  "query": "apify actors",
  "engine": "google",
  "locale": "en-US",
  "device": "desktop",
  "baseline": [
    {
      "rank": 1,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    },
    {
      "rank": 2,
      "url": "https://example.org/store",
      "title": "Actor Store"
    }
  ],
  "current": [
    {
      "rank": 1,
      "url": "https://example.org/store",
      "title": "Actor Store"
    },
    {
      "rank": 3,
      "url": "https://example.com/guide",
      "title": "Actor Guide"
    }
  ]
}' |
apify call ceddl/serp-rank-change-detector --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ceddl/serp-rank-change-detector"
        }
    }
}
```

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/D5GdSHh3Syg5WO6Zm/builds/IIptBkEyNeIbWIoEc/openapi.json
