# Bulk URL Status and Redirect Chain Checker (`daddyd-prints/url-status-redirect-checker`) Actor

Checks the HTTP status and redirect chain of each URL and flags loops. Pay per checked URL. robots.txt blocks, 401, 403, 429, login redirects and marked CAPTCHA answers are listed, not charged.

- **URL**: https://apify.com/daddyd-prints/url-status-redirect-checker.md
- **Developed by:** [Derek Derringer](https://apify.com/daddyd-prints) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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 URL Status and Redirect Chain Checker

### What it does

This Actor checks the HTTP status and redirect chain of each URL in your list. For every URL it records each answer from the start address to the final one, the number of redirects, any loop and the response time. It reads each site's robots.txt first, sends HEAD requests and never reads a page body. Every URL it skips is listed with the reason and is not charged. It never logs in. It stops at login redirects and marked CAPTCHA answers and never tries to get past them.

### How to use it

1. Open the Actor in Apify Console.
2. The form opens with `https://docs.apify.com` in **URLs to check** as a sample. Replace it with your own URLs, one per line. If you keep the sample, it is checked and charged.
3. Change **Maximum URLs**, **Request timeout** or **Maximum redirects** if the defaults do not suit you.
4. Click **Start**.
5. When the run ends, open the **Output** tab for the checked URLs, the run report and the skipped URLs.

### Input

| Field | What it does | Default | Limits |
| --- | --- | --- | --- |
| URLs to check (`urls`) | The URLs to check, one per line. A domain name with no scheme gets `https://` added. The scheme and host are lower-cased and any `#` part is removed. The rest is kept as written, then duplicates are removed | Required. Prefilled with `https://docs.apify.com` | http and https only, up to 2,000 characters. Other entries are listed in SKIPPED and not charged (see Limits) |
| Maximum URLs (`maxUrls`) | The most URLs one run will check. The rest are listed in SKIPPED as `over_max_urls`, never requested and not charged | 1,000 | 1 to 10,000 |
| Request timeout (`requestTimeoutSecs`) | How long one request may take to answer with its status and headers. A URL that does not answer in time is listed in SKIPPED as `timeout` and not charged | 15 seconds | 3 to 60 seconds |
| Maximum redirects (`maxHops`) | The most redirects followed for one URL. A chain that keeps redirecting stops there and is marked `max_hops_reached`. 0 reports only the first answer | 10 | 0 to 20 |

### Output

Each checked URL adds one item to the run's dataset. This real item is from a local test run against docs.apify.com:

```json
{
  "startUrl": "https://docs.apify.com/platform/limits",
  "finalUrl": "https://docs.apify.com/account/limits",
  "finalStatus": 200,
  "hops": [
    {"url": "https://docs.apify.com/platform/limits", "status": 301},
    {"url": "https://docs.apify.com/limits", "status": 302},
    {"url": "https://docs.apify.com/account/limits", "status": 200}
  ],
  "hopCount": 2,
  "loop": false,
  "stopReason": null,
  "redirectNotFollowed": null,
  "responseTimeMs": 346,
  "checkedAt": "2026-10-15T02:20:57+00:00"
}
```

| Field | What it holds |
| --- | --- |
| `startUrl` | The address from your list, with `https://` added where needed and any `#` part removed |
| `finalUrl` | The last address requested in the chain |
| `finalStatus` | The HTTP status of the last answer. When a chain stops early, this is the last redirect status |
| `hops` | Every answer in order, from the start URL to the final URL, each with `url` and `status` |
| `hopCount` | How many of those answers were redirects (301, 302, 303, 307 or 308) |
| `loop` | `true` when a redirect points back to an address already in the chain. In a local test, a URL that redirected to a second address and back gave `true`, with `stopReason` `loop` and the start address in `redirectNotFollowed` |
| `stopReason` | `null` when the chain ended normally. Otherwise `loop`, `max_hops_reached`, `redirect_without_location`, `redirect_to_unsupported_address` (such as an ftp address), `redirect_to_invalid_address` or `redirect_target_too_long` |
| `redirectNotFollowed` | Where the last redirect pointed, for `loop`, `max_hops_reached` and `redirect_to_unsupported_address`. Otherwise `null` |
| `responseTimeMs` | For each answer in `hops`, the milliseconds from sending the request to getting its status and headers, added up. robots.txt reads and Crawl-delay waits are not included |
| `checkedAt` | When the check ended, in UTC |

Items arrive in the order the checks finish, not in your list order, because up to 5 sites are checked at once. The dataset has two views, **Status and redirects** and **Full redirect chains**.

### Pricing

This Actor is priced per event, with two charges:

- **Actor start:** Apify's automatic start event, at Apify's default price.
- **URL checked:** one charge for each checked URL written to the dataset.

**Price per checked URL: $1.00 per 1,000 URLs, which is $0.001 each.**

A checked URL is charged whatever its final status, so a 404 or a 500 counts, and so does a chain that loops or stops at Maximum redirects.

Never charged: everything listed in SKIPPED. That covers URLs and sites skipped because of robots.txt, final 401 and 403 answers, 429 answers, login redirects, marked CAPTCHA answers, invalid, malformed and duplicate URLs, unreachable sites, timeouts, network errors and anything past Maximum URLs. A 401 or 403 entry keeps its full chain and status, so you still see the answer. REPORT, SKIPPED and STATE are not charged either.

If you set a maximum cost for the run, the Actor checks before each URL starts that one more charge still fits, counting the URLs already being checked. When none fits, it starts no more checks, lists the rest in SKIPPED as `spending_limit_reached`, writes REPORT saying the spending limit was reached and ends the run without an error.

If Apify restarts the run on another server, URLs already in the run's dataset are not checked or charged again. A new run starts from the beginning and charges again.

### robots.txt and site rules

The robots.txt check is always on, and no input turns it off. Before any other request to a site (scheme, host and port), the Actor reads its robots.txt and follows the group for `url-status-redirect-checker`, otherwise the `*` group, for at most 24 hours. Every new site in a redirect chain gets the same check first. Every skip below is listed in SKIPPED with the reason and any chain seen so far, and is not charged.

| Case | What the Actor does |
| --- | --- |
| robots.txt disallows a URL, or an address its chain redirects to | Never requests it (`robots_disallowed`). SKIPPED shows the rule and, after a redirect, the address not requested |
| `Disallow: /` for this Actor, even with Allow lines | Skips every URL on that site (`robots_disallow_all`) |
| robots.txt answers 401, 403, 429 or a server error, or cannot be reached or read | Skips every URL on that site |
| robots.txt answers another 4xx, such as 404 | Treats the site as having no rules |
| robots.txt sets a Crawl-delay | Waits that long between requests to that site, counted from the end of the last one |
| The Crawl-delay is over 30 seconds | Skips the site (`crawl_delay_too_long`) |
| How each address is asked | A HEAD request, on every hop. Only when HEAD is refused (405, 501 or a reset connection) is one GET sent instead, and it is closed as soon as the status and headers arrive. The body is never read |
| Requests to one site | One at a time. Up to 5 sites from your list are checked at once |
| The answer is 401, 403 or 429 | Stops the chain there (`http_401`, `http_403` or `http_429`), keeps the chain and status, and never retries |
| A redirect to a login address (a path or host name with words such as login, signin, auth or sso), or to an address with a user name or password | Stops before requesting it (`login_redirect`) |
| A marked CAPTCHA answer: a response header that marks a bot check or CAPTCHA, or a redirect to a CAPTCHA address | Stops there (`captcha`). A CAPTCHA address is never requested |
| On Apify, a site on a private or local network address | Refused before any request (`not_public_address`) |

The user agent is `url-status-redirect-checker/0.1 (Apify Actor)`, and it never borrows another crawler's name. No cookies are stored or sent and no proxy is used. There is no cookie, header, login, proxy or user agent input.

### Your responsibility

You are responsible for submitting only URLs you are allowed to access and for following each website's terms of use. The Actor checks robots.txt for you, but it cannot read or judge a site's terms.

### Run records

Three records in the run's key-value store, never charged:

| Record | What it holds |
| --- | --- |
| REPORT | Why the run stopped, in words, such as "all URLs were processed" or "the spending limit set for this run was reached". Start and finish times, duration, the user agent, the settings used and any input problems. Counts: URLs in the input, accepted, checked, checked before a restart, charged and skipped, skips by reason, final statuses and stop reasons. Whether a final 401 or 403 is charged (`false`). The robots.txt result for each site, up to 500 sites. Requests: total, robots.txt, HEAD, GET and GET after a refused HEAD, sites contacted, whether robots.txt was the first request on every site, and the most requests at once on one site and across sites. Storage writes. If you abort the run, checks in progress stop and are listed with the rest as `run_aborted`. If an unexpected error stops the run, unchecked URLs are listed as `run_stopped_by_error`. REPORT and SKIPPED are written either way |
| SKIPPED | `count` and `items`. Each item has the `url`, a `reason` code and a `detail` in words. Items from the checks also have `listIndex`, the URL's place among the accepted URLs counting from 0. A URL stopped part way also has `lastStatus`, the `hops` seen and `stoppedAtUrl`, the address it pointed to that was not requested. Problems found in your list come first, then the rest in list order. Reason codes: `invalid_url`, `malformed_url`, `duplicate`, `over_max_urls`, `robots_disallowed`, `robots_disallow_all`, `robots_server_error`, `robots_unreachable`, `robots_forbidden`, `robots_rate_limited`, `not_public_address`, `crawl_delay_too_long`, `not_web_url`, `http_401`, `http_403`, `http_429`, `captcha`, `login_redirect`, `timeout`, `network_error`, `unexpected_error`, `spending_limit_reached`, `run_aborted`, `run_stopped_by_error` |
| STATE | A progress record: a hash of your URL list, the list positions already handled, the skipped entries, counts, whether the run finished, and when it was saved. Written about once a minute and at the end |

### Limits

| Limit | Value |
| --- | --- |
| URLs | Maximum URLs, 1 to 10,000, 1,000 by default, counted after bad entries and duplicates are removed |
| Redirects | Maximum redirects, 0 to 20, 10 by default. With 10, a chain holds at most 11 answers. A chain cut there is a charged result marked `max_hops_reached` |
| Timeouts | Request timeout, 3 to 60 seconds, 15 by default, for each request. A GET sent after a refused HEAD gets its own. robots.txt reads have a fixed timeout of 15 seconds per step and 20 seconds in all |
| Addresses | http and https only, up to 2,000 characters, with a host name, no spaces and no user name or password. Anything else is listed as `invalid_url`. An address the HTTP client cannot send, such as one with a DEL character in the path, full-width letters in the host name or a zero-width space right after the host, is listed as `malformed_url`. Neither is requested or charged. A zero-width space later in the path is percent-encoded and sent, and that URL is checked like any other |
| Redirect targets | A redirect with no Location header, or to a non-web address such as ftp, to an address the HTTP client cannot send, or to one longer than 8,192 characters, ends the chain as a charged result with that `stopReason` |
| Login and CAPTCHA pages | Found only from statuses, headers and redirect addresses, because the body is never read. A login or CAPTCHA page that answers 200 with no marker header, at an address with no login or CAPTCHA words in it, is reported as a 200 result like any other page |
| Input values | A number outside its range is set to the nearest limit, and one that is not a number gets the default. REPORT notes either |
| Memory | 256 MB, set as the minimum, maximum and default |

# Actor input Schema

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

One web address per line, starting with http:// or https://. https:// is added only to a bare domain name written without a scheme, with or without a path after it. Any other line without http:// or https:// is refused, listed in the SKIPPED record and not charged. Duplicates are removed. Before anything is requested from a site, the Actor reads that site's robots.txt, and any URL it disallows is skipped, listed in the SKIPPED record and not charged. The same check is made again for every redirect that leads to another site.

## `maxUrls` (type: `integer`):

The most URLs this run will check. Addresses beyond this number are listed in SKIPPED and not charged. Your spending limit for the run is also respected.

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

How long one request may take to answer with its status and headers. A URL that does not answer in time is listed in SKIPPED and not charged.

## `maxHops` (type: `integer`):

The most redirects followed for one URL. A chain that keeps redirecting after this many stops there and is marked max_hops_reached. 0 reports only the first answer.

## Actor input object example

```json
{
  "urls": [
    "https://docs.apify.com"
  ],
  "maxUrls": 1000,
  "requestTimeoutSecs": 15,
  "maxHops": 10
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

## `skipped` (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": [
        "https://docs.apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("daddyd-prints/url-status-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": ["https://docs.apify.com"] }

# Run the Actor and wait for it to finish
run = client.actor("daddyd-prints/url-status-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": [
    "https://docs.apify.com"
  ]
}' |
apify call daddyd-prints/url-status-redirect-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,daddyd-prints/url-status-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/S3CvpiDsUroLT3eaJ/builds/C3lAv5R4ZzthKTorh/openapi.json
