# Vendor Status Normalizer (`agate_seafloor/vendor-status-normalizer`) Actor

Turn public vendor status pages (Atlassian Statuspage, Instatus) into one normalized JSON row per vendor: overall status, components, active incidents, scheduled maintenance.

- **URL**: https://apify.com/agate\_seafloor/vendor-status-normalizer.md
- **Developed by:** [Richard k](https://apify.com/agate_seafloor) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 vendor status normalizeds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Vendor Status Normalizer

Turn the public status pages of the vendors you depend on into **one normalized JSON row per vendor**, in one call. No browser, no scraping of HTML: the Actor reads the public JSON that Atlassian Statuspage and Instatus pages already expose, and maps their different vocabularies onto one schema.

Typical uses:

- Feed a "third-party health" panel in your own dashboard or on-call tooling.
- Let an AI agent or MCP client ask "is anything we depend on degraded right now?" and get a machine-readable answer.
- Correlate your incident timeline with vendor incidents (every row carries `fetched_at` and incident `started_at`).

### Input

| Field | Type | Default | Meaning |
|---|---|---|---|
| `vendors` | string\[] | `[]` | Names from the built-in alias table below (case-insensitive, a few synonyms like `gh`, `claude`, `chatgpt`, `do` work). |
| `urls` | string\[] | `[]` | Root URLs of status pages not in the table, e.g. `https://status.example.com`. Only absolute `http`/`https` URLs. |
| `maxConcurrency` | integer 1..20 | `5` | Vendors checked in parallel. |
| `timeoutSecs` | integer 1..60 | `10` | Per-request timeout. A vendor that does not answer in time becomes an `unsupported` row instead of failing the run. |
| `includeComponents` | boolean | `true` | Set `false` to leave `components` empty. Cloudflare alone reports ~470 components; skip them if you only need status + incidents. |

At least one of `vendors` / `urls` must be non-empty. Duplicates are collapsed. Example:

```json
{
  "vendors": ["github", "cloudflare", "openai"],
  "urls": ["https://status.supabase.com"],
  "maxConcurrency": 5
}
```

Two ready-made tasks live in [`.actor/examples/`](.actor/examples/): a core-SaaS set and a mixed vendors+urls set.

### Output

One dataset item per requested vendor or URL, always the same keys:

```json
{
  "vendor": "github",
  "source_url": "https://www.githubstatus.com",
  "provider": "statuspage",
  "overall_status": "operational",
  "effective_status": "degraded",
  "indicator": "none",
  "status_description": "All Systems Operational",
  "page_name": "GitHub",
  "components": [{ "name": "Git Operations", "status": "operational" }],
  "active_incidents": [
    { "name": "Incidents with Actions", "status": "identified", "impact": "minor",
      "started_at": "2026-09-01T10:02:00.000Z", "url": "https://stspg.io/abc123" }
  ],
  "scheduled_maintenances": [
    { "name": "Database upgrade", "status": "scheduled", "impact": "maintenance",
      "scheduled_for": "2026-09-16T04:00:00.000Z", "scheduled_until": "2026-09-16T06:00:00.000Z", "url": "https://stspg.io/def456" }
  ],
  "fetched_at": "2026-09-03T00:00:00.000Z",
  "probed_url": "https://www.githubstatus.com/api/v2/summary.json"
}
```

`overall_status` is the provider's page-level verdict, normalized. `effective_status` is the worst of that and every active incident's impact, for the common case where a vendor keeps the banner green while an incident is open. `indicator` is the provider's raw token so you can still see the original.

| `overall_status` | Statuspage `indicator` | Instatus `page.status` |
|---|---|---|
| `operational` | `none` | `UP` |
| `maintenance` | `maintenance` | `UNDERMAINTENANCE` |
| `degraded` | `minor` | `HASISSUES` (no incident with worse impact) |
| `partial_outage` | `major` | `HASISSUES` + incident impact `PARTIALOUTAGE` |
| `major_outage` | `critical` | `HASISSUES` + incident impact `MAJOROUTAGE` |
| `unknown` | anything else, or `unsupported` rows | |

`components[].status`, `active_incidents[].status` and `.impact` keep the provider's native strings (Statuspage lowercase: `operational`, `degraded_performance`, `identified`, `minor`...; Instatus uppercase: `OPERATIONAL`, `INVESTIGATING`, `MAJOROUTAGE`...). Normalizing those would lose information; filter on `provider` if you need to branch.

Rows where no supported API was found look like this and are **not charged**:

```json
{ "vendor": "stripe", "source_url": "https://status.stripe.com", "provider": "unsupported",
  "overall_status": "unknown", "indicator": null, "components": [], "active_incidents": [],
  "scheduled_maintenances": [], "fetched_at": "...", "error": "custom status page, no public summary.json (HTTP 404 on both known endpoints)" }
```

### Pricing (pay per event)

| Event | Charged when | Price |
|---|---|---|
| `vendor-checked` | one vendor produced a row with `provider` = `statuspage` or `instatus` **and** `overall_status` is not `unknown` | $0.002 per row (i.e. $2 per 1,000 vendor checks) |

`unsupported` rows, rows whose status token could not be mapped, unknown vendor names and invalid URLs are free. There is no per-run or per-start fee beyond Apify's platform `apify-actor-start` event.

Set a "maximum total charge" on the run if you want a hard budget cap. When it is reached the Actor stops fetching; every vendor it did not get to is still written as a free row with `error: "skipped: run budget (max total charge) exhausted ..."`, so you can see exactly what was left out.

### Built-in vendor table

Every entry was probed live on 2026-09-03 (ledger: `docs/vendor-verification-2026-09-03.json`).

| Alias | Status page | Provider | Live probe 2026-09-03 |
|---|---|---|---|
| `github` | https://www.githubstatus.com | ✅ statuspage | operational, 12 components |
| `cloudflare` | https://www.cloudflarestatus.com | ✅ statuspage | degraded, 470 components |
| `openai` | https://status.openai.com | ✅ statuspage | operational, 25 components |
| `anthropic` | https://status.anthropic.com | ✅ statuspage | operational, 6 components |
| `stripe` | https://status.stripe.com | ❌ unsupported | https://status.stripe.com/api/v2/summary.json: HTTP 404 ; https://status.stripe.com/summary.json: HTTP 404 |
| `twilio` | https://status.twilio.com | ✅ statuspage | degraded, 171 components |
| `datadog` | https://status.datadoghq.com | ✅ statuspage | operational, 39 components |
| `pagerduty` | https://status.pagerduty.com | ❌ unsupported | https://status.pagerduty.com/api/v2/summary.json: HTTP 404 ; https://status.pagerduty.com/summary.json: HTTP 200 but body is not a recognised summary document |
| `atlassian` | https://status.atlassian.com | ✅ statuspage | operational, 0 components |
| `vercel` | https://www.vercel-status.com | ✅ statuspage | operational, 56 components |
| `netlify` | https://www.netlifystatus.com | ✅ statuspage | operational, 39 components |
| `digitalocean` | https://status.digitalocean.com | ✅ statuspage | operational, 239 components |
| `linode` | https://status.linode.com | ✅ statuspage | operational, 242 components |
| `fastly` | https://status.fastly.com | ❌ unsupported | https://status.fastly.com/api/v2/summary.json: HTTP 403 ; https://status.fastly.com/summary.json: HTTP 403 |
| `zoom` | https://status.zoom.us | ✅ statuspage | operational, 301 components |
| `slack` | https://status.slack.com | ❌ unsupported | https://status.slack.com/api/v2/summary.json: HTTP 404 ; https://status.slack.com/summary.json: HTTP 404 |
| `dropbox` | https://status.dropbox.com | ✅ statuspage | operational, 12 components |
| `hubspot` | https://status.hubspot.com | ✅ statuspage | operational, 11 components |
| `shopify` | https://www.shopifystatus.com | ✅ statuspage | operational, 9 components |
| `reddit` | https://www.redditstatus.com | ✅ statuspage | operational, 10 components |
| `bunny` | https://status.bunny.net | ✅ statuspage | operational, 17 components |
| `tailscale` | https://status.tailscale.com | ✅ statuspage | operational, 11 components |
| `supabase` | https://status.supabase.com | ✅ statuspage | degraded, 27 components |
| `render` | https://status.render.com | ✅ statuspage | operational, 57 components |
| `instatus` | https://instat.us | ✅ instatus | operational, 18 components |
| `philo` | https://status.philo.com | ✅ instatus | operational, 6 components |
| `restream` | https://status.restream.io | ✅ instatus | operational, 27 components |

#### Known unsupported

These vendors run custom status sites without a public Statuspage/Instatus JSON API. Passing them returns an `unsupported` row (free) rather than an error:

- **stripe**: HTTP 404 on both known endpoints.
- **pagerduty**: `/summary.json` returns 200 but a different document shape.
- **fastly**: edge WAF answers HTTP 403 to non-browser clients.
- **slack**: has its own API (`https://status.slack.com/api/v2.0.0/current`), not Statuspage/Instatus.

Want one of these? Open an issue on the Actor page; the vendor table is a plain JS object and easy to extend.

### How it probes a raw URL

```text
https://status.example.com
  |
  +--> GET /api/v2/summary.json      (Atlassian Statuspage)
  |       200 + recognised shape --> row (provider=statuspage)
  |
  +--> GET /summary.json             (Instatus)
          200 + recognised shape --> GET /v2/components.json (best effort)
                                 --> row (provider=instatus)
  |
  +--> nothing matched --> row (provider=unsupported, error explains each attempt)
```

Known vendors skip the probe and go straight to their provider's endpoint, so one vendor costs one HTTP request (Instatus: two).

### Limits

- **Only Atlassian Statuspage and Instatus** are supported. Custom status sites (Stripe, Slack, AWS Health, Google Cloud, Azure) are reported as `unsupported`. Pages that block non-browser clients (Fastly) are too; the Actor does not spoof browsers or bypass WAFs by design.
- **`overall_status` is the provider's page-level verdict, not derived from the incident list.** A Statuspage vendor can list an active incident while its indicator is still `none` (seen live on OpenAI and Linode on 2026-09-03). Use `effective_status` if an open incident should count as degraded regardless of the banner.
- **Alias table is small on purpose** (27 entries, all verified live). Anything else goes through `urls`; the error message on an unknown name says so.
- **Statuspage incident `impact: "none"`** (informational posts) does not change `effective_status`.
- **Component lists can be large** (Cloudflare reports ~470 components). Each row is capped at 2 MiB of source JSON; anything bigger becomes `unsupported` with a `too_large` error.
- **No history.** Each run is a snapshot at `fetched_at`. Schedule the Actor if you want a time series.
- **Rate limits belong to the vendors.** The Actor makes 1-3 requests per vendor per run with a descriptive `User-Agent`. Polling dozens of vendors every few seconds is your responsibility to keep reasonable.
- Redirects are followed (max 5); the final URL is not reported separately.
- **Transient failures are retried** up to 3 attempts with exponential backoff (300 ms, 600 ms) for timeouts, connection errors, HTTP 429 and 5xx. Nothing else is retried. A vendor that stays down becomes one `unsupported` row; the other vendors in the run are unaffected.
- **A dead host** (one that times out or refuses the connection) **occupies one concurrency slot for `3 x timeoutSecs + 0.9 s`** at worst -- the second endpoint is not probed once the first one proves the host is unreachable -- about 31 s at the default `timeoutSecs=10`, and about 181 s at the maximum `timeoutSecs=60`. Raise `maxConcurrency` or lower `timeoutSecs` if a batch of dead hosts would stall the run.

### Security

User-supplied URLs are treated as untrusted input:

- Only absolute `http`/`https` URLs on ports 80/443, no credentials in the URL.
- Loopback, link-local (including cloud metadata `169.254.169.254`), RFC 1918, CGNAT `100.64/10`, reserved and multicast ranges are refused, both as IPv4 literals and in their IPv6-embedded forms (`::ffff:10.0.0.1`, NAT64, 6to4).
- Internal-looking hostnames (`localhost`, `*.local`, `*.internal`, `metadata.google.internal`, single-label names) are refused before DNS.
- **DNS answers are checked at connect time**, not just the hostname: a public name that resolves to a private address (e.g. `10.0.0.1.nip.io`) is refused, and a name that returns a mix of public and private addresses is refused too. This also covers every redirect hop, since redirects are followed manually (max 5) and re-validated.
- Responses are capped at 2 MiB and each request at `timeoutSecs`.

Blocked targets become an `unsupported` row with `error: "blocked: ..."` and are free.

### Privacy and terms

- The Actor only reads **public, unauthenticated** status endpoints that vendors publish for exactly this purpose. It sends no cookies, no credentials and stores nothing beyond the dataset rows you see.
- Your input (vendor names / URLs) is processed in the run and not sent anywhere else.
- Status page content belongs to the respective vendors and is subject to their terms; you are responsible for how you use it downstream.
- Atlassian Statuspage documents its public API here: https://support.atlassian.com/statuspage/docs/ (each page also exposes `/api` with its own docs). Instatus documents `summary.json` here: https://instatus.com/help/api/status-page-summary 🧪 (URL not re-verified this shift).

### Running locally

```bash
cd /path/to/vendor-status-normalizer
npm install
echo '{"vendors":["github","cloudflare"]}' > storage/key_value_stores/default/INPUT.json
npm start                      # rows land in storage/datasets/default/
npm test                       # 56 tests, all against fixtures, zero network
```

To exercise pay-per-event locally the SDK accepts test-mode environment variables (`ACTOR_TEST_PAY_PER_EVENT=1`, `ACTOR_USE_CHARGING_LOG_DATASET=1`); charged events are then written to `storage/datasets/charging_log/`.

### Deploying

```bash
apify login
apify push
```

Then in Apify Console open the Actor, **Publication → Monetization → Pay per event** and add the event from `docs/ppe-events.json` (`vendor-checked`, $0.002). 🧪 The console flow was not exercised while preparing this build; field names follow https://docs.apify.com/platform/actors/publishing/monetize/pay-per-event as read on 2026-09-03.

# Actor input Schema

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

Names from the built-in alias table, e.g. github, cloudflare, openai, anthropic, datadog. Case-insensitive. See README for the full list.

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

Root URLs of status pages not in the alias table, e.g. https://status.example.com. The Actor probes /api/v2/summary.json (Statuspage) then /summary.json (Instatus).

## `maxConcurrency` (type: `integer`):

How many vendors to check in parallel.

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

Seconds to wait for each status page request before marking the vendor as unsupported.

## `includeComponents` (type: `boolean`):

Set to false to leave the components array empty. Big pages (Cloudflare ~470 components) make rows large; turn this off if you only need overall status and incidents.

## Actor input object example

```json
{
  "vendors": [
    "github",
    "cloudflare",
    "openai"
  ],
  "urls": [
    "https://status.supabase.com"
  ],
  "maxConcurrency": 5,
  "timeoutSecs": 10,
  "includeComponents": true
}
```

# Actor output Schema

## `rows` (type: `string`):

Overall status, effective status, components, active incidents and scheduled maintenances for each requested vendor. Rows with provider=unsupported are free.

# 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": [
        "github",
        "cloudflare",
        "openai"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("agate_seafloor/vendor-status-normalizer").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": [
        "github",
        "cloudflare",
        "openai",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("agate_seafloor/vendor-status-normalizer").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": [
    "github",
    "cloudflare",
    "openai"
  ]
}' |
apify call agate_seafloor/vendor-status-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,agate_seafloor/vendor-status-normalizer"
        }
    }
}

```

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/fxzZ2fxNIcFPZlzWu/builds/08ogsBxJo5osJgzRG/openapi.json
