# Website Uptime, Status & Redirect Monitor (`isnard/uptime-monitor`) Actor

Check a list of URLs for status code, response time, redirect chain, TLS and a required keyword. Remembers the last state of each URL and flags when a site goes down or comes back up. Built for scheduled runs.

- **URL**: https://apify.com/isnard/uptime-monitor.md
- **Developed by:** [Matias Isnard](https://apify.com/isnard) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 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.

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

## Website Uptime, Status & Redirect Monitor

Give it a list of URLs. For each one you get the final status code, the total response time, every redirect on the way (status and location per hop), the final URL, whether TLS was valid, the content length and, if you ask for it, whether a keyword appears on the page. Each URL is marked `up`, `down` or `degraded`.

The Actor remembers the state of every URL between runs. When a URL goes down or comes back up, the result has `stateChanged: true` and the previous state, so you can schedule it every few minutes and only react to changes.

Each URL gets one GET request, plus one per redirect. Nothing else is fetched: no images, scripts or other pages. Requests identify themselves with the User-Agent `Mozilla/5.0 (compatible; ApifyUptimeMonitor/1.0; +https://apify.com/isnard/uptime-monitor)`.

### Input

```json
{
    "urls": ["http://wikipedia.org", "https://github.com", "https://shop.example.com/health"],
    "timeoutSecs": 15,
    "expectedStatus": "200-399",
    "keyword": "",
    "slowThresholdMs": 3000,
    "maxRedirects": 10
}
```

- `urls` (required): URLs to check. `https://` is added when there is no scheme. Duplicates are checked once. Invalid lines are skipped with a warning and not charged.
- `timeoutSecs` (default 15): limit for each request in the chain.
- `expectedStatus` (default `200-399`): final status codes that count as up. Ranges and single codes separated by commas, for example `200-299,301` or `200,401`.
- `keyword` (optional): text that must appear in the final response body. Case-insensitive, first 2 MB only. The same keyword is used for all URLs in the run.
- `slowThresholdMs` (default 3000): above this total time, a URL that is otherwise fine is `degraded`.
- `maxRedirects` (default 10): more redirects than this make the URL `down`.

### How the state is decided

| State | When |
| --- | --- |
| `down` | DNS, connection, TLS or timeout error; too many redirects; final status not in `expectedStatus`; keyword not found |
| `degraded` | Not down, but total response time above `slowThresholdMs` |
| `up` | Everything else |

`stateChanged` is `true` only when a URL goes from `up` or `degraded` to `down`, or from `down` back to `up` or `degraded`. Moving between `up` and `degraded` keeps `stateChanged: false` (you still see it in `previousState`), so a slow minute does not wake anyone up. The first time a URL is checked there is no previous state and `stateChanged` is `false`.

`downSince` is the time of the first check that found the current outage. It stays the same while the URL keeps failing and goes back to `null` when it recovers. On the recovery result, `wasDownSince` tells you when that outage started.

States are kept in a named key-value store called `uptime-monitor-state` in your Apify account (key `STATE`, one entry per URL). It survives between runs and between schedules. Delete the store to start fresh.

### Output

One dataset item per URL. Example of a URL with two redirects (real output from a test run, more in `ejemplos/salida.json` in the source):

```json
{
    "url": "http://wikipedia.org",
    "checkedAt": "2026-10-29T09:25:43+00:00",
    "state": "up",
    "previousState": "up",
    "stateChanged": false,
    "downSince": null,
    "wasDownSince": null,
    "reason": null,
    "statusCode": 200,
    "finalUrl": "https://www.wikipedia.org/",
    "contentLength": 122421,
    "keywordFound": null,
    "tlsOk": true,
    "error": null,
    "responseTimeMs": 383,
    "redirectCount": 2,
    "redirectChain": [
        { "url": "http://wikipedia.org", "status": 301, "location": "https://wikipedia.org/" },
        { "url": "https://wikipedia.org/", "status": 301, "location": "https://www.wikipedia.org/" }
    ]
}
```

A URL that just recovered has `"previousState": "down"`, `"stateChanged": true`, `"downSince": null` and `"wasDownSince"` set to the start of the outage.

- `reason`: why the URL is down or degraded, for example `Unexpected status 404`, `DNS lookup failed (host not found)`, `TLS certificate not valid: certificate has expired`.
- `responseTimeMs`: total for all hops, from sending each request until its body is read.
- `tlsOk`: `true` when every https hop had a valid certificate, `false` on a TLS error, `null` when no https request got an answer (plain http, or DNS failure).
- `contentLength`: the `Content-Length` header, or the bytes received (up to 2 MB) when the server does not send it.
- `keywordFound`: `null` when no keyword was given.

The run's status message summarizes the run, for example `4 up, 0 degraded, 2 down. Changed: https://shop.example.com/health down`.

A URL that cannot be reached still gets a result with `state: "down"`. The Actor does not fail because of a bad URL.

### Scheduling every 5 or 15 minutes

1. Save your input as a task: open the Actor in Apify Console, fill in the input and click **Save as a new task**.
2. Go to **Schedules**, click **Create new**, and set the cron expression:
   - every 5 minutes: `*/5 * * * *`
   - every 15 minutes: `*/15 * * * *`
3. Add the task to the schedule and save.

Because the state lives in a named store, every scheduled run compares against the previous one automatically. If you run several tasks with different URL lists, they share the store, which is fine as long as the same URL is not checked by two tasks at the same moment.

### Getting alerts

Each run is a normal Apify run, so any Apify integration can react to it. On the task, open the **Integrations** tab:

- **Email**: add the email integration on "Run succeeded". You get a message after each run with the status message (which lists changed URLs). With a 5-minute schedule that is a lot of email, so this works best with longer intervals.
- **Slack**: connect the Slack integration and post to a channel on "Run succeeded". Same idea: the status message tells you what changed.
- **Webhooks** (alerts only on changes): add an HTTP webhook on "Run succeeded" pointing to your own endpoint, or to Make, Zapier or n8n. The payload includes `resource.defaultDatasetId`. Fetch `https://api.apify.com/v2/datasets/<defaultDatasetId>/items?clean=true` and keep the items where `stateChanged` is `true`. Send the alert only when that list is not empty.

### Pricing

Pay per event:

- `url-checked`: each URL checked, including URLs that turn out to be down, because the requests were still made. Invalid input lines are not charged.
- `state-changed`: only when a URL goes down or comes back up.

Example: 10 URLs every 15 minutes is about 28,800 checks per month. You can cap the cost of a run with the "Maximum cost per run" setting; the Actor stops cleanly when the cap is reached and saves the state of the URLs it did check.

### Limitations

- Checks run from Apify's servers, in one region. A site blocked only in some countries, or a problem with one CDN location, may not show up.
- Each run is a single check per URL with no retry. One timeout is enough to mark a URL down. If that is too sensitive, raise `timeoutSecs`.
- Only GET. HEAD, POST, custom headers, authentication and cookies are not supported.
- The keyword search reads the raw body. Text added later by JavaScript is not seen.
- Some sites block automated requests and answer 403 or 503 even when they are working. Add those codes to `expectedStatus` if that is normal for your site.
- Response times include TLS handshake and body download, measured from Apify's network. Compare them with each other, not with a browser.
- The state store keeps entries for URLs you no longer check. They do no harm; delete the store if you want to clean up.

### Run locally

```bash
python -m venv .venv
.venv/Scripts/pip install -r requirements.txt   # on Linux/macOS: .venv/bin/pip
## put input in storage/key_value_stores/default/INPUT.json
.venv/Scripts/python -m my_actor
python -m unittest discover -s tests -t .
```

Author: Matias Isnard.

# Actor input Schema

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

URLs to check, one per line. https:// is added when no scheme is given. Each URL gets one GET request, plus one per redirect. Duplicates are checked once.

## `timeoutSecs` (type: `integer`):

Maximum time for each request in the chain. A URL that does not answer in time is down.

## `expectedStatus` (type: `string`):

Final status codes that count as up. Ranges and single codes separated by commas, for example 200-399 or 200,204,301.

## `keyword` (type: `string`):

Optional. Text that must appear in the final response body (first 2 MB, case-insensitive). If it is missing, the URL is down.

## `slowThresholdMs` (type: `integer`):

When the total response time (all hops) is above this, an otherwise healthy URL is degraded.

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

Redirects to follow before giving up. More than this makes the URL down. With 0, any redirect makes the URL down.

## Actor input object example

```json
{
  "urls": [
    "https://example.com",
    "http://github.com"
  ],
  "timeoutSecs": 15,
  "expectedStatus": "200-399",
  "slowThresholdMs": 3000,
  "maxRedirects": 10
}
```

# 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": [
        "https://example.com",
        "http://github.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("isnard/uptime-monitor").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://example.com",
        "http://github.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("isnard/uptime-monitor").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://example.com",
    "http://github.com"
  ]
}' |
apify call isnard/uptime-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,isnard/uptime-monitor"
        }
    }
}
```

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/hm5Cj0juGp0WmQKth/builds/5Aip6A3gKfXuqOQtj/openapi.json
