# URL Health Check (`westbrook_data/url-health-check`) Actor

Checks HTTP status, response time and TLS validity for a list of URLs. Returns one dataset item per URL.

- **URL**: https://apify.com/westbrook_data/url-health-check.md
- **Developed by:** [Lucas Rincent](https://apify.com/westbrook_data) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 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

## URL Health Check: status, redirects, TLS, content / Vérification d’URL

Check a list of URLs in one run and get, for each one: final HTTP status, an actionable **category** (OK, 404-type, 5xx-type, DNS, timeout, TLS…), the full **redirect chain**, the **TLS certificate** (validity, issuer, days left), an optional **expected text** check and, between runs, a **change flag**. Independent tool.

Vérifiez une liste d’URL en un run : statut HTTP final, **catégorie** exploitable (OK, erreur client/serveur, DNS, délai, TLS…), **chaîne de redirections** complète, **certificat TLS** (validité, émetteur, jours restants), contrôle optionnel d’un **texte attendu** et, entre deux runs, un **indicateur de changement**. Outil indépendant.

### Try it / Essayer

```json
{"urls": ["https://example.com", "http://example.com", "https://example.org/page-inconnue"]}
```

Monitoring with expectations and change tracking / Veille avec attentes et suivi des changements:

```json
{"urls":["https://example.com","https://example.org"],"expectedText":"Example Domain","minTlsDays":14,"trackChanges":true,"stateKey":"mon-site"}
```

Files / Fichiers : `examples/INPUT.basic.json`, `examples/INPUT.monitoring.json`, `examples/OUTPUT.sample.json` (fictive shape, not a real run / forme fictive).

### What you get / Ce que vous obtenez

One dataset row per checked URL / une ligne par URL vérifiée :

| Field | Meaning / Sens |
| --- | --- |
| `url`, `finalUrl` | normalized input and final URL after redirects / entrée normalisée et URL finale |
| `category` | `OK`, `REDIRECT`, `CLIENT_ERROR`, `SERVER_ERROR`, `TLS_ERROR`, `DNS_ERROR`, `CONNECTION_ERROR`, `TIMEOUT`, `REDIRECT_LOOP`, `TOO_MANY_REDIRECTS`, `REDIRECT_ERROR`… |
| `ok` | final status 2xx and valid certificate if HTTPS / statut 2xx et certificat valide si HTTPS |
| `passed` | verdict against **your** expectations (`expectedStatus`, `expectedText`, `minTlsDays`) / verdict selon **vos** attentes |
| `statusCode`, `responseTimeMs`, `attempts` | final status (null on network failure), total time, requests made / statut final, durée totale, requêtes |
| `redirects`, `redirectCount` | each hop: URL, status, `Location` / chaque saut |
| `tls` | validity and error, protocol, issuer, subject, dates, `daysRemaining` (final HTTPS URL only / URL HTTPS finale seulement) |
| `contentMatch` | expected text found, or `null` if not requested / texte attendu trouvé ou `null` |
| `headers` | `server`, `contentType`, `contentLength`, `hsts` only; no cookies / uniquement ces champs, aucun cookie |
| `previousCategory`, `changed` | with `trackChanges`: previous category and whether it changed (`null` on first observation) / avec `trackChanges` |

`SUMMARY` (key-value store) reports counts per category, `passed`/`failed`, rejected inputs, duplicates, budget limits, status and `complete: false` for failed or aborted runs. / `SUMMARY` fournit compteurs par catégorie, entrées refusées, doublons, limites de budget, statut et `complete: false` en cas d’échec ou d’arrêt.

### Options

`method` GET (default) or HEAD · `expectedStatus` (`2xx`, `301`…) · `expectedText` (first 256 KiB, case-insensitive, GET only) · `minTlsDays` (0–365) · `timeoutSecs` per URL 1–60 (default 10) · `maxRedirects` 0–10 (0 returns the first 3xx) · `retries` 0–2 for transient network errors only · `concurrency` 1–20 (results keep input order) · `trackChanges` + `stateKey`.

### Safety and billing / Sécurité et facturation

- Only `http`/`https`. Localhost, private, link-local, cloud-metadata and other non-public addresses are refused **at connection time, for every redirect hop and every DNS answer**, so a public name cannot reach internal services. URLs with embedded credentials are rejected and never echoed. / Adresses non publiques refusées à la connexion, à chaque saut et à chaque réponse DNS ; URL avec identifiants refusées et jamais recopiées.
- Rejected inputs and duplicates produce **no row and no charge**; they are listed in `SUMMARY.rejected`. A private address discovered during a check also produces no row. / Entrées refusées et doublons : aucune ligne, aucune facturation.
- Each real check writes one row and is billed as one dataset item, **including "site is down" results**: the answer is the product. See the Pricing tab for the current price. Your spending limit caps the number of URLs checked **before** any request is made (`limitedByBudget`, `skippedByBudget`). / Chaque vérification réelle écrit une ligne facturée, y compris un site en panne. Le plafond de dépense limite le nombre d’URL avant toute requête.
- A certificate that fails validation (expired, self-signed, wrong host) does **not** stop the measurement: you still get the status, with `category: TLS_ERROR` and `passed: false`. / Un certificat invalide n’empêche pas la mesure : statut conservé, `TLS_ERROR` et `passed: false`.
- Public addresses of third parties are requested with a plain GET/HEAD and a fixed `UrlHealthCheck/1.0` user agent. Check only sites you may monitor; respect their terms and rate limits. / Ne vérifiez que des sites que vous avez le droit de surveiller.

### Change tracking / Suivi des changements

With `trackChanges`, a named key-value store (`url-health-state-<hash>`) keeps, per URL, a **SHA-256 prefix of the URL and its last category**, never the URL itself or page content, up to 5,000 URLs per `stateKey`. Reaching the limit fails the run before any request; use another `stateKey`. An unreadable state also fails explicitly. Runs are not locked: do not run two at once on the same `stateKey` (last write wins). Deleting the store resets the history. Storage costs may apply. Use an Apify Schedule for recurring checks and filter `changed: true` or `passed: false` downstream (webhook, n8n, Slack). / Avec `trackChanges`, un stockage nommé conserve par URL une empreinte et la dernière catégorie, jamais l’URL ni le contenu. Pas de verrou : ne pas lancer deux runs simultanés sur la même clé.

### Limits / Limites

- A single request per hop with a hard time limit; it is a health check, not a crawler, uptime SLA service or load tester. Results describe one moment from one Apify location. / Une requête par saut ; ce n’est ni un crawler, ni un service d’uptime garanti, ni un test de charge. Résultat ponctuel depuis un seul emplacement.
- The TLS certificate shown is the one served for the final URL; certificates of intermediate redirect hops are not reported. / Seul le certificat de l’URL finale est rapporté.
- Content checks look at the first 256 KiB; JavaScript is not executed. / Pas d’exécution JavaScript.
- No synthetic demo mode: the default input checks `example.com`. / Pas de mode démo : l’entrée par défaut vérifie `example.com`.
- Changes from the previous 0.0.4 build: new columns and categories; `ok` now also requires a valid certificate; `error` is a short message. / Changements depuis 0.0.4 : nouvelles colonnes et catégories ; `ok` exige aussi un certificat valide.

### Use with MCP and n8n / Avec MCP et n8n

Run the Actor with Apify's MCP server or API using the input above and read `get-dataset-items`. In n8n, use the Apify node "Run Actor and get dataset" then an IF on `passed` or `changed`. Keep your Apify token in the client's credentials, never in the input. / Gardez le jeton Apify dans les credentials du client, jamais dans l’entrée.

# Actor input Schema

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

1–1,000 http(s) URLs. A missing scheme means https. Duplicates are checked once; private addresses, credentials in URLs and other protocols are rejected without a request or charge. / 1 à 1 000 URL http(s). Sans schéma : https. Les doublons sont vérifiés une fois ; adresses privées, identifiants dans l’URL et autres protocoles sont refusés sans requête ni facturation.

## `method` (type: `string`):

GET reads the response (required for expected text); HEAD is lighter but some servers refuse it. / GET lit la réponse (requis pour le texte attendu) ; HEAD est plus léger mais certains serveurs le refusent.

## `expectedStatus` (type: `array`):

Final codes counted as success in `passed`: exact (301) or class (2xx). Default 2xx. / Codes finaux comptés comme succès dans `passed` : exact (301) ou classe (2xx). Défaut : 2xx.

## `expectedText` (type: `string`):

Case-insensitive text that must appear in the first 256 KiB of the final page. Requires GET. Empty: no content check. / Texte (sans casse) à trouver dans les 256 premiers Kio de la page finale. Exige GET. Vide : pas de contrôle.

## `minTlsDays` (type: `integer`):

Fails `passed` if the HTTPS certificate expires within this many days. 0 disables the expiry threshold; an invalid certificate always fails. / `passed` échoue si le certificat HTTPS expire dans ce nombre de jours. 0 : pas de seuil ; un certificat invalide échoue toujours.

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

Total time for one URL including redirects and retries, 1–60. / Durée totale par URL, redirections et nouvelles tentatives comprises, 1 à 60.

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

0 returns the first 3xx response as the result. / 0 renvoie la première réponse 3xx comme résultat.

## `retries` (type: `integer`):

Extra attempts for transient network errors only (timeout, reset). HTTP errors and DNS failures are not retried. / Tentatives supplémentaires pour les erreurs réseau transitoires seulement. Les erreurs HTTP et DNS ne sont pas rejouées.

## `concurrency` (type: `integer`):

1–20 URLs checked at the same time; results stay in input order. / 1 à 20 URL en parallèle ; les résultats restent dans l’ordre d’entrée.

## `trackChanges` (type: `boolean`):

Compares each URL's category with the previous run using a private named store (only URL hashes and categories, max 5,000 URLs per stateKey). Adds `previousCategory` and `changed`. Avoid concurrent runs on one stateKey. / Compare la catégorie de chaque URL au run précédent via un stockage nommé privé (empreintes d’URL et catégories, 5 000 URL max par stateKey). Ajoute `previousCategory` et `changed`. Éviter les runs simultanés sur une même clé.

## `stateKey` (type: `string`):

Name of the monitor for trackChanges (1–64 letters, digits, \_ or -). It is not a secret. / Nom de la veille pour trackChanges (1 à 64 lettres, chiffres, \_ ou -). Ce n’est pas un secret.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "method": "GET",
  "expectedStatus": [
    "2xx"
  ],
  "expectedText": "",
  "minTlsDays": 0,
  "timeoutSecs": 10,
  "maxRedirects": 5,
  "retries": 1,
  "concurrency": 5,
  "trackChanges": false,
  "stateKey": "default"
}
```

# Actor output Schema

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

One row per URL: status, category, redirects, TLS, headers. / Une ligne par URL : statut, catégorie, redirections, TLS, en-têtes.

## `summary` (type: `string`):

Counts per category, rejected inputs, budget limits and status. / Compteurs par catégorie, entrées refusées, limites de budget et statut.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("westbrook_data/url-health-check").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"] }

# Run the Actor and wait for it to finish
run = client.actor("westbrook_data/url-health-check").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"
  ]
}' |
apify call westbrook_data/url-health-check --silent --output-dataset

```

## MCP server setup

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

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/SYVkyuw8KKdO37Rb9/builds/VJXIK1guEaFsqydWz/openapi.json
