If your customers already look at an Atlassian Statuspage, you know the awkward gap: a monitor goes red, your phone buzzes, and the status page stays a reassuring green until someone logs in and flips the component by hand. Ten minutes later, the support queue has already filled with "is it just me?" Statuspage's API can change a component's status with one request, and CronAlert's webhook channel fires one request when a monitor goes down and one when it recovers. This guide puts a small Cloudflare Worker between them so the component turns red while the outage is still a few seconds old and turns green on its own when the monitor recovers.

A word on fit. CronAlert includes its own status pages on every plan, with components tied directly to monitors and no relay needed; if you are starting from nothing, use those. This post is for teams whose customers, contracts, or subscriber lists already live on Statuspage and who want CronAlert to be the detector behind it.

How the two sides fit

CronAlert's webhook channel has two settings, a URL and an optional signing secret, and sends one POST per transition with a JSON body: event (monitor.down or monitor.recovered), the monitor's name and url, the incident's startedAt and resolvedAt, and the triggering check's status code, response time, error message, and region. With a secret set, the body is signed with HMAC-SHA256 in the X-CronAlert-Signature header. The webhook integrations guide documents every field.

Statuspage's REST API lives at https://api.statuspage.io/v1/, authenticates with the header Authorization: OAuth <api_key>, and updates a component with PATCH /pages/{page_id}/components/{component_id} and a body of {"component":{"status":"major_outage"}}. The allowed statuses are operational, under_maintenance, degraded_performance, partial_outage, and major_outage; a successful update returns 200. Incidents are a separate resource: POST /pages/{page_id}/incidents creates one (201) and PATCH /pages/{page_id}/incidents/{incident_id} with "status":"resolved" closes it. Each key is limited to 1 request per second on a rolling 60-second window; 420 or 429 means you exceeded it.

So the relay has four jobs: verify the signature, find the component for the monitor, choose a status, and respect the rate limit.

Step 1: collect the Statuspage IDs

  1. Log in to manage.statuspage.io, click your avatar (bottom left) and open API info. Your Page ID is shown there. Create an API key if you do not have one (account owners only); keys created now carry an expiry of 1 to 365 days, so note the date.
  2. For each component you want automated, copy its component ID from the Components page (it is in the URL when you edit the component, or from GET /pages/{page_id}/components with your key).
  3. Decide the mapping. It does not have to be one-to-one: five monitors on different API routes can all map to a single "API" component, and a monitor that covers a secondary path can map with a softer status.

Step 2: deploy the relay

Create a Worker, paste the code below, edit the COMPONENTS table, and set three secrets with wrangler secret put: CRONALERT_WEBHOOK_SECRET (a random string you will also give CronAlert), STATUSPAGE_PAGE_ID, and STATUSPAGE_API_KEY.

// CronAlert webhook → Atlassian Statuspage component status
// monitor URL → component ID; monitors not listed here are ignored
const COMPONENTS = {
  "https://api.example.com/health": "abc123def456",
  "https://app.example.com/": "ghi789jkl012",
  "https://docs.example.com/": "mno345pqr678",
};

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("ok");
    const raw = await request.text();
    if (!(await verify(raw, request.headers.get("X-CronAlert-Signature"), env.CRONALERT_WEBHOOK_SECRET))) {
      return new Response("bad signature", { status: 401 });
    }
    const a = JSON.parse(raw);
    const componentId = COMPONENTS[a.monitor.url];
    if (!componentId) return new Response(null, { status: 204 });   // not a public component

    // "[degraded] Search API" → partial_outage instead of major_outage when it goes down
    const degraded = /\[degraded\]/i.test(a.monitor.name);
    const status = a.event === "monitor.down" ? (degraded ? "partial_outage" : "major_outage") : "operational";

    const res = await patchComponent(env, componentId, status);
    return new Response(null, { status: res.ok ? 204 : 502 });   // 502 → CronAlert logs a failed delivery
  },
};

async function patchComponent(env, id, status, attempt = 0) {
  const res = await fetch(`https://api.statuspage.io/v1/pages/${env.STATUSPAGE_PAGE_ID}/components/${id}`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", Authorization: `OAuth ${env.STATUSPAGE_API_KEY}` },
    body: JSON.stringify({ component: { status } }),
  });
  // 1 request/second per key: wait and retry once if a burst of alerts tripped the limit
  if ((res.status === 429 || res.status === 420) && attempt < 1) {
    await new Promise((r) => setTimeout(r, 1500));
    return patchComponent(env, id, status, attempt + 1);
  }
  return res;
}

async function verify(raw, header, secret) {
  if (!header || !secret) return false;
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(raw));
  const hex = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return hex.length === header.length && crypto.subtle.timingSafeEqual(new TextEncoder().encode(hex), new TextEncoder().encode(header));
}

Three details worth noticing. The signature is verified against the raw body with a constant-time comparison before anything is parsed, because the Worker URL is effectively public and a forged request could paint your status page red. Monitors absent from the table return 204 and do nothing, which is what keeps staging and internal monitors off the customer-facing page. And a failed Statuspage call returns 502 to CronAlert, so it appears in the webhook channel's notification log rather than vanishing.

Step 3: add the webhook channel in CronAlert

  1. Open Alert Channels, add a Webhook channel named Statuspage.
  2. Paste the Worker URL and the same signing secret you gave the Worker. Those are the channel's only two settings.
  3. Click Send test alert. The test payload's monitor URL is not in your table, so the Worker returns 204 and the channel shows a successful delivery; that confirms the signature and the plumbing. To see a component move, use the real test in the next section.

The channel belongs to the team, so every monitor in this team now reaches the Worker, and the table decides which of them matter. If you would rather keep the Statuspage channel on a separate team containing only public-facing monitors, alert routing with teams explains that layout.

Components only, or incidents too?

Changing a component's status changes the colour and the history on the page. It does not notify subscribers. Creating an incident does: every email and SMS subscriber to the page, or to that component, hears about it, and the incident has a public timeline you will be expected to update. That asymmetry is the whole argument for automating components and not incidents by default. A monitor that flaps for ninety seconds, a false positive, or a deploy that briefly returns 503 becomes a red component for ninety seconds, which is honest, instead of an incident email to ten thousand people, which is a conversation with your CEO.

If your process genuinely wants an incident opened automatically, do it carefully:

  • Confirm first. On the Team plan, multi-region monitors can alert only when N of M regions fail, so the webhook already represents a confirmed outage. Otherwise, have the Worker store the down event in Workers KV with a short TTL and only open the incident if no recovery arrives within, say, three minutes (a scheduled Worker handles the delay).
  • Create quietly. POST /pages/{page_id}/incidents with {"incident":{"name":"Degraded API availability","status":"investigating","body":"We are investigating errors on the API and will update shortly.","component_ids":["abc123def456"],"components":{"abc123def456":"major_outage"},"deliver_notifications":false}}. The incident appears on the page with the component attached; a human adds the first real update and decides whether to notify.
  • Resolve on recovery. Store the returned incident ID in KV keyed by the monitor URL; on monitor.recovered, PATCH it with "status":"resolved" and a body noting the duration from incident.startedAt to incident.resolvedAt. GET /pages/{page_id}/incidents/unresolved is the fallback if KV and reality disagree.

Whichever you choose, the person on call should still be paged through a channel built for paging; PagerDuty is native, and incident.io or Opsgenie use the same relay pattern as this post. The status page is for your customers, not your responders.

The rate limit in practice

One request per second per key sounds generous until a shared dependency fails: a provider outage takes twenty monitors down within one check cycle, CronAlert sends twenty webhooks, and the Worker tries twenty PATCHes in two seconds. Statuspage will refuse most of them with 429. The Worker above retries once after 1.5 seconds, which absorbs small bursts, and returns 502 for anything still refused, so the misses are visible. Two structural fixes help more than clever retries: map many monitors to few components, so the repeats are setting a status that is already set; and remember that recovery is usually staggered, so the operational updates arrive spread out on their own. If you need guaranteed delivery under large bursts, put a Cloudflare Queue between the fetch handler and the PATCH, which serialises the calls at a rate you control.

Testing

  1. With curl, PATCH one component to degraded_performance directly against the API with your key, watch it change on the page, and set it back. This confirms the key, the page ID, and the component ID.
  2. Add a spare monitor whose URL is in the table and point it at a URL that returns 500. Within one check interval the component should go major_outage. Fix the URL and watch it return to operational. Recoveries are where relays break (no status code, resolvedAt present), so do not skip the second half.
  3. Put an HTTP monitor on the Worker URL itself (a GET returns ok), per monitoring webhook receivers, so a deleted Worker or an expired API key becomes an alert.

Frequently asked questions

Is there a native Statuspage channel?

No. The relay is about thirty lines and fits Cloudflare's free Worker tier; the webhook channel is available on every CronAlert plan.

Why not use Statuspage's own automation (email or Datadog, New Relic, and PagerDuty integrations)?

Statuspage can update components from several monitoring vendors' integrations. CronAlert is not one of them, and the webhook relay gives you more control anyway: which monitors count, which status each sets, and whether incidents are opened at all.

Can I set under_maintenance during a deploy?

Not from CronAlert alerts, since a maintenance window suppresses alerts rather than sending a different one. Set under_maintenance from your deploy pipeline with the same PATCH, and let the relay's operational on recovery, or a final pipeline step, clear it.

Does a red component affect Statuspage's uptime percentage for that component?

Yes, component uptime history records the time spent in each status, which is exactly why automated components are more honest than manually flipped ones. If a monitor is noisy, fix the monitor rather than leaving it off the page; reducing alert noise covers how.

What if we move off Statuspage later?

Point the monitors at a CronAlert status page, which needs no relay, and delete the Worker. The monitors and their history stay where they are.

Get started

A page ID, an API key, a component map, and one Worker, and your Statuspage components change colour within seconds of CronAlert detecting the problem and change back on their own when it clears. Create a free CronAlert account and start with the curl test. Related reading: status pages vs uptime monitoring, incident response workflows, SLA reporting from uptime data, and webhook alert integrations.