Vendor Status Normalizer avatar

Vendor Status Normalizer

Pricing

from $2.00 / 1,000 vendor status normalizeds

Go to Apify Store
Vendor Status Normalizer

Vendor Status Normalizer

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

Pricing

from $2.00 / 1,000 vendor status normalizeds

Rating

0.0

(0)

Developer

Richard k

Richard k

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Categories

Share

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

FieldTypeDefaultMeaning
vendorsstring[][]Names from the built-in alias table below (case-insensitive, a few synonyms like gh, claude, chatgpt, do work).
urlsstring[][]Root URLs of status pages not in the table, e.g. https://status.example.com. Only absolute http/https URLs.
maxConcurrencyinteger 1..205Vendors checked in parallel.
timeoutSecsinteger 1..6010Per-request timeout. A vendor that does not answer in time becomes an unsupported row instead of failing the run.
includeComponentsbooleantrueSet 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:

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

Two ready-made tasks live in .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:

{
"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_statusStatuspage indicatorInstatus page.status
operationalnoneUP
maintenancemaintenanceUNDERMAINTENANCE
degradedminorHASISSUES (no incident with worse impact)
partial_outagemajorHASISSUES + incident impact PARTIALOUTAGE
major_outagecriticalHASISSUES + incident impact MAJOROUTAGE
unknownanything 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:

{ "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)

EventCharged whenPrice
vendor-checkedone 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).

AliasStatus pageProviderLive probe 2026-09-03
githubhttps://www.githubstatus.com✅ statuspageoperational, 12 components
cloudflarehttps://www.cloudflarestatus.com✅ statuspagedegraded, 470 components
openaihttps://status.openai.com✅ statuspageoperational, 25 components
anthropichttps://status.anthropic.com✅ statuspageoperational, 6 components
stripehttps://status.stripe.com❌ unsupportedhttps://status.stripe.com/api/v2/summary.json: HTTP 404 ; https://status.stripe.com/summary.json: HTTP 404
twiliohttps://status.twilio.com✅ statuspagedegraded, 171 components
datadoghttps://status.datadoghq.com✅ statuspageoperational, 39 components
pagerdutyhttps://status.pagerduty.com❌ unsupportedhttps://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
atlassianhttps://status.atlassian.com✅ statuspageoperational, 0 components
vercelhttps://www.vercel-status.com✅ statuspageoperational, 56 components
netlifyhttps://www.netlifystatus.com✅ statuspageoperational, 39 components
digitaloceanhttps://status.digitalocean.com✅ statuspageoperational, 239 components
linodehttps://status.linode.com✅ statuspageoperational, 242 components
fastlyhttps://status.fastly.com❌ unsupportedhttps://status.fastly.com/api/v2/summary.json: HTTP 403 ; https://status.fastly.com/summary.json: HTTP 403
zoomhttps://status.zoom.us✅ statuspageoperational, 301 components
slackhttps://status.slack.com❌ unsupportedhttps://status.slack.com/api/v2/summary.json: HTTP 404 ; https://status.slack.com/summary.json: HTTP 404
dropboxhttps://status.dropbox.com✅ statuspageoperational, 12 components
hubspothttps://status.hubspot.com✅ statuspageoperational, 11 components
shopifyhttps://www.shopifystatus.com✅ statuspageoperational, 9 components
reddithttps://www.redditstatus.com✅ statuspageoperational, 10 components
bunnyhttps://status.bunny.net✅ statuspageoperational, 17 components
tailscalehttps://status.tailscale.com✅ statuspageoperational, 11 components
supabasehttps://status.supabase.com✅ statuspagedegraded, 27 components
renderhttps://status.render.com✅ statuspageoperational, 57 components
instatushttps://instat.us✅ instatusoperational, 18 components
philohttps://status.philo.com✅ instatusoperational, 6 components
restreamhttps://status.restream.io✅ instatusoperational, 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

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

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

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.