# Shadow Outage Ledger: Vendor-Claimed Status (`kingii98/shadow-outage-ledger-vendor-claimed-status`) Actor

Run this on a short schedule. Each run reads what the vendor status page claims and measures a public endpoint of the same service from outside. Each check writes one typed event: MATCH, SHADOW\_OUTAGE when the endpoint fails and the vendor still claims th

- **URL**: https://apify.com/kingii98/shadow-outage-ledger-vendor-claimed-status.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 run\_starts

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

## Shadow Outage Ledger: Vendor-Claimed Status Against Measured Reachability

A vendor status page tells you what the vendor wants to say. This Actor tells you
what you measured. Run it on a short schedule. Each run reads the public status
document of each vendor and, at the same time, probes a public endpoint of the
same service from outside. It then writes one typed event for each vendor.

The value is the unbroken record. The open dispute windows and the last vendor
claim live in the key-value store between runs, so a window that opens at 02:10
and closes at 03:40 is one window with one length, and not 6 unrelated rows.

### What each run writes

One typed event for each vendor checked:

| Event | Meaning |
| --- | --- |
| `MATCH` | Nothing to dispute. The endpoint answers as expected, or the endpoint fails and the vendor already declares the incident. |
| `SHADOW_OUTAGE` | The endpoint fails for you and the vendor still claims that all systems operate. The first such check opens a mismatch window. |
| `LATE_ACKNOWLEDGEMENT` | The endpoint still fails and the vendor now declares the incident. The window closes, and the row carries the delay from your first failed check. |
| `RECOVERED` | The endpoint answers as expected again while a window is open. The window closes with its full length. |
| `CHECK_ERROR` | The check could not be made: the status document is unreadable, or the probe target is refused by the private-address rule. No window opens, because a dispute window must not rest on a guess. |

Each event holds both timestamps (when you measured, and when the vendor page
last changed), the measured status code, the latency in milliseconds, the number
of probes, the vendor status text and the state of the mismatch window. Each run
also writes one `run_summary` record with the counts.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `vendors` | 3 public examples | 1 to 25 vendors. Each entry: `name`, `status_json_url`, `probe_url`, `probe_method` (`GET` or `HEAD`), `expect_status` (default `[200]`). |
| `checkIntervalMinutes` | `15` | The interval of your schedule. The run measures the gap since the last check against it, and each event says whether the ledger is unbroken. |
| `maxProbeAttempts` | `3` | The largest number of probes of one target in one run. A healthy target costs one request. |
| `requestTimeoutSeconds` | `20` | Timeout for the status read and for each probe. |
| `concurrency` | `5` | The largest number of vendors checked at the same time. |
| `maxRedirects` | `5` | The largest number of redirects for one read or one probe. Every hop is checked again. |
| `maxResponseBytes` | `2000000` | The largest body this Actor reads. |
| `stateStoreName` | `shadow-outage-ledger-state` | The named key-value store that carries the ledger. Give a second schedule a second name. |
| `stateRetentionHours` | `720` | How long a vendor stays in the stored ledger after the input stops naming it. |

Every field has a default, so a run with an empty input succeeds and checks the
three public examples.

`status_json_url` must be a public Statuspage v2 status document (normally
`https://status.<vendor>.com/api/v2/status.json`) or an Instatus summary
document. `probe_url` must be a public endpoint that needs no credentials.

### State between runs

Each run gets its own **default** key-value store, so a ledger kept there would
start again on every run. The ledger therefore lives in a **named** store: the
record `SHADOW_OUTAGE_LEDGER_STATE` in the store that `stateStoreName` names.
The account keeps that store between runs.

The record holds, for each vendor, the last claim, the last measurement and the
open mismatch window. Two rules keep it safe over a long life:

- The record names its schema version. A record of another version is dropped,
  the ledger starts again, and the run summary says so.
- The record is pruned on every write. A vendor the input no longer names is
  kept for `stateRetentionHours` and is then dropped, and the record never holds
  more than 200 vendors.

Two schedules that share one store name write over each other. Give each
schedule its own name.

### Pricing: pay per event

| Event | Unit | Counted |
| --- | --- | --- |
| `run_start` | one run | Once for each run, charged first. It covers the container start and the read and write of the ledger store. |
| `vendor_check` | one vendor checked in one run | Once for each vendor actually checked. It covers one status read and up to 3 probes. A vendor entry this Actor cannot read, and a vendor the charge limit does not cover, are not checked and are not charged. |
| `mismatch_window_event` | one window opened or closed | Once when a mismatch window opens, and once when it closes. A long outage therefore costs two, and not one for each check inside it. |

The run never checks more vendors than its charge limit allows. A vendor left
out gets a `skipped_vendor` row.

### What this Actor does not do

It uses HTTP only. It uses no browser, no proxy, no credentials and no paid API.
It refuses a target that resolves to a private or reserved address, at every
redirect hop.

A business verdict is never a failed run. A shadow outage, an unreadable status
page, a refused probe target and an input that breaks a bound are all reported
in the dataset and in the status message, and the run ends SUCCEEDED. A FAILED
run means a real malfunction.

### Development

```bash
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `vendors` (type: `array`):

1 to 25 vendors. Each entry is an object with 'name', 'status\_json\_url', 'probe\_url', 'probe\_method' and 'expect\_status'. 'status\_json\_url' must be a public Statuspage v2 status document, normally https://status.<vendor>.com/api/v2/status.json, or an Instatus summary document. 'probe\_url' must be a public endpoint of the service that needs no credentials; the run measures this URL and compares the result with the vendor claim. 'probe\_method' is GET or HEAD, and it defaults to GET. 'expect\_status' lists the HTTP status codes that count as a healthy answer, and it defaults to \[200]. An entry this Actor cannot read gets its own row and does not stop the run.

## `checkIntervalMinutes` (type: `integer`):

The interval of the schedule that runs this Actor. The run does not wait; it uses this number to measure the gap since the last check. A gap of more than two intervals means that the schedule missed a run, and each event then says that the ledger has a hole. Set this to the same value as your schedule.

## `maxProbeAttempts` (type: `integer`):

The largest number of probes of one target in one run. A probe that answers as expected ends the repeats, so a healthy vendor costs one request. One short failure is noise, and a repeat keeps the ledger free of a window that opens on it.

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

Timeout for the status read and for each probe. A probe that reaches this timeout counts as a failure, because a user waits the same time.

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

The largest number of vendors checked at the same time. A public status page refuses a burst, so a small number is safer.

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

The largest number of redirects followed for one read or one probe. Every hop is checked again against the private-address rule. A probe stops at the first status code that your 'expect\_status' list names, so a buyer who expects 301 measures the redirect itself.

## `maxResponseBytes` (type: `integer`):

The largest body this Actor reads. A status document larger than the cap is refused. A probe keeps its status code and its latency, because the cap stops the body only.

## `stateStoreName` (type: `string`):

The named key-value store that carries the ledger between runs. Each run gets its own default store, so the ledger cannot live there. Keep one name for one schedule. Give a second schedule a second name, or the two schedules write over each other. The name must be 3 to 63 characters, and it must use letters, numbers and inner dashes only.

## `stateRetentionHours` (type: `integer`):

The ledger keeps the open windows and the last vendor claim in the key-value store between runs. A vendor that the input no longer names is kept for this many hours and is then dropped, so the stored state does not grow without a bound. The record never holds more than 200 vendors.

## Actor input object example

```json
{
  "vendors": [
    {
      "name": "GitHub",
      "status_json_url": "https://www.githubstatus.com/api/v2/status.json",
      "probe_url": "https://github.com/robots.txt",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    },
    {
      "name": "Cloudflare",
      "status_json_url": "https://www.cloudflarestatus.com/api/v2/status.json",
      "probe_url": "https://www.cloudflare.com/cdn-cgi/trace",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    },
    {
      "name": "npm",
      "status_json_url": "https://status.npmjs.org/api/v2/status.json",
      "probe_url": "https://registry.npmjs.org/-/ping",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    }
  ],
  "checkIntervalMinutes": 15,
  "maxProbeAttempts": 3,
  "requestTimeoutSeconds": 20,
  "concurrency": 5,
  "maxRedirects": 5,
  "maxResponseBytes": 2000000,
  "stateStoreName": "shadow-outage-ledger-state",
  "stateRetentionHours": 720
}
```

# Actor output Schema

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

One typed event for each vendor checked in this run, with both timestamps, the measured status code, the latency, the vendor status text and the mismatch window, plus one run-summary record.

## `shadowOutages` (type: `string`):

The checks where the probe target failed for you and the vendor still claimed that all systems operate. These are the rows that support a dispute.

## `lateAcknowledgements` (type: `string`):

The checks where the vendor declared the incident after your first failed probe. Each row carries the delay in minutes.

## `openWindows` (type: `string`):

The checks that opened or closed a mismatch window. These are the rows the pay-per-event charge 'mismatch\_window\_event' counts.

# 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 = {
    "vendors": [
        {
            "name": "GitHub",
            "status_json_url": "https://www.githubstatus.com/api/v2/status.json",
            "probe_url": "https://github.com/robots.txt",
            "probe_method": "GET",
            "expect_status": [
                200
            ]
        },
        {
            "name": "Cloudflare",
            "status_json_url": "https://www.cloudflarestatus.com/api/v2/status.json",
            "probe_url": "https://www.cloudflare.com/cdn-cgi/trace",
            "probe_method": "GET",
            "expect_status": [
                200
            ]
        },
        {
            "name": "npm",
            "status_json_url": "https://status.npmjs.org/api/v2/status.json",
            "probe_url": "https://registry.npmjs.org/-/ping",
            "probe_method": "GET",
            "expect_status": [
                200
            ]
        }
    ],
    "checkIntervalMinutes": 15,
    "maxProbeAttempts": 3,
    "requestTimeoutSeconds": 20,
    "concurrency": 5,
    "maxRedirects": 5,
    "maxResponseBytes": 2000000,
    "stateStoreName": "shadow-outage-ledger-state",
    "stateRetentionHours": 720
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/shadow-outage-ledger-vendor-claimed-status").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 = {
    "vendors": [
        {
            "name": "GitHub",
            "status_json_url": "https://www.githubstatus.com/api/v2/status.json",
            "probe_url": "https://github.com/robots.txt",
            "probe_method": "GET",
            "expect_status": [200],
        },
        {
            "name": "Cloudflare",
            "status_json_url": "https://www.cloudflarestatus.com/api/v2/status.json",
            "probe_url": "https://www.cloudflare.com/cdn-cgi/trace",
            "probe_method": "GET",
            "expect_status": [200],
        },
        {
            "name": "npm",
            "status_json_url": "https://status.npmjs.org/api/v2/status.json",
            "probe_url": "https://registry.npmjs.org/-/ping",
            "probe_method": "GET",
            "expect_status": [200],
        },
    ],
    "checkIntervalMinutes": 15,
    "maxProbeAttempts": 3,
    "requestTimeoutSeconds": 20,
    "concurrency": 5,
    "maxRedirects": 5,
    "maxResponseBytes": 2000000,
    "stateStoreName": "shadow-outage-ledger-state",
    "stateRetentionHours": 720,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/shadow-outage-ledger-vendor-claimed-status").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 '{
  "vendors": [
    {
      "name": "GitHub",
      "status_json_url": "https://www.githubstatus.com/api/v2/status.json",
      "probe_url": "https://github.com/robots.txt",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    },
    {
      "name": "Cloudflare",
      "status_json_url": "https://www.cloudflarestatus.com/api/v2/status.json",
      "probe_url": "https://www.cloudflare.com/cdn-cgi/trace",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    },
    {
      "name": "npm",
      "status_json_url": "https://status.npmjs.org/api/v2/status.json",
      "probe_url": "https://registry.npmjs.org/-/ping",
      "probe_method": "GET",
      "expect_status": [
        200
      ]
    }
  ],
  "checkIntervalMinutes": 15,
  "maxProbeAttempts": 3,
  "requestTimeoutSeconds": 20,
  "concurrency": 5,
  "maxRedirects": 5,
  "maxResponseBytes": 2000000,
  "stateStoreName": "shadow-outage-ledger-state",
  "stateRetentionHours": 720
}' |
apify call kingii98/shadow-outage-ledger-vendor-claimed-status --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/shadow-outage-ledger-vendor-claimed-status"
        }
    }
}
```

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/BBAsQmp8AQduuFwqP/builds/qI9v7CnfWIQ4SzJyO/openapi.json
