Skip to content

Alerts

Outage alerts on Slack, Teams, Discord, webhooks, and email

OutageDeck checks every provider's official status source about every 10 minutes and fires an alert the moment something changes. This page is the whole setup: what fires, how to wire each channel, and exactly what we send.

What triggers an alert

Events are detected by diffing each ingestion run against the last known state, so an alert means something actually changed rather than a page being re-checked. Four event types exist:

  • incident.created

    A new incident appeared on the provider's official feed, and only if it started within the last 24 hours. Historical backfills never alert.

  • incident.status_changed

    An open incident moved through its lifecycle (investigating → identified → monitoring …).

  • incident.resolved

    An incident was resolved upstream, or disappeared from the provider's active feed.

  • provider.status_changed

    The provider's overall status changed (e.g., Operational → Partial outage). Never fired from or to an unknown state.

Each subscription can watch all providers (leave the scope empty) or specific providers (aws, cloudflare). Naming services (aws-ec2) or regions (us-east-1) alongside providers narrows what arrives from those providers, and can never reach a vendor the provider list does not contain. A narrowed subscription receives only incidents touching what it named, so provider-level status flips are deliberately skipped.

Regions are offered only for a provider whose feed actually separates them, and are named by that vendor's own code (AWS us-east-1, Google Cloud europe-west4, Oracle Cloud ap-sydney-1) rather than by the place name it prints beside it. An incident a vendor reports without a region still reaches a region-narrowed alert, because narrowing may only drop what a feed positively placed somewhere else. Narrowing is free on every plan, because it only ever means fewer alerts. Naming services or regions with no provider to anchor them is the cross-provider form, which spans the whole catalog and comes with a paid plan, as does the empty whole-stack scope.

Set up Slack, step by step

  1. Open api.slack.com/apps Create New AppFrom scratch, then name it something like “OutageDeck Alerts” and pick your workspace.
  2. In the app's sidebar choose Incoming Webhooks and switch them On.
  3. Click Add New Webhook to Workspace and pick the channel that should receive outage alerts (e.g. #outages).
  4. Copy the generated URL. It looks like https://hooks.slack.com/services/T…/B…/….
  5. On your account page, create a subscription with channel Slack, paste the URL as the target, then hit Send test. The test message lands in the channel within seconds.

Alerts arrive as a one-line summary with a link back to the incident timeline on OutageDeck.

Set up Discord, step by step

  1. In your server: Server Settings → Integrations → Webhooks → New Webhook.
  2. Pick the channel that should receive alerts.
  3. Click Copy Webhook URL (https://discord.com/api/webhooks/…).
  4. Create a subscription with channel Discord on your account page, paste the URL, and hit Send test.

Discord alerts render as an embed with a severity color and a link to the incident.

Set up Microsoft Teams, step by step

  1. In Teams, open the channel that should receive alerts and click ⋯ → Workflows.
  2. Pick the template “Post to a channel when a webhook request is received” and create it.
  3. Copy the request URL the workflow shows (it looks like https://prod-….logic.azure.com/workflows/…).
  4. Create a subscription with channel Microsoft Teams on your account page, paste the URL, and hit Send test.

Teams alerts arrive as an Adaptive Card carrying the incident title, the one-line summary, the provider's latest update when there is one, and a button to the incident timeline.

Generic webhooks and the JSON contract

Point a subscription at any HTTPS endpoint you control and OutageDeck POSTs this envelope on every matching event. The shape is versioned like the public API (meta.version: "v1") and will not change under you:

POST body: incident.created example

{
  "meta": {
    "version": "v1",
    "kind": "outagedeck.alert",
    "generatedAt": "2026-07-12T09:40:00.000Z"
  },
  "data": {
    "event": {
      "key": "incident:aws:INC-123:investigating",
      "type": "incident.created",
      "occurredAt": "2026-07-12T09:40:00.000Z",
      "summary": "🔴 AWS new incident: Increased error rates in us-east-1 (Investigating, Critical severity)",
      "url": "https://outagedeck.com/incidents/aws-increased-error-rates-us-east-1-2026-07-12",
      "provider": { "id": "provider_aws", "slug": "aws", "name": "AWS" },
      "incident": {
        "id": "incident_aws_INC-123",
        "slug": "aws-increased-error-rates-us-east-1-2026-07-12",
        "title": "Increased error rates in us-east-1",
        "status": "investigating",
        "previousStatus": null,
        "severity": "critical",
        "startedAt": "2026-07-12T09:31:00.000Z",
        "officialUrl": "https://health.aws.amazon.com/health/status",
        "affectedServiceSlugs": ["aws-ec2", "aws-s3"],
        "affectedServiceNames": ["EC2", "S3"],
        "latestUpdate": {
          "status": "investigating",
          "body": "We are investigating increased error rates and latencies in the us-east-1 Region.",
          "createdAt": "2026-07-12T09:38:00.000Z"
        }
      },
      "providerStatus": null
    }
  }
}

For provider.status_changed events, incident is null and providerStatus carries { "from": "...", "to": "..." }. affectedServiceNames and latestUpdate (the provider's newest human-written update, clamped to 600 characters) are optional; treat them as absent when the detecting run did not have them. Answer with any 2xx within 10 seconds; the body is ignored.

Verifying the signature

Every generic-webhook delivery carries an OutageDeck-Signature header: t=<unix seconds>,v1=<hex>. The signature is HMAC-SHA256 over <timestamp>:<raw body> using the secret shown once when you created the subscription. Without it, the only thing telling an OutageDeck delivery from anyone who learned your URL is the URL itself.

Verify a delivery (Node)

import { createHmac, timingSafeEqual } from "node:crypto";

// Verify BEFORE parsing. The signature covers the raw bytes, so a JSON
// round-trip through your framework produces a different string and a
// signature that never matches.
export function verifyOutageDeckDelivery(rawBody, header, secret) {
  if (typeof header !== "string") return false;

  // Every "key=value" pair in the header, trimmed on both sides.
  const pairs = header.split(",").map((part) => {
    const at = part.indexOf("=");
    return at === -1
      ? [part.trim(), ""]
      : [part.slice(0, at).trim(), part.slice(at + 1).trim()];
  });

  const timestamp = Number(pairs.filter(([key]) => key === "t").pop()?.[1]);
  if (!Number.isInteger(timestamp)) return false;

  // Refuse a replay. Absolute, so a delivery from the future is refused too.
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = Buffer.from(
    createHmac("sha256", secret)
      .update(timestamp + ":" + rawBody)
      .digest("hex"),
  );

  // Every v1= value is checked: during a rotation overlap we send one per
  // live secret, so whichever you still hold verifies. Byte lengths are
  // compared first, because timingSafeEqual throws on a mismatch.
  return pairs
    .filter(([key, value]) => key === "v1" && value.length > 0)
    .map(([, value]) => Buffer.from(value))
    .some(
      (signature) =>
        signature.length === expected.length &&
        timingSafeEqual(signature, expected),
    );
}

Two things worth getting right. Verify the raw body before your framework parses it, because re-serialising JSON changes the bytes and the signature will never match. And check every v1= value: when you rotate a secret we sign with both the old and the new one for an overlap window, so you can swap on your own schedule instead of during a coordinated cutover.

Secrets live on your account page. A webhook created before signing existed carries no signature until you generate a secret for it there, and rotating a secret keeps the previous one signing for 24 hours, so your endpoint can swap on its own schedule; rotating again inside those hours asks first, then retires the secret that rotation issued and keeps the older one verifying until the time you were given. The Slack, Discord and Teams channels carry no signature at all: those post to a vendor endpoint that authenticates by the URL and cannot verify one.

Email

Pick the Email channel and alerts go to your account address, the same one you sign in with. Each email carries the summary line, a link to the incident on OutageDeck, and the provider's official status-page entry when one exists.

Paid plans can also send to a devops group, an on-call alias, or a client. Add the address under Alert recipients on your account and we email it a confirmation link; it becomes selectable once someone reading that mailbox clicks. That click is the whole gate, and it is deliberately not something a plan can buy: OutageDeck will not mail an address that has not agreed to hear from it. Every alert to such an address carries a one-click link that removes it, with no sign-in and no need to ask you first, and delivery is re-checked on every run, so a removal or a lapsed plan stops the mail immediately.

Delivery guarantees

  • Claimed once per event. Every (subscription, event) pair is claimed in a delivery ledger before anything is sent, so two overlapping check runs can never both notify you about the same change.
  • A retry can still repeat one delivery. That claim is a guarantee about our bookkeeping, not about your inbox. A destination can accept a request and lose its reply on the way back, and the retry below then posts the identical body to a receiver that already has it. Every generic-webhook delivery carries an idempotency-key header holding that same (subscription, event) pair, with the event key percent-encoded so it is safe in a header, or hashed when a vendor id makes it long. Deduplicate on it and a repeat is never mistaken for a new event. Alert emails carry the same key to Resend, which resolves a repeated send to the first delivery, so a lost acknowledgement does not arrive as a second alert. Slack, Discord and Teams have nowhere to put a key, so a repeat there arrives as a second message.
  • Digest mode groups the message, not the ledger. Switch a Slack, Teams, Discord, or email subscription to a digest on your account page and a check that moves eleven things sends one message listing them, instead of eleven. Each event is still claimed separately before anything is sent, so nothing can appear in two digests. A check too large for one message is split into several rather than trimmed, so no event is left out of the listing it was recorded against. It is free on every plan, and it is not offered on the generic webhook channel: that payload is one event per delivery, and grouping it would change the shape your receiver parses.
  • 10-second timeout, one retry. A 5xx, a 429, a timeout or a dropped connection is retried once, waiting as long as a Retry-After asks up to five seconds, and the outcome is recorded either way. Two things are not retried: any other 4xx, because the destination understood the request and refused it, and a destination whose hostname will not resolve or resolves somewhere private, because the second answer would be the first one again.
  • Flap suppression. An incident transition is keyed by its target state, so the same change observed twice never alerts twice. Provider-level status keys reset daily.
  • Test any time. Every subscription has a Send test button on your account page that fires a clearly labeled synthetic alert through the exact production pipeline, so you can verify the wiring in seconds instead of waiting for a real outage.

Plans

Alerts start free: any account can create email alerts for up to 5 named providers, narrowed to their services and regions if you want, no card required. Paid plans add the Slack, Teams, Discord, and webhook channels, unlimited providers, and the whole-stack and cross-provider scopes: Starter includes 5 subscriptions, Pro 20, and Business 50. See pricing. Checkout takes a minute, and your API key plus the paid channels are live right after.