Vendor Status Normalizer
Pricing
from $2.00 / 1,000 vendor status normalizeds
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
Maintained by CommunityActor 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_atand incidentstarted_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:
{"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_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:
{ "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.jsonreturns 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_statusis the provider's page-level verdict, not derived from the incident list. A Statuspage vendor can list an active incident while its indicator is stillnone(seen live on OpenAI and Linode on 2026-09-03). Useeffective_statusif 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 changeeffective_status. - Component lists can be large (Cloudflare reports ~470 components). Each row is capped at 2 MiB of source JSON; anything bigger becomes
unsupportedwith atoo_largeerror. - 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
unsupportedrow; 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 sat worst -- the second endpoint is not probed once the first one proves the host is unreachable -- about 31 s at the defaulttimeoutSecs=10, and about 181 s at the maximumtimeoutSecs=60. RaisemaxConcurrencyor lowertimeoutSecsif a batch of dead hosts would stall the run.
Security
User-supplied URLs are treated as untrusted input:
- Only absolute
http/httpsURLs on ports 80/443, no credentials in the URL. - Loopback, link-local (including cloud metadata
169.254.169.254), RFC 1918, CGNAT100.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
/apiwith its own docs). Instatus documentssummary.jsonhere: https://instatus.com/help/api/status-page-summary 🧪 (URL not re-verified this shift).
Running locally
cd /path/to/vendor-status-normalizernpm installecho '{"vendors":["github","cloudflare"]}' > storage/key_value_stores/default/INPUT.jsonnpm 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 loginapify 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.