# Uptime Monitor API - Website Status, SSL & Response Time (`stas_k/uptime-monitor-api`) Actor

An uptime check that calls everything healthy because it silently dropped the slow sites is worse than none: you learn it from your customers. This actor reports every URL it could not check, with the reason, and pays only for URLs it really checked. Status codes, redirects, TLS expiry.

- **URL**: https://apify.com/stas_k/uptime-monitor-api.md
- **Developed by:** [Stas K](https://apify.com/stas_k) (community)
- **Categories:** Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$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

## Uptime Monitor API - Website Status, SSL and Response Time

Bulk website uptime checks with automatic retries and an optional second
EU vantage point. HTTP status, response time, SSL validity and certificate
expiry for scheduled monitoring, agency client portfolios and deployment
gates. Pay only for delivered observations.

### The pain this Actor removes

The leading uptime actors in this niche have three known gaps:

1. **No automatic retries.** A single transient timeout or a hiccup on the
   way to the site is reported as downtime, and you get woken up for nothing.
   This Actor probes every URL at least twice before it calls the site
   unreachable, and keeps every probe attempt visible in the output.

2. **One observer only.** "Does it measure global uptime? No. One Apify
   runtime location." A false alarm or a false all-clear both start there.
   This Actor can add a second vantage point: an EU gate reports whether the
   target answers at all. A URL is reported up when EITHER observer saw it
   answer, so a healthy site is not marked down just because one observer
   had a bad path to it.

3. **SSL stops at "valid or not".** No certificate expiry date is extracted
   by the leading actor. This Actor adds sslDaysToExpiry for HTTPS targets,
   read even while the target is failing, so a certificate that dies in 7
   days shows up before it breaks your site.

Also: no charge for starting the run. A run of 100 URLs here costs the same
per observation as the niche leader ($0.50 / 1,000) but with retries, a
second observer and certificate expiry included.

### What you get per URL

| Field | Meaning |
|---|---|
| up | A probe received a final HTTP status in 200-399, or the second observer reported the target answering. Custom health rules do not change up. |
| healthy | Transport succeeded and all configured health rules passed. |
| healthIssues | Reasons such as unexpected_status, slow_response, timeout, dns_error, tls_error, gate_unverified (second observer answered without an HTTP code). |
| statusCode | Final HTTP status after redirects; null without a response. When the second observer is the only one that saw the target answer, its reported code is used; when the runtime itself answered with a final 4xx/5xx, that code is reported even if the second observer saw something else. |
| responseTimeMs | Latency of the accepted final apify response, through final headers. A 500 in 100 ms followed by a 200 in 900 ms reads 900, not the minimum over attempts. The second observer's latency is visible in probes and is not mixed in. A final 5xx from the runtime carries no latency number: responseTimeMs is null there, because only accepted probe responses count toward it. |
| httpStatusExpected | Whether the final status matches expectedStatusCodes; null when only the second observer saw the target. |
| responseTimeOk | Whether latency is within maxResponseTimeMs; null when only the second observer saw the target. |
| sslValid, sslDaysToExpiry | TLS verification result and days until expiry for HTTPS. sslValid is true when the HTTPS handshake completed and the chain verified (any final status, including 4xx and 5xx), false on tls_error, null when no answer arrived. Expiry is read even for targets that are currently down. |
| probes | Every probe attempt: location (apify or eu-gate), attempt, status, latency, error. |
| fromCache | True when the observation was reused from another run inside the cache window. Cached rows are free. |
| errorType | One of: timeout, dns_error, tls_error, connection_error, redirect_blocked, too_many_redirects, private_target, invalid_url. |

### Billing, honestly

- $0.0005 per delivered URL observation. An unreachable public website,
  including one whose DNS does not resolve (errorType dns_error), is a
  useful, billed observation, exactly like the niche leader.
- Starting a run costs nothing.
- Invalid URLs and non-public targets (private, loopback, link-local) are
  reported for free and are never billed. URLs with embedded credentials
  (https://user:pass@host/) are rejected as invalid and never billed.
- A fresh observation from another run within cacheMinutes is reused for
  free and marked fromCache. The cache key includes the health rules, so
  changing expectedStatusCodes or maxResponseTimeMs does not reuse a verdict
  made under different rules.
- Set a maximum cost per run (maxTotalChargeUsd). When the budget is
  exhausted, rows stop being delivered and the coverage summary reports the
  gap so an incomplete run cannot look like an all-clear. The platform refuses charges only when the next event would not fit, so a run can overshoot the cap by up to one event (41 checks at $0.0005 costs $0.0205 against a $0.02 cap).

### Coverage summary: no false all-clear

RUN_SUMMARY in the run's key-value store provides:
requested_unique_urls, selected_urls, checked_urls, omitted_by_max_items,
omitted_by_time_budget, invalid_count, private_count, cached_count,
refused_by_charge_limit, delivery_failures, healthy_count,
unhealthy_count, unhealthy_urls, finished, run_error, complete, all_healthy.

For an all-clear, require run status SUCCEEDED plus finished=true,
complete=true and all_healthy=true. all_healthy is false while any row was
invalid, private or undeliverable, so a run cannot read as an all-clear when
part of the input was never measured. A run may succeed while a website is
down: that is a valid measurement. complete=false means not every supplied
URL produced an observation.

### Example

Input:

```json
{
  "urls": ["https://example.com", "https://apify.com"],
  "expectedStatusCodes": [200],
  "maxResponseTimeMs": 5000
}
```

Output row:

```json
{
  "url": "https://example.com/",
  "up": true,
  "healthy": true,
  "healthIssues": [],
  "statusCode": 200,
  "responseTimeMs": 412,
  "httpStatusExpected": true,
  "responseTimeOk": true,
  "sslValid": true,
  "sslDaysToExpiry": 83,
  "probes": [
    {"location": "apify", "attempt": 1, "status_code": 200, "latency_ms": 412, "error": null},
    {"location": "eu-gate", "alive": true, "http_code": 200, "latency_ms": 51, "error": null}
  ]
}
```

### Limits and honest notes

- "Unhealthy" is an observation from up to two locations, not global uptime
  and not a contractual SLA percentage. It does not measure availability
  between checks.
- The Actor sends a HEAD/GET request and stops after final headers. It does
  not render pages, run JavaScript or detect content errors behind a
  healthy HTTP response.
- Redirects are followed up to ten hops, and every hop is checked against
  private ranges. Private, loopback and link-local targets are refused.
  Public IPv6 literals are accepted.
- The query string is part of the checked URL: https://e.com/a?token=1 and
  https://e.com/a?token=2 are checked as two different targets.
- The eu-gate observer is optional: it is used when the run's environment
  provides UPTIME_GATE_URL and UPTIME_GATE_AUTH. Without them, checks run
  from the Apify runtime only and the retry logic still applies.
- timeBudgetSeconds caps the checking phase; when it is exhausted the
  coverage summary reports omitted_by_time_budget instead of silently
  finishing.
- The Actor sends no emails and creates no schedules automatically. For
  alerts, save your input as a task, add an Apify Schedule and route the
  ACTOR.RUN.SUCCEEDED webhook to your alerting system.

# Actor input Schema

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

Public HTTP(S) URLs or bare domains. Bare domains use HTTPS. Duplicate normalized URLs are checked once. Query strings are kept: two URLs that differ only by query are checked separately.

## `expectedStatusCodes` (type: `array`):

Health rule: the response is healthy only when its final status code is in this list. Leave empty for the default HTTP 200-399 rule.

## `maxResponseTimeMs` (type: `integer`):

Health rule: a response slower than this is marked slow_response. Leave empty to disable.

## `timeoutSec` (type: `integer`):

Timeout per probe attempt.

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

Input cap: only the first N unique URLs are checked. The coverage summary reports the rest.

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

How many URLs are probed in parallel.

## `cacheMinutes` (type: `integer`):

A fresh observation from another run within this window is reused for free and marked fromCache. The cache key includes the health rules. Zero disables the cache.

## `timeBudgetSeconds` (type: `integer`):

Upper bound on the checking phase. When it is exhausted, the coverage summary reports omitted_by_time_budget. Leave empty for the default.

## Actor input object example

```json
{
  "urls": [
    "https://example.com",
    "https://apify.com"
  ],
  "timeoutSec": 15,
  "maxItems": 200,
  "concurrency": 8,
  "cacheMinutes": 5,
  "timeBudgetSeconds": 3300
}
```

# Actor output Schema

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

One row per URL

# 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",
        "https://apify.com"
    ]
};

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

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

```

## MCP server setup

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

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/tZEwMONJTCrYDUyHt/builds/XFuJNchZtIIkT9x3G/openapi.json
