# Redirect Release QA (`h_murdock/redirect-release-qa`) Actor

Check redirect maps against live public GET requests or supplied traces. Detect wrong targets, temporary redirects, loops and regressions; export JSON, CSV and HTML evidence.

- **URL**: https://apify.com/h\_murdock/redirect-release-qa.md
- **Developed by:** [Gilad Ronen](https://apify.com/h_murdock) (community)
- **Categories:** Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / completed report

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

### What does Redirect Release QA do?

**Redirect Release QA checks an intended URL migration map against HTTP evidence.** Give it old URLs and their expected destinations, then choose live public GET requests or previously captured traces. It reports the exact observed chain, wrong destinations, unexpected redirect codes, loops, failed terminal statuses and incomplete observations. The supplied example is deterministic and uses synthetic evidence; switch to live mode to check an actual public site.

The Actor works through the Apify API and can be scheduled after releases or for recurring legacy-link checks. Its purpose is narrow: check the HTTP behavior you specify. It does not modify websites, deploy rewrite rules or estimate search rankings.

### Why use it?

Migration spreadsheets become difficult to review when redirects cross hosts, change query strings or accidentally lead to a successful but incorrect page. This report keeps every mapping visible and distinguishes an assertion failure from an inaccessible destination. Teams can review the CSV, share a standalone HTML report, and compare a later run with a previous JSON report. Google's [site-move guidance](https://developers.google.com/search/docs/crawling-indexing/site-move-with-url-changes) describes URL mapping and redirect testing as parts of migration work; this tool checks those supplied expectations without claiming to certify a migration.

### How to use it

1. Start with the Input tab's supplied example. Each `id` is a unique string, including leading zeros.
2. Replace `mappings` with your URLs or provide `mappingsCsv` instead. Remove the unused alternative.
3. For live checks set `mode` to `live` and remove `traces`. Use public sites you are permitted to access.
4. Run the Actor. Review `status`, `issues`, `evidenceComplete` and the captured chain for each row.
5. Download the full JSON as your baseline. Supply it as `previousReport` on a later run with the same mapping IDs and expectations.

### Input

| Field | Meaning |
| --- | --- |
| `mode` | `supplied` by default, or `live` for public HTTP GET requests |
| `mappings` | 1–250 objects with `id`, `sourceUrl`, `expectedFinalUrl` |
| `allowedRedirectStatuses` | Per mapping; defaults to `[301,308]`; optionally permit 302, 303 or 307 |
| `expectedFinalStatus` | Per mapping; defaults to 200. An explicit 410 is supported |
| `maxHops` | Per mapping; maximum 8 redirect transitions, default 8 |
| `traces` | Supplied mode only; objects with `mappingId`, `steps`, and optional failure code |
| `mappingsCsv`, `csvColumns` | CSV alternative and optional canonical-field-to-header mapping |
| `previousReport` | Previous full JSON report, schema 1.0 and engine 0.1.0 |
| `maxRequests` | Up to 1,000 requests, including robots files and their redirects |
| `totalTimeoutSeconds` | 120 seconds by default, maximum 150, within a 180-second Actor timeout |
| `requestTimeoutSeconds` | 8 seconds by default, maximum 10, bounded by the remaining overall time |

A minimal live input is:

```json
{"mode":"live","mappings":[{"id":"001","sourceUrl":"https://example.com/","expectedFinalUrl":"https://example.com/"}]}
```

For CSV use `id,sourceUrl,expectedFinalUrl` and optionally `expectedFinalStatus,maxHops,allowedRedirectStatuses`. Status lists use `301|308`. BOM and quoted fields are supported. Duplicate, blank or padded headers, irregular records and overlapping column mappings are rejected. Duplicate mapping IDs are rejected; duplicate source URLs with different IDs remain separate rows and receive input findings. Conflicting targets are never silently merged.

Supplied traces must be contiguous: each step URL must equal the preceding Location after resolution. Each step requires `url` and numeric `status`; redirect steps can supply `location`. Optional `durationMs` is milliseconds and `observedAt` is an ISO timestamp with timezone. The Actor does not invent timestamps for supplied evidence. Missing traces are incomplete.

### Output

The dataset contains **one complete report item**, including `summary`, `rows`, `baselineChanges` and a report integrity digest. The key-value store has full JSON (`OUTPUT`), flat mapping CSV (`redirects.csv`) and standalone HTML (`report.html`). Use the download link for HTML. CSV formula cells are neutralized and HTML source content is escaped.

```json
{"rowId":"001","status":"fail","evidenceComplete":true,"observedFinalStatus":404,"issues":[{"code":"wrong_final_status","category":"assertion"}]}
```

| Result field | How to interpret it |
| --- | --- |
| `status` | `pass`, `fail`, or `incomplete` |
| `issues` | Stable issue IDs, code, severity, category, reason and evidence reference |
| `observedFinalUrl`, `observedFinalStatus` | Terminal observation, or null if not established |
| `hasUsableObservation` | At least one non-access-denied mapping response was observed |
| `evidenceComplete` | Terminal response was observed without a transport/access failure |
| `trace` | Observed step URL, status, Location, and available timing |
| `baselineChanges` | `new`, `unchanged`, `resolved`, `unobserved` or `not_comparable` |

A row can contain a proven assertion failure and incomplete terminal evidence, so summary `incomplete` can overlap `failed`. A known old issue resolves only when the same contract is comparable and the current terminal evidence is complete. Changed expectations, changed evidence mode or changed duplicate-source groups are not comparable. Unobserved and omitted rows do not resolve old findings. Incomplete transport findings themselves are excluded from assertion regression counts.

### Pricing

The price is **$0.50 per completed report**, including platform usage, for the entire bounded batch. No startup or per-row event is charged. A new run produces a new billable report. A live batch with zero usable mapping HTTP observations fails before the report event; HTTP 404 and 410 are usable observations, while DNS failures, timeouts, blocked robots access and 401/403/407/429 are incomplete. A partially observed batch can deliver a billable report containing explicit incomplete rows.

The dataset is the primary paid deliverable and is persisted before the report charge is confirmed. If a convenience-export write is interrupted, resurrecting the same run restores exports from the saved report without refetching the web or charging again. Recovery requires the original input, intact saved dataset and platform charge counts. The digest detects accidental modification; it is not an authenticated signature. If the process stops after a charge but the dataset is missing, recovery refuses to create another charge.

### Limits and comparison rules

Only HTTP and HTTPS on ports 80 or 443 are supported. Live DNS is checked by the actual socket lookup, all returned addresses must be public, and a validated address is pinned for that connection. Private, loopback, link-local, reserved and special address ranges are rejected on every hop. TLS certificate verification is enabled. No credentials, cookies, arbitrary headers, proxies or host overrides are accepted.

Requests run sequentially with at least 100 ms per-host spacing; applicable robots crawl delays are respected. Robots are checked on every new origin and can redirect at most five times. A missing robots file (404/410) permits the request; inaccessible or unsupported robots policy yields incomplete evidence. Robots bodies are limited to 128 KB of uncompressed UTF-8 and response headers to 16 KB. Compressed robots responses are rejected. For mapping GET requests, the stream is closed after headers and the page body is not downloaded or stored.

URL parsing normalizes scheme, hostname and default ports using standard HTTP URL rules. Path case, query order and values, percent-encoded forms and trailing slashes remain significant. Fragments are omitted from requests and comparisons because HTTP does not send them. Relative Locations are resolved against their response URL. No HTML meta refresh, JavaScript redirect, browser rendering, content relevance or authenticated staging check is performed.

Input JSON is limited to 4 MB. Full report JSON must remain below 8 MB and every export below 9 MB; limits are checked before billing. Split large or slow site inventories into smaller batches. A public site can still refuse or vary its response by location, time or user agent; this is a point-in-time header audit, not a search engine simulation.

### Support

Use the Actor's Issues tab with a minimal redacted mapping and captured trace. Do not include tokens, cookies, personal information or confidential query parameters. Only submit URLs you are authorized to inspect. A passed mapping is evidence that the supplied HTTP expectations passed for that observation, not a ranking or traffic guarantee.

# Actor input Schema

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

Supplied traces are deterministic. Live sends public HTTP GET requests and respects robots.

## `mappings` (type: `array`):

JSON mappings with stable string IDs. Supply mappings OR mappingsCsv.

## `traces` (type: `array`):

Supplied mode only. Each mappingId identifies a contiguous chain. Evidence is labeled supplied.

## `mappingsCsv` (type: `string`):

Alternative to mappings. Headers id,sourceUrl,expectedFinalUrl; optional expectedFinalStatus,maxHops,allowedRedirectStatuses. Status list uses 301|308.

## `csvColumns` (type: `object`):

Map canonical field names to your CSV headers. Effective fields must be distinct.

## `previousReport` (type: `object`):

Previous complete JSON report for comparable issue changes. Missing or unobserved rows never resolve old issues.

## `maxRequests` (type: `integer`):

Includes robots requests and redirects.

## `totalTimeoutSeconds` (type: `integer`):

120 seconds by default, up to 150; leaves time for report delivery within a 180-second Actor run.

## `requestTimeoutSeconds` (type: `integer`):

Includes DNS, TLS, headers and robots body. Capped by remaining overall budget.

## Actor input object example

```json
{
  "mode": "supplied",
  "mappings": [
    {
      "id": "001",
      "sourceUrl": "https://example.com/Old?campaign=Q3#section",
      "expectedFinalUrl": "https://example.com/New/?campaign=Q3"
    },
    {
      "id": "002",
      "sourceUrl": "https://example.com/legacy",
      "expectedFinalUrl": "https://example.com/current"
    },
    {
      "id": "003",
      "sourceUrl": "https://example.com/sale-old",
      "expectedFinalUrl": "https://example.com/sale-new"
    },
    {
      "id": "004",
      "sourceUrl": "https://example.com/retired",
      "expectedFinalUrl": "https://example.com/retired",
      "expectedFinalStatus": 410
    },
    {
      "id": "005",
      "sourceUrl": "https://example.com/loop-a",
      "expectedFinalUrl": "https://example.com/loop-final"
    },
    {
      "id": "006",
      "sourceUrl": "https://example.com/unreachable",
      "expectedFinalUrl": "https://example.com/available"
    },
    {
      "id": "007",
      "sourceUrl": "https://example.com/missing",
      "expectedFinalUrl": "https://example.com/missing"
    }
  ],
  "traces": [
    {
      "mappingId": "001",
      "steps": [
        {
          "url": "https://example.com/Old?campaign=Q3",
          "status": 301,
          "location": "/New/?campaign=Q3"
        },
        {
          "url": "https://example.com/New/?campaign=Q3",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "002",
      "steps": [
        {
          "url": "https://example.com/legacy",
          "status": 308,
          "location": "/current"
        },
        {
          "url": "https://example.com/current",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "003",
      "steps": [
        {
          "url": "https://example.com/sale-old",
          "status": 302,
          "location": "/home"
        },
        {
          "url": "https://example.com/home",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "004",
      "steps": [
        {
          "url": "https://example.com/retired",
          "status": 410
        }
      ]
    },
    {
      "mappingId": "005",
      "steps": [
        {
          "url": "https://example.com/loop-a",
          "status": 301,
          "location": "/loop-b"
        },
        {
          "url": "https://example.com/loop-b",
          "status": 301,
          "location": "/loop-a"
        }
      ]
    },
    {
      "mappingId": "006",
      "steps": [],
      "failure": "transport_error"
    },
    {
      "mappingId": "007",
      "steps": [
        {
          "url": "https://example.com/missing",
          "status": 404
        }
      ]
    }
  ],
  "maxRequests": 1000,
  "totalTimeoutSeconds": 120,
  "requestTimeoutSeconds": 8
}
```

# Actor output Schema

## `dataset` (type: `string`):

One report item with all rows; primary paid deliverable.

## `json` (type: `string`):

No description

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

No description

## `html` (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 = {
    "mappings": [
        {
            "id": "001",
            "sourceUrl": "https://example.com/Old?campaign=Q3#section",
            "expectedFinalUrl": "https://example.com/New/?campaign=Q3"
        },
        {
            "id": "002",
            "sourceUrl": "https://example.com/legacy",
            "expectedFinalUrl": "https://example.com/current"
        },
        {
            "id": "003",
            "sourceUrl": "https://example.com/sale-old",
            "expectedFinalUrl": "https://example.com/sale-new"
        },
        {
            "id": "004",
            "sourceUrl": "https://example.com/retired",
            "expectedFinalUrl": "https://example.com/retired",
            "expectedFinalStatus": 410
        },
        {
            "id": "005",
            "sourceUrl": "https://example.com/loop-a",
            "expectedFinalUrl": "https://example.com/loop-final"
        },
        {
            "id": "006",
            "sourceUrl": "https://example.com/unreachable",
            "expectedFinalUrl": "https://example.com/available"
        },
        {
            "id": "007",
            "sourceUrl": "https://example.com/missing",
            "expectedFinalUrl": "https://example.com/missing"
        }
    ],
    "traces": [
        {
            "mappingId": "001",
            "steps": [
                {
                    "url": "https://example.com/Old?campaign=Q3",
                    "status": 301,
                    "location": "/New/?campaign=Q3"
                },
                {
                    "url": "https://example.com/New/?campaign=Q3",
                    "status": 200
                }
            ]
        },
        {
            "mappingId": "002",
            "steps": [
                {
                    "url": "https://example.com/legacy",
                    "status": 308,
                    "location": "/current"
                },
                {
                    "url": "https://example.com/current",
                    "status": 200
                }
            ]
        },
        {
            "mappingId": "003",
            "steps": [
                {
                    "url": "https://example.com/sale-old",
                    "status": 302,
                    "location": "/home"
                },
                {
                    "url": "https://example.com/home",
                    "status": 200
                }
            ]
        },
        {
            "mappingId": "004",
            "steps": [
                {
                    "url": "https://example.com/retired",
                    "status": 410
                }
            ]
        },
        {
            "mappingId": "005",
            "steps": [
                {
                    "url": "https://example.com/loop-a",
                    "status": 301,
                    "location": "/loop-b"
                },
                {
                    "url": "https://example.com/loop-b",
                    "status": 301,
                    "location": "/loop-a"
                }
            ]
        },
        {
            "mappingId": "006",
            "steps": [],
            "failure": "transport_error"
        },
        {
            "mappingId": "007",
            "steps": [
                {
                    "url": "https://example.com/missing",
                    "status": 404
                }
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("h_murdock/redirect-release-qa").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 = {
    "mappings": [
        {
            "id": "001",
            "sourceUrl": "https://example.com/Old?campaign=Q3#section",
            "expectedFinalUrl": "https://example.com/New/?campaign=Q3",
        },
        {
            "id": "002",
            "sourceUrl": "https://example.com/legacy",
            "expectedFinalUrl": "https://example.com/current",
        },
        {
            "id": "003",
            "sourceUrl": "https://example.com/sale-old",
            "expectedFinalUrl": "https://example.com/sale-new",
        },
        {
            "id": "004",
            "sourceUrl": "https://example.com/retired",
            "expectedFinalUrl": "https://example.com/retired",
            "expectedFinalStatus": 410,
        },
        {
            "id": "005",
            "sourceUrl": "https://example.com/loop-a",
            "expectedFinalUrl": "https://example.com/loop-final",
        },
        {
            "id": "006",
            "sourceUrl": "https://example.com/unreachable",
            "expectedFinalUrl": "https://example.com/available",
        },
        {
            "id": "007",
            "sourceUrl": "https://example.com/missing",
            "expectedFinalUrl": "https://example.com/missing",
        },
    ],
    "traces": [
        {
            "mappingId": "001",
            "steps": [
                {
                    "url": "https://example.com/Old?campaign=Q3",
                    "status": 301,
                    "location": "/New/?campaign=Q3",
                },
                {
                    "url": "https://example.com/New/?campaign=Q3",
                    "status": 200,
                },
            ],
        },
        {
            "mappingId": "002",
            "steps": [
                {
                    "url": "https://example.com/legacy",
                    "status": 308,
                    "location": "/current",
                },
                {
                    "url": "https://example.com/current",
                    "status": 200,
                },
            ],
        },
        {
            "mappingId": "003",
            "steps": [
                {
                    "url": "https://example.com/sale-old",
                    "status": 302,
                    "location": "/home",
                },
                {
                    "url": "https://example.com/home",
                    "status": 200,
                },
            ],
        },
        {
            "mappingId": "004",
            "steps": [{
                    "url": "https://example.com/retired",
                    "status": 410,
                }],
        },
        {
            "mappingId": "005",
            "steps": [
                {
                    "url": "https://example.com/loop-a",
                    "status": 301,
                    "location": "/loop-b",
                },
                {
                    "url": "https://example.com/loop-b",
                    "status": 301,
                    "location": "/loop-a",
                },
            ],
        },
        {
            "mappingId": "006",
            "steps": [],
            "failure": "transport_error",
        },
        {
            "mappingId": "007",
            "steps": [{
                    "url": "https://example.com/missing",
                    "status": 404,
                }],
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("h_murdock/redirect-release-qa").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 '{
  "mappings": [
    {
      "id": "001",
      "sourceUrl": "https://example.com/Old?campaign=Q3#section",
      "expectedFinalUrl": "https://example.com/New/?campaign=Q3"
    },
    {
      "id": "002",
      "sourceUrl": "https://example.com/legacy",
      "expectedFinalUrl": "https://example.com/current"
    },
    {
      "id": "003",
      "sourceUrl": "https://example.com/sale-old",
      "expectedFinalUrl": "https://example.com/sale-new"
    },
    {
      "id": "004",
      "sourceUrl": "https://example.com/retired",
      "expectedFinalUrl": "https://example.com/retired",
      "expectedFinalStatus": 410
    },
    {
      "id": "005",
      "sourceUrl": "https://example.com/loop-a",
      "expectedFinalUrl": "https://example.com/loop-final"
    },
    {
      "id": "006",
      "sourceUrl": "https://example.com/unreachable",
      "expectedFinalUrl": "https://example.com/available"
    },
    {
      "id": "007",
      "sourceUrl": "https://example.com/missing",
      "expectedFinalUrl": "https://example.com/missing"
    }
  ],
  "traces": [
    {
      "mappingId": "001",
      "steps": [
        {
          "url": "https://example.com/Old?campaign=Q3",
          "status": 301,
          "location": "/New/?campaign=Q3"
        },
        {
          "url": "https://example.com/New/?campaign=Q3",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "002",
      "steps": [
        {
          "url": "https://example.com/legacy",
          "status": 308,
          "location": "/current"
        },
        {
          "url": "https://example.com/current",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "003",
      "steps": [
        {
          "url": "https://example.com/sale-old",
          "status": 302,
          "location": "/home"
        },
        {
          "url": "https://example.com/home",
          "status": 200
        }
      ]
    },
    {
      "mappingId": "004",
      "steps": [
        {
          "url": "https://example.com/retired",
          "status": 410
        }
      ]
    },
    {
      "mappingId": "005",
      "steps": [
        {
          "url": "https://example.com/loop-a",
          "status": 301,
          "location": "/loop-b"
        },
        {
          "url": "https://example.com/loop-b",
          "status": 301,
          "location": "/loop-a"
        }
      ]
    },
    {
      "mappingId": "006",
      "steps": [],
      "failure": "transport_error"
    },
    {
      "mappingId": "007",
      "steps": [
        {
          "url": "https://example.com/missing",
          "status": 404
        }
      ]
    }
  ]
}' |
apify call h_murdock/redirect-release-qa --silent --output-dataset

```

## MCP server setup

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

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/cvqIpQ4ByOIPG46na/builds/ggGyDHIyQLCM6PrmS/openapi.json
