Plainwork

Plainwork / Status Endpoint Atlas / Slack Alerts for Vendor Status Page Changes

Get a Slack message when a vendor status page changes

You can alert your team when GitHub, Vercel, npm or any other vendor reports an incident without paying for a monitoring service. Most vendor status pages run on Atlassian Statuspage or a compatible service, and all of them serve the same JSON at /api/v2/summary.json. The script below reads that file for the hosts you give it and posts to a Slack incoming webhook only when the state changes, so a long incident does not repeat every few minutes. It needs Node 18 or newer and no packages. Discord accepts the same idea if you change the payload field from text to content and use a Discord webhook URL.

The script

Save it as status-to-slack.mjs. On 2026-10-15 I ran it with --dry-run against GitHub, npm and Vercel (all none), and against a made-up host, which printed an error line and did not stop the others. I also seeded the state file with an older state for GitHub to check that a change prints the message that would be sent. I did not post to a real Slack workspace, so test the first run with your own webhook.

// status-to-slack.mjs: post to a Slack incoming webhook when a vendor status page changes state.
// Node 18+, no dependencies. Usage:
//   SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... node status-to-slack.mjs www.githubstatus.com status.npmjs.org
//   node status-to-slack.mjs --dry-run www.githubstatus.com   (prints the message, posts nothing)
import { readFileSync, writeFileSync } from "node:fs";
const args = process.argv.slice(2);
const dry = args.includes("--dry-run");
const hosts = args.filter((a) => !a.startsWith("--"));
const hook = process.env.SLACK_WEBHOOK_URL;
if (!hosts.length || (!dry && !hook)) { console.error("usage: SLACK_WEBHOOK_URL=... node status-to-slack.mjs [--dry-run] <status-host> [...]"); process.exit(2); }
const STATE = "status-to-slack.state.json";
let prev = {};
try { prev = JSON.parse(readFileSync(STATE, "utf8")); } catch {}
const next = { ...prev };
for (const h of hosts) {
  try {
    const r = await fetch(`https://${h}/api/v2/summary.json`, { signal: AbortSignal.timeout(10000) });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const j = await r.json();
    const ind = j?.status?.indicator;
    if (typeof ind !== "string") throw new Error("no status.indicator");
    const names = (j.incidents || []).map((i) => i.name);
    const key = ind + "|" + names.join(",");   // alert on indicator or incident-list change, not every run
    const changed = prev[h] !== undefined && prev[h] !== key;
    const first = prev[h] === undefined && ind !== "none";
    console.log(`${h}: ${ind} (${names.length} open incident${names.length === 1 ? "" : "s"})${changed || first ? " -> notify" : ""}`);
    if (changed || first) {
      const text = `${j.page?.name || h}: ${j.status.description || ind} [${ind}]` + (names.length ? `\n- ${names.join("\n- ")}` : "") + `\n<${j.page?.url || "https://" + h}|status page>`;
      if (dry) console.log("DRY RUN message:\n" + text);
      else { const p = await fetch(hook, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text }) }); if (!p.ok) throw new Error(`Slack HTTP ${p.status}`); }
    }
    next[h] = key;
  } catch (e) { console.log(`${h}: ERROR ${e.message}`); }
}
if (!dry) writeFileSync(STATE, JSON.stringify(next, null, 1));

Set it up

  1. In Slack, create an incoming webhook for a channel (Slack's own documentation, "Sending messages using incoming webhooks", lists the steps). Keep the URL secret: anyone who has it can post to that channel.
  2. Dry run first: node status-to-slack.mjs --dry-run www.githubstatus.com. Nothing is posted and no state is saved.
  3. Run it on a schedule. On a server, cron works: */5 * * * * cd /opt/status && SLACK_WEBHOOK_URL=... node status-to-slack.mjs www.githubstatus.com status.npmjs.org. Every 5 minutes is polite; do not go under 60 seconds.
  4. In GitHub Actions, add a scheduled workflow, store the webhook as a repository secret, and cache or commit status-to-slack.state.json between runs. Without saved state the script cannot tell a change from a repeat. Scheduled runs can start several minutes late, so treat it as a heads-up, not a pager.

What it does and does not do

  • Alerts on change. It stores the indicator plus the names of open incidents per host. A new incident, a worse indicator or a recovery sends one message. The first run stays quiet for healthy hosts and sends a message for any host that is already not none.
  • Handles missing keys. (j.incidents || []) is deliberate: incident.io's compatible pages leave out the incidents array when it is empty. See the API guide for that and other differences.
  • Errors are logged, not alerted. A timeout or a 404 on one host prints an error and the rest still run. Add your own alert if a missing status page matters to you.
  • Declared incidents only. A status page is often updated some minutes after an outage begins, so this tells you what the vendor has acknowledged.
  • Noise. Large vendors are often minor because one component is degraded. If that is too chatty, skip messages for minor with a one-line check, and see the status values reference for the strings you will meet.

Finding the right hosts

The host is not always status.vendor.com: GitHub is www.githubstatus.com and Vercel is www.vercel-status.com. The Status Endpoint Atlas lists 246 hosts whose summary.json was fetched and parsed on 2026-10-09, with component counts, CORS and feed availability. To test a single host in your browser, use the free status page checker. To block a deploy instead of posting a message, see the deploy gate script.

More from the catalogue

Other small tools

See everything →