# Redirect Checker & Migration QA (Redirect Map Validator) (`saiberops/redirect-migration-qa-checker`) Actor

Check redirect chains and validate site-migration redirect maps. Records every hop and flags loops, 302s, HTTPS downgrades, wrong destinations, broken final pages, canonical mismatches and noindex. Plain HTTP, robots.txt-aware.

- **URL**: https://apify.com/saiberops/redirect-migration-qa-checker.md
- **Developed by:** [Saiber Operations](https://apify.com/saiberops) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 url 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

## Redirect Checker & Migration QA (Redirect Map Validator)

Check where your URLs really end up. Paste a list of URLs, or an old-URL → new-URL redirect map from a site migration, and get one row per URL with every redirect hop, the final destination and status code, and a pass / warning / fail verdict with plain issue codes.

**Built for:**

- **Site migrations and redesigns.** Prove that every old URL lands on the right new page with a single permanent redirect, before rankings drop.
- **SEO audits.** Find redirect chains, loops, temporary (302) redirects, HTTPS downgrades and pages that redirect to a 404.
- **Ad and email campaigns.** Confirm final URLs still resolve, so you don't pay for clicks that land on broken pages.
- **Developers and AI agents.** One API call returns clean JSON for a whole URL list.

### What it checks

For each URL, the Actor follows redirects itself, one hop at a time, so nothing is hidden. It reports:

- **Every hop:** URL, status code, `Location` header and response time.
- **The final URL and status code**, and the redirect count.
- **Loops**, chains longer than your limit, and redirects missing a `Location` header.
- **Temporary redirects** (302, 303, 307), which search engines may not treat as permanent moves.
- **HTTPS → HTTP downgrades**, and final pages that are not on HTTPS.
- **Expected vs. actual destination** for each pair in your redirect map, with strict or relaxed matching.
- **Indexability of the final page:** canonical link, robots meta tags and the `X-Robots-Tag` header, including canonicals that still point at the old domain.

### Quick start

1. Add URLs to **URLs to check**, or old/new pairs to **Redirect map**.
2. (Optional) Set a **Base URL** if your map uses paths such as `/old-page`.
3. Click **Start**. Results appear in the **Overview**, **Redirect map** and **Indexability** tabs.

Redirect-map entries can be objects or one-line strings:

```json
[
  { "from": "https://old.example.com/pricing", "to": "https://www.example.com/plans" },
  "https://old.example.com/about, https://www.example.com/company",
  "/blog/launch -> /news/launch"
]
```

### Sample output

```json
{
  "index": 3,
  "status": "fail",
  "inputUrl": "http://shop.example.com/legacy",
  "expectedUrl": "https://shop.example.com/landing",
  "finalUrl": "https://shop.example.com/",
  "finalStatus": 200,
  "matchesExpected": false,
  "redirectCount": 1,
  "chain": "302 → 200",
  "issues": ["TEMPORARY_REDIRECT", "EXPECTED_MISMATCH"],
  "hops": [
    { "url": "http://shop.example.com/legacy", "status": 302, "location": "https://shop.example.com/", "timeMs": 84 },
    { "url": "https://shop.example.com/", "status": 200, "location": null, "timeMs": 121 }
  ],
  "canonicalUrl": "https://shop.example.com/",
  "metaRobots": null,
  "xRobotsTag": null,
  "indexable": true,
  "totalTimeMs": 212,
  "stoppedAt": null,
  "errorMessage": null,
  "checkedAt": "2026-10-11T22:40:05+00:00"
}
```

Turn on **Create a migration summary report** to also get `MIGRATION_REPORT` (totals, redirect-map match rate, issue counts, slowest URLs) and `FAILURES.csv` (every URL that did not pass) in the run's key-value store.

### Issue codes

| Code | Result | Meaning |
|---|---|---|
| `EXPECTED_MISMATCH` | fail | The URL did not land on the expected URL from your redirect map |
| `FINAL_CLIENT_ERROR` / `FINAL_SERVER_ERROR` | fail | The final page returned 4xx / 5xx |
| `REDIRECT_LOOP` | fail | The chain comes back to a URL it already visited |
| `TOO_MANY_REDIRECTS` | fail | The chain is longer than "Max redirects to follow" |
| `MISSING_LOCATION` | fail | A 3xx response without a `Location` header |
| `HTTPS_TO_HTTP` | fail | A hop downgrades from HTTPS to HTTP |
| `DNS_ERROR` / `CONNECTION_ERROR` / `TLS_ERROR` | fail | The host does not resolve, refuses connections, or has a certificate problem |
| `TIMEOUT` | fail | No answer within the timeout (not charged) |
| `INVALID_URL` | fail | Not an http(s) URL (not charged) |
| `TEMPORARY_REDIRECT` | warning | The chain uses 302, 303 or 307 instead of 301 or 308 |
| `REDIRECT_CHAIN` | warning | More than one redirect before the final page |
| `FINAL_NOT_HTTPS` | warning | The final page is served over HTTP |
| `CANONICAL_MISMATCH` | warning | The final page's canonical points to a different URL |
| `NOINDEX` | warning | The final page has noindex in robots meta tags or `X-Robots-Tag` |
| `POSSIBLY_BOT_BLOCKED` | warning | 401, 403 or 429, often bot protection (see FAQ) |
| `SLOW_RESPONSE` | warning | The whole chain took more than 3 seconds |
| `BLOCKED_BY_ROBOTS` / `ROBOTS_UNAVAILABLE` | skipped | robots.txt disallows the URL, or returned a server error (not charged) |

### Pricing

Pay per event: you pay only for results.

| Event | Free plan | Bronze | Silver | Gold |
|---|---|---|---|---|
| URL checked (per URL with a definitive answer) | $0.0018 | $0.0015 | $0.00135 | $0.0012 |
| Migration report (once per run, only when enabled) | $0.06 | $0.05 | $0.045 | $0.04 |

That is $1.20–$1.80 per 1,000 URLs, plus Apify's standard Actor start fee. **Not charged:** timeouts, invalid URLs, URLs skipped because of robots.txt, and internal errors. Set **Max cost per run** to cap any run; the Actor stops cleanly when it reaches the cap.

### Integrations and API

Run it from the Apify API, on a schedule (for example, a weekly check of your top landing pages), or from Make, Zapier, n8n and other tools through Apify's integrations. AI agents can call it through Apify's MCP server: input is plain arrays of URLs, and no credentials are needed.

### FAQ

**Does it follow JavaScript or meta-refresh redirects?** Not yet. It follows HTTP redirects (301, 302, 303, 307, 308), which is what search engines and most migrations rely on.

**Why do I see `POSSIBLY_BOT_BLOCKED` on my own site?** Some firewalls block automated requests. The Actor identifies itself honestly as `SaiberOpsBot`. Allowlist that User-Agent on your firewall or CDN, then re-run.

**Will it overload a website?** No. It sends at most 4 requests at a time to any single host by default, and reads only the response headers of redirects and the `<head>` of the final page (up to 512 KB).

**Can it check pages behind a login?** No. It sends no cookies or credentials and only checks public URLs.

**My map uses relative paths.** Set **Base URL** (for example `https://www.example.com`), and paths such as `/old-page` resolve against it.

### Responsible use

The Actor only requests the URLs you provide, plus each site's `robots.txt`. It respects robots.txt by default (switch this off only for sites you own or are authorized to test), identifies itself as `SaiberOpsBot`, never logs in, never solves CAPTCHAs, and does not rotate IP addresses to get around blocks. It collects no personal data: output is limited to URLs, status codes and HTTP headers. You are responsible for checking only sites you are allowed to test.

### Support

Found a bug or need a feature? Open an issue on this Actor's **Issues** tab and we'll respond there.

*Maintained by Saiber Operations LLC.*

# Changelog

This Actor's version history is a separate document: https://apify.com/saiberops/redirect-migration-qa-checker/changelog.md

# Actor input Schema

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

Each URL is requested without auto-following redirects, so every hop is recorded. Use full URLs, or paths like /pricing together with a base URL. Can be left empty when you only use the redirect map.

## `redirectMap` (type: `array`):

Migration pairs to verify. Each item is {"from": "old URL", "to": "expected final URL"} or a string such as "old-url, new-url" or "old-url -> new-url". Relative paths use the base URL. Each pair reports whether the old URL really lands on the expected URL.

## `baseUrl` (type: `string`):

Optional. Resolves paths such as /old-page in either list, for example https://www.example.com.

## `matchMode` (type: `string`):

strict: exact match (scheme and host compared case-insensitively). ignoreTrailingSlash: /page and /page/ count as the same. ignoreQueryAndTrailingSlash: also ignores ?query strings.

## `checkIndexability` (type: `boolean`):

Reads the final HTML page's <head> (up to 512 KB) for the canonical link and robots meta tags, plus the X-Robots-Tag header.

## `migrationReport` (type: `boolean`):

Saves a pass/fail summary (MIGRATION_REPORT) and a CSV of every URL that did not pass (FAILURES.csv) to the run's key-value store. Charged once per run as the migration-report event.

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

On by default: URLs that robots.txt disallows for SaiberOpsBot are skipped and not charged. Turn off only for sites you own or are authorized to test.

## `maxRedirects` (type: `integer`):

Chains longer than this are reported as TOO_MANY_REDIRECTS.

## `maxConcurrency` (type: `integer`):

Total number of URLs checked at the same time.

## `maxConcurrencyPerHost` (type: `integer`):

Keeps the load on any single website polite. Raise it only for your own sites.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each hop. Timed-out URLs are reported but not charged.

## Actor input object example

```json
{
  "urls": [
    "http://apify.com/",
    "https://apify.com/store",
    "https://example.com/"
  ],
  "redirectMap": [
    {
      "from": "http://apify.com/",
      "to": "https://apify.com/"
    }
  ],
  "baseUrl": "",
  "matchMode": "ignoreTrailingSlash",
  "checkIndexability": true,
  "migrationReport": false,
  "respectRobotsTxt": true,
  "maxRedirects": 10,
  "maxConcurrency": 10,
  "maxConcurrencyPerHost": 4,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

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

No description

## `migrationReport` (type: `string`):

No description

## `failuresCsv` (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 = {
    "urls": [
        "http://apify.com/",
        "https://apify.com/store",
        "https://example.com/"
    ],
    "redirectMap": [
        {
            "from": "http://apify.com/",
            "to": "https://apify.com/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("saiberops/redirect-migration-qa-checker").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": [
        "http://apify.com/",
        "https://apify.com/store",
        "https://example.com/",
    ],
    "redirectMap": [{
            "from": "http://apify.com/",
            "to": "https://apify.com/",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("saiberops/redirect-migration-qa-checker").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": [
    "http://apify.com/",
    "https://apify.com/store",
    "https://example.com/"
  ],
  "redirectMap": [
    {
      "from": "http://apify.com/",
      "to": "https://apify.com/"
    }
  ]
}' |
apify call saiberops/redirect-migration-qa-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,saiberops/redirect-migration-qa-checker"
        }
    }
}
```

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/LQ9y3ha59z09Nklcu/builds/V1xvBtgyFvQHJYaCt/openapi.json
