# Bulk Redirect Checker: 301 Chains, Loops & Redirect Maps (`quanta1810/bulk-redirect-checker`) Actor

Bulk redirect checker: every hop of each URL's 301/302 redirect chain, final URL and status code, redirect loops, meta refresh, HTTPS downgrade, and old -> new redirect map validation for site migrations. $0.50 per 1,000 URLs, no start fee; unreachable or blocked URLs are free. API, MCP, n8n.

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

## Pricing

$0.50 / 1,000 urls

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

## Bulk Redirect Checker: 301 Chains, Loops & Redirect Maps

Paste a list of URLs — or a **redirect map** of `old URL -> new URL` pairs — and get **one clean row per URL** with **every hop of the redirect chain**, where it really ends, and **plain issue codes**: redirect loops, chains, temporary (302) instead of permanent (301) redirects, meta refreshes, HTTPS → HTTP downgrades, redirects to the homepage, dead destinations and **mismatches against the URL you expected**.

Every URL gets **every hop** and up to **17 issue codes** in a single pass:

| What | Details |
|---|---|
| **Every hop** | URL, HTTP status, `Location`, redirect type (301 / 302 / 303 / 307 / 308 / meta refresh) and time per hop |
| **Where it ends** | Final URL, final status code, page title, content type |
| **Expected destination** | Optional per line: does the chain end exactly where your redirect map says? (`expectedMatch`) |
| **Loops & limits** | Redirect loops, chains longer than your limit, `3xx` without `Location`, unparseable `Location`, redirects to `mailto:` and other non-web addresses |
| **Chain quality** | Chains of 2+ redirects, temporary redirects, meta-refresh redirects, JavaScript redirect pages, permanent-only chains |
| **HTTPS** | `http://` → `https://` upgrade, HTTPS → HTTP downgrade, invalid / expired certificates on the way |
| **SEO red flags** | Deep URL redirected to the homepage (soft-404 risk), cross-domain redirects, final 4xx / 5xx |
| **Dead destinations** | The start URL redirects, but the target does not answer |

No browser, no proxy, no API keys. In our test, a list of 30 mixed real-world URLs (23 traced, 7 that failed fast) took **about 11 seconds** at the default settings (10 in parallel), plus a few seconds for the run to start. **$0.0005 per URL ($0.50 per 1,000)** — URLs that can't be reached or block automated access are free.

### Who is it for?

- **SEO & site migrations**: paste your redirect map (`old URL -> new URL`) before and after launch; filter `expectedMatch = false` to see every old URL that lands in the wrong place, on a 404, on the homepage, or through a 302.
- **SEO audits**: find redirect chains and loops that waste crawl budget, temporary redirects that should be permanent, and HTTP pages that never upgrade to HTTPS.
- **Marketing & PPC teams**: check that campaign, QR-code and short links (bit.ly, t.co …) still end on the right landing page.
- **Affiliate & link managers**: expand tracking links and verify the final destination.
- **Developers**: verify `http → https → www` canonicalisation and redirect rules after a deploy.

### Quick links

- [Validate a redirect map](#validate-a-redirect-map-site-migration) · [Issue codes](#issue-codes) · [Status column](#status-column) · [Use with n8n, Make, Zapier, Google Sheets](#use-it-with-n8n-make-zapier-or-google-sheets) · [Pricing](#pricing) · [FAQ](#faq)

### How to use

1. Add your URLs in **URLs to check** — one per line. Full URLs are checked **exactly as typed** (`http://` and `https://`, `www` and non-`www`, trailing slash — these are different redirect tests). Bare domains (`example.com`) start on `http://` so you see the whole `http → https → www` chain.
2. To check a redirect map, put the expected destination on the same line: `https://old.com/page -> https://new.com/page`. A tab also works (paste two columns straight from a spreadsheet — a third column such as `301` is ignored), and so does a space. The expected URL may be a path (`/new-page`) on the same site. Lines that can't be read safely are listed as `invalid` and are free.
3. Click **Start**. Download the results as **CSV, Excel, JSON** or use the API.

#### Input example

```json
{
  "urls": [
    "http://wikipedia.org -> https://www.wikipedia.org/",
    "http://example.com/old-page -> https://example.com/new-page",
    "https://httpbin.org/redirect/2",
    "http://github.com"
  ],
  "maxRedirects": 10,
  "userAgent": "browser"
}
```

#### Output example (real row)

```json
{
  "input": "http://wikipedia.org -> https://www.wikipedia.org/",
  "url": "http://wikipedia.org/",
  "status": "ok",
  "charged": true,
  "errorCode": null,
  "expectedUrl": "https://www.wikipedia.org/",
  "expectedMatch": true,
  "finalUrl": "https://www.wikipedia.org/",
  "finalStatusCode": 200,
  "redirectCount": 2,
  "redirectChain": "301 → 301 → 200",
  "redirectPath": "http://wikipedia.org/ → https://wikipedia.org/ → https://www.wikipedia.org/",
  "permanentOnly": true,
  "httpsUpgrade": true,
  "issues": ["REDIRECT_CHAIN"],
  "issueCount": 1,
  "hops": [
    { "step": 1, "url": "http://wikipedia.org/", "statusCode": 301, "location": "https://wikipedia.org/", "type": "301", "timeMs": 249, "tlsError": null },
    { "step": 2, "url": "https://wikipedia.org/", "statusCode": 301, "location": "https://www.wikipedia.org/", "type": "301", "timeMs": 69, "tlsError": null },
    { "step": 3, "url": "https://www.wikipedia.org/", "statusCode": 200, "location": null, "type": null, "timeMs": 294, "tlsError": null }
  ],
  "finalTitle": "Wikipedia",
  "totalTimeMs": 619
}
```

The same run also found `http://example.com/old-page -> https://example.com/new-page` → `status: "http_error"`, `redirectChain: "404"`, `issues: ["FINAL_4XX", "EXPECTED_MISMATCH"]` — the old URL was never redirected.

### Validate a redirect map (site migration)

1. Export your redirect map as two columns (old URL, new URL) and paste it into **URLs to check** — one pair per line.
2. Run it on the old domain **before** launch (staging rules) and **again after** launch.
3. Open the **Redirect map QA** view and filter `expectedMatch = false`. The `issues` column tells you why: `EXPECTED_MISMATCH` with `REDIRECT_TO_HOMEPAGE`, `FINAL_4XX`, `TEMPORARY_REDIRECT` or `REDIRECT_CHAIN`.
4. Options for the comparison: **Ignore a trailing slash** (on by default) and **Ignore the query string** (off by default — turn it on when the site adds tracking parameters). Host case and default ports are always ignored; scheme, host, path and query otherwise must match.

### Issue codes

| Code | Meaning |
|---|---|
| `REDIRECT_LOOP` | The chain comes back to a URL it already visited (cookies are kept like a browser, so "set a cookie and redirect to itself" is not a false loop) |
| `TOO_MANY_REDIRECTS` | More redirects than **Max redirects to follow** (default 10) |
| `REDIRECT_CHAIN` | 2 or more redirects before the final page — each extra hop costs time and link equity |
| `TEMPORARY_REDIRECT` | At least one 302 / 303 / 307 in the chain (search engines may keep the old URL) |
| `META_REFRESH_REDIRECT` | An HTML `<meta http-equiv="refresh">` redirect (delay up to 10 s) |
| `HTTPS_TO_HTTP` | A hop goes from HTTPS back to plain HTTP |
| `REDIRECT_TO_HOMEPAGE` | A deep URL ends on the homepage — often a lazy migration rule (soft-404 risk) |
| `CROSS_DOMAIN_REDIRECT` | The chain ends on another domain (`www.` is ignored) |
| `FINAL_4XX` / `FINAL_5XX` | The chain ends on a client / server error page |
| `MISSING_LOCATION` | A 3xx answer without a `Location` header |
| `INVALID_LOCATION` | A `Location` header that is not a valid URL |
| `NON_HTTP_REDIRECT` | A redirect to `mailto:`, an app scheme or another non-web address |
| `REDIRECT_TARGET_UNREACHABLE` | The start URL redirects, but the destination does not answer (DNS / timeout / connection error) |
| `EXPECTED_MISMATCH` | You gave an expected URL and the chain does not end there with a page |
| `INVALID_CERTIFICATE` | A hop uses an expired, self-signed or otherwise invalid HTTPS certificate (the chain is still followed) |
| `JAVASCRIPT_REDIRECT` | The chain ends on a tiny page whose script sends the browser elsewhere (`window.location = …`). Scripts are not run, so the real destination is not followed |

#### Status column

Every row starts with the same columns, so failures are easy to filter:

| `status` | Meaning | Charged? |
|---|---|---|
| `ok` | The chain was traced and ends on a 2xx page | **Yes** |
| `http_error` | The chain was traced but ends badly: a 4xx / 5xx page (`HTTP_4XX` / `HTTP_5XX`), `REDIRECT_LOOP`, `TOO_MANY_REDIRECTS`, `MISSING_LOCATION`, `INVALID_LOCATION`, `NON_HTTP_REDIRECT` or `REDIRECT_TARGET_UNREACHABLE` — these are the findings a checker is for | **Yes** |
| `blocked` | The site refused automated access (`ACCESS_REFUSED` = HTTP 401/403/405/429, `BOT_CHALLENGE` = Cloudflare / Akamai / AWS WAF challenge). The hops before the refusal are still listed | No |
| `unreachable` | The first request failed (`DNS_NOT_FOUND`, `TIMEOUT`, `CONNECTION_REFUSED`, `CONNECTION_FAILED`, `TLS_ERROR`), or a redirect points into a private / internal network (`PRIVATE_ADDRESS`, never followed) | No |
| `invalid` | The input line is not a public URL or domain | No |
| `skipped` | Your **Max cost per run** was reached before this URL | No |
| `error` | Unexpected internal error (please open an issue) | No |

The dataset has two ready-made table views: **Overview** and **Redirect map QA**.

### Options

- **Max redirects to follow** (default 10, up to 30).
- **Check as**: *Browser* (Chrome) or the *Googlebot user-agent string* (smartphone) — to spot sites that redirect search engines differently. Requests still come from Apify's servers, not from Google, so firewalls that verify Googlebot by IP block them; those rows show up as `blocked` and are free.
- **Follow `<meta http-equiv="refresh">`** (on by default).
- **Ignore a trailing slash / Ignore the query string** when comparing with the expected URL.
- **…or URLs from a dataset**: use the output of another Actor (a sitemap or crawler). Fields `url` / `loc` / `finalUrl` / `website` / `domain` are read; an `expectedUrl` field becomes the expected destination.

### Use it with n8n, Make, Zapier or Google Sheets

Every run produces a normal Apify dataset, so it works with all Apify integrations:

- **n8n** — Apify node: *Run an Actor* → *Get dataset items*, then send rows to Google Sheets, Airtable or your CRM.
- **Make / Zapier** — Apify app (Make: *Watch Actor Runs* / *Run an Actor*; Zapier: trigger *Finished Actor Run* / action *Run Actor*).
- **Google Sheets / Excel** — download the dataset as CSV/Excel. In CSV the hops are flattened into `hops/0/url`, `hops/0/statusCode` …; the `redirectChain` and `redirectPath` columns give the same in one cell.
- **API (small lists)** — `POST https://api.apify.com/v2/acts/quanta1810~bulk-redirect-checker/run-sync-get-dataset-items?token=YOUR_TOKEN` with the input JSON returns the results in one call (synchronous calls time out after 300 s).
- **API (bulk)** — `POST …/acts/quanta1810~bulk-redirect-checker/runs?token=YOUR_TOKEN`, then poll the run and read `GET …/datasets/{defaultDatasetId}/items`, or attach a webhook for *run succeeded*.
- **Weekly checks** — save your list as a **task** and add an Apify **Schedule**; connect the Slack or email integration to get the results.

### Pricing

Pay only for results: **$0.0005 per URL** with a traced chain ($0.50 per 1,000). There is no start fee.

- URLs that **can't be reached on the first request**, are **behind bot protection**, point into a **private network**, are **invalid** or were **skipped** are listed with the reason but **not charged**.
- Set **Max cost per run** in the run options to cap your spend; the Actor processes as many URLs as fit and lists the rest as `skipped`.
- Exact duplicate lines are checked (and charged) once. `http://` vs `https://`, `www` vs non-`www` and a trailing slash are kept apart on purpose — they are different redirect tests.

### Limits & notes

- Up to **50,000 URLs per run** — split bigger lists into several runs. With the minimum memory (256 MB) the Actor checks 5 URLs in parallel; the default 512 MB allows 10.
- It follows HTTP redirects and HTML meta refreshes. **JavaScript redirects** (`window.location = …`) are not executed — a tiny page that contains one is reported as the final page with the `JAVASCRIPT_REDIRECT` issue.
- Each URL gets at most about a minute (longer with a higher request timeout): a host that never answers is retried once and then reported as unreachable, free.
- Requests come from Apify's datacenter IPs. Sites with strict bot protection (some news and retail sites) answer `403` or a challenge page: those rows are `blocked` and free, and the hops before the refusal are still shown.
- Only public URLs are checked. Redirects into private or internal networks (`10.x`, `127.x`, `169.254.169.254`, names that resolve to such addresses …) are never followed.
- Only the first 256 KB of the final page are read (enough for the title and meta refresh).

### FAQ

**Is this a bulk redirect checker?** Yes — paste up to 50,000 URLs per run; each gets the full chain, hop by hop, with status codes and timing.

**Can it check a 301 redirect map after a site migration?** Yes — put `old URL -> new URL` on each line and filter `expectedMatch = false`.

**Does it detect redirect loops and redirect chains?** Yes — `REDIRECT_LOOP`, `TOO_MANY_REDIRECTS` and `REDIRECT_CHAIN`, with every hop listed.

**Can I check 302 vs 301 redirects?** Yes — every hop has its `type`; `permanentOnly` is true when the chain uses only 301/308, and `TEMPORARY_REDIRECT` flags any 302/303/307.

**Does it expand short links (bit.ly, t.co, goo.gl)?** Yes — the final URL and every intermediate hop are listed.

**Is it an HTTP status code checker too?** Yes — URLs without redirects simply give one hop with their status code (`redirectChain: "200"` or `"404"`).

**Can I check with a Googlebot user-agent?** Yes — set **Check as** to the *Googlebot user-agent string*. It shows sites that redirect by user-agent; sites that verify Googlebot by IP will block it.

**Can I schedule it?** Yes — use Apify Schedules to run it daily, weekly or monthly.

**Why was a URL not charged?** Only URLs whose chain could be traced are charged. Unreachable, blocked, private-network, invalid and skipped URLs are free — see the `status` column.

**Found a bug or need another check?** Open an issue on the Actor's Issues tab — requests are welcome.

### Related tools

Other bulk checkers by the same developer, built to run side by side on the same list of sites:

- **[Bulk Website Audit](https://apify.com/quanta1810/website-health-audit)** — one health score per site: on-page SEO, SSL expiry, security headers, SPF/DMARC email DNS and public contact emails.
- **[Bulk PageSpeed Checker](https://apify.com/quanta1810/bulk-pagespeed)** — Lighthouse scores and real-user Core Web Vitals (LCP, INP, CLS) for many URLs, mobile and desktop.

### Support & changelog

Issues opened on the **Issues** tab are answered within 5 business days.

**Need something this tool does not do yet?** An extra check, a different output column or an option for your workflow — open an issue on the **Issues** tab and describe it. Requests from real users decide what gets built next.

- **0.1.0 (18 October 2026)** — first release.

# Actor input Schema

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

One URL per line. To validate a redirect map (site migration), put the expected destination on the same line: "https://old.com/page -> https://new.com/page". A tab also works (paste two columns from a spreadsheet; a third column is ignored), as does a space or a comma right before "https://". The expected URL may be a path (/new-page). Bare domains (example.com) start on http:// so you see the full http → https → www chain. Exact URLs are kept as typed: http/https and www/non-www are checked separately. Lines that can't be read safely are listed as invalid (free).

## `datasetId` (type: `string`):

Optional: ID of an Apify dataset (for example a sitemap or crawler result). Fields url / loc / finalUrl / website / domain are used; an expectedUrl field becomes the expected destination.

## `maxItems` (type: `integer`):

Stop after this many URLs — handy for a quick test on a big list.

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

A chain longer than this is reported as TOO_MANY_REDIRECTS. Browsers give up around 20.

## `userAgent` (type: `string`):

Browser = a normal Chrome visitor. Googlebot = the smartphone Googlebot user-agent STRING, to spot sites that redirect search engines differently. Requests still come from Apify's servers, not from Google, so some firewalls that verify Googlebot by IP will block them (those rows are free).

## `followMetaRefresh` (type: `boolean`):

Treat an HTML meta refresh (delay up to 10 s) as a redirect hop, like a browser does. It is flagged as META_REFRESH_REDIRECT because search engines handle it worse than a 301.

## `ignoreTrailingSlash` (type: `boolean`):

When on, https://new.com/page and https://new.com/page/ count as a match. Scheme, host, path and query must otherwise match exactly (host case and default ports are ignored).

## `ignoreQueryString` (type: `boolean`):

When on, https://new.com/page?utm_source=x matches https://new.com/page (tracking or session parameters added by the site). Off by default: the query is part of the URL.

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

How many URLs to check at the same time.

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

Give up on one request after this many seconds. A URL that never answers on the first request is reported as unreachable and not charged.

## Actor input object example

```json
{
  "urls": [
    "http://github.com",
    "http://apify.com -> https://apify.com/",
    "http://wikipedia.org -> https://www.wikipedia.org/"
  ],
  "maxItems": 0,
  "maxRedirects": 10,
  "userAgent": "browser",
  "followMetaRefresh": true,
  "ignoreTrailingSlash": true,
  "ignoreQueryString": false,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 15
}
```

# Actor output Schema

## `results` (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://github.com",
        "http://apify.com -> https://apify.com/",
        "http://wikipedia.org -> https://www.wikipedia.org/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("quanta1810/bulk-redirect-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://github.com",
        "http://apify.com -> https://apify.com/",
        "http://wikipedia.org -> https://www.wikipedia.org/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("quanta1810/bulk-redirect-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://github.com",
    "http://apify.com -> https://apify.com/",
    "http://wikipedia.org -> https://www.wikipedia.org/"
  ]
}' |
apify call quanta1810/bulk-redirect-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,quanta1810/bulk-redirect-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/RRY0TmVmQp3gGwfLd/builds/tBsMVpYZ8D3G1NVpM/openapi.json
