Skip to content

Alert channel

Signed webhooks for down, recovery and SSL events

Webhooks send every alert as JSON to a URL you control, so you can open tickets, page through your own tooling or record outages anywhere.

Updated 25 October 2026 · By the Upcertain team

Set it up

  1. In Upcertain, open Settings, Alert channels and choose Webhook. Owners, admins and editors can add channels.
  2. Paste the webhook URL. Use an https:// address that is reachable from the internet; private and local addresses are refused. Add a signing secret so you can verify each request.
  3. Pick the events to send: Down, Up (recovery) and SSL certificate expiry.
  4. Tick Pre-select on new monitors if this should be the default for monitors you create from now on. Existing monitors keep their own choice.
  5. Save, then use Send a test in the channel list to post a test message.
  6. Open each monitor that should alert here and add the channel under its alert settings, with an optional delay and repeat interval.

Events

EventSent when
monitor.downAn incident opens after a confirmed failure
monitor.still_downA reminder, if the monitor's channel has a repeat interval
monitor.upThe monitor recovers and the incident resolves
ssl.expiringA certificate reaches one of its warning days
monitors.down_groupedAlert grouping combined several failures into one alert
test.channel, test.down, test.upYou sent a test from the app

Request

Each delivery is a POST with a JSON body and these headers:

  • X-Upcertain-Event: the event name.
  • X-Upcertain-Timestamp: Unix seconds when the request was signed.
  • X-Upcertain-Delivery: a unique id; the same id is reused when a delivery is retried, so you can ignore duplicates.
  • X-Upcertain-Signature: v1= followed by the hex HMAC-SHA256 of timestamp + "." + body, keyed with your signing secret. Sent only when the channel has a secret.
{
  "delivery_id": "6f1c...",
  "sent_at": "2026-10-25T09:14:03.000Z",
  "event": "monitor.down",
  "title": "Down: payments-api",
  "summary": "payments-api is down: HTTP 503.",
  "account": "Acme",
  "fields": [
    { "label": "Monitor", "value": "https://api.example.com/health" },
    { "label": "Root cause", "value": "HTTP 503" },
    { "label": "Started", "value": "25 Oct 2026, 09:13:21 UTC" }
  ],
  "links": {
    "acknowledge": "https://upcertain.com/incidents/<id>?ack=1",
    "view_incident": "https://upcertain.com/incidents/<id>"
  },
  "monitor": { "id": "...", "name": "payments-api", "type": "http", "target": "https://api.example.com/health" },
  "incident": {
    "id": "...",
    "started_at": "2026-10-25T09:13:21.000Z",
    "resolved_at": null,
    "cause": "status_code",
    "cause_detail": "HTTP 503",
    "status_code": 503
  }
}

ssl.expiring adds days_left, threshold_days and expires_at. monitors.down_grouped adds a monitors list. Treat unknown fields as optional: new ones may be added.

Verify the signature

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

// rawBody: the request body exactly as received, before JSON parsing.
export function verify(rawBody: string, headers: Headers, secret: string): boolean {
  const ts = headers.get("x-upcertain-timestamp") ?? "";
  const sig = headers.get("x-upcertain-signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // reject replays
  const expected = "v1=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
import hashlib, hmac, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers.get("X-Upcertain-Timestamp", "")
    sig = headers.get("X-Upcertain-Signature", "")
    if abs(time.time() - int(ts or 0)) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, "v1=" + mac)

Delivery and retries

Answer with any 2xx status within a few seconds. Anything else, or no answer, is retried at 30 seconds, 1 minute, 2 minutes and 4 minutes; after the fifth attempt the delivery is marked failed. Deliveries are at least once, so use X-Upcertain-Delivery to drop duplicates.

Questions

Can I receive webhooks on a private network address?
No. To protect both sides, Upcertain refuses webhook URLs that resolve to private, loopback or link-local addresses.
Is there an API to read monitors and incidents as well?
Yes. The REST API and the MCP server are described in the MCP and API docs.