reference
Integrations
An alert has to arrive somewhere. That “somewhere” is split into two ideas, and keeping them apart is what lets one Slack workspace serve fifty monitors without fifty pieces of configuration.
Channels and routes
Section titled “Channels and routes”A channel is a configured destination — “Production Slack” pointing at one specific webhook URL, or “on-call inbox” pointing at one address. You create it once, in workspace settings, and it holds the credential.
A route says which events reach that channel, and for what. Routes are what you attach to monitors; channels are what routes point at.
The practical consequence: rotating a Slack webhook URL is one edit to one channel, not an edit to every monitor that uses it.
Available channels
Section titled “Available channels”| Channel | Notes |
|---|---|
| Any address. Must be verified before it receives alerts — see below. | |
| Slack | An incoming webhook URL for one channel. |
| Webhook | A signed JSON POST to your own HTTPS endpoint — see below. |
| PagerDuty | An Events API v2 integration key. Triggers and resolves incidents — see below. |
| Opsgenie | An Alerts API v2 key. Creates and closes alerts — see below. |
Every workspace starts with a route to the owner’s email address, so a monitor created five minutes after signup already has somewhere to alert.
Webhooks
Section titled “Webhooks”A webhook channel takes an HTTPS URL and an optional signing secret. Every alert
routed to it arrives as a POST with a JSON body:
{ "version": 1, "event": "down", "sent_at": "2026-08-18T12:15:00Z", "monitor": { "id": "00000000-0000-7000-8000-000000000001", "name": "nightly-backup", "last_ping_at": "2026-08-18T10:00:00Z" }, "silent_for_seconds": 8100, "text": "nightly-backup hasn't checked in — silent for 2h 15m."}event is one of the four routing events, plus test for a test delivery.
Durations are seconds, and only the ones the event is about are present:
down and escalation carry silent_for_seconds, recovery carries
down_for_seconds, and slow carries run_duration_seconds alongside
baseline_run_seconds. monitor.last_ping_at is null — not missing — when the
monitor has never checked in.
Correlate on monitor.id, not monitor.name. Names are editable; ids are not.
New fields may be added to this payload without warning. Anything that would
break a working receiver — a renamed or retyped field — comes with a version
bump instead, so decode leniently and branch on version if you need to.
Verifying the signature
Section titled “Verifying the signature”Set a signing secret (16 characters or more) and every delivery carries three headers:
| Header | Value |
|---|---|
X-Deadpost-Event |
The event name, so you can route without parsing the body. |
X-Deadpost-Timestamp |
Unix seconds, when we sent it. |
X-Deadpost-Signature |
v1= followed by a hex HMAC-SHA256. |
The signature covers the timestamp and the body joined by a dot —
"<timestamp>.<raw body>" — keyed with your secret:
import hashlib, hmac, time
def verify(headers, raw_body, secret): ts = headers["X-Deadpost-Timestamp"] if abs(time.time() - int(ts)) > 300: # reject anything stale return False mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256) return hmac.compare_digest("v1=" + mac.hexdigest(), headers["X-Deadpost-Signature"])Two details are easy to get wrong and both make the check worthless:
- Verify the raw bytes, before your framework parses the JSON. Re-encoding a decoded body reorders keys and changes whitespace, and the HMAC won’t match.
- Check the timestamp. It’s inside the signed string so you can. A signature alone stays valid forever, which means anyone who captures one “monitor down” delivery can replay it at 3am — indefinitely.
Without a secret we send no signature header at all, rather than an empty-key HMAC that would look like proof to a receiver that only checks the header is present. That’s fine for a Zapier or n8n catch hook that can’t verify anything; it isn’t fine for an endpoint that does something consequential.
Your endpoint has to be publicly reachable
Section titled “Your endpoint has to be publicly reachable”We refuse to deliver to a private address — loopback, RFC1918, link-local, and
the rest — and the check runs against the resolved IP at connection time, so a
public hostname pointing somewhere private is refused too. http:// URLs are
rejected outright.
This is not configurable, and the reason is that it’s the one place where our
infrastructure would otherwise make requests of your choosing from inside our
network. If you’re developing a receiver locally, use a tunnel
(cloudflared, ngrok) rather than looking for a way to point us at
localhost.
Use Send test while wiring one up: it delivers the same shape with
"event": "test" and a placeholder monitor, so your parser and your signature
check are exercised before a real outage depends on them.
PagerDuty and Opsgenie
Section titled “PagerDuty and Opsgenie”Both take a key and a region. The key is the integration credential — PagerDuty’s
Integration Key from a service’s Events API v2 integration, Opsgenie’s API
Key from an API integration with the Create and Update access right — and the
region is us or eu, matching where your account lives. An EU account’s events
are rejected by the US endpoint, so if alerts are arriving nowhere, check this
first.
These two differ from Slack and webhooks in one way that matters: they open and close something. A chat channel posts four independent messages; an on-call platform is told that four alerts are one incident.
| Your monitor | PagerDuty | Opsgenie |
|---|---|---|
| goes down | triggers a critical incident |
creates a P1 alert |
| escalates | updates that same incident | de-duplicates into that same alert |
| recovers | resolves it | closes it |
| runs slow | triggers a separate warning incident |
creates a separate P3 alert |
The grouping key is your monitor’s id, so renaming a monitor mid-outage does not orphan the incident its own recovery has to close. A slow run is deliberately kept apart: it is a claim about a job that ran, and letting it share the outage key would mean a slow-run warning could resolve a live incident.
An escalation arrives at the same severity as the down it repeats. Your own escalation policy decides whether to re-notify — ours does not second-guess it, and a nag that arrived quieter would read as the situation improving.
Send test triggers a real incident at the lowest severity the platform has
(info / P5), and deliberately does not resolve it. Auto-resolving a moment
later would close it before you could see it arrive, which is the one thing the
button is for. Every test shares one grouping key, so pressing it ten times leaves
ten notes on one incident, not ten incidents — close it when you’re done.
If you close an Opsgenie alert by hand and the monitor recovers afterwards, the close we send matches nothing and is recorded as a success, not a failure. The alert is already gone, which is the outcome that was wanted.
Events
Section titled “Events”A route subscribes to one or more of four events:
| Event | Fires when |
|---|---|
down |
A monitor passed its grace deadline. |
recovery |
A monitor that was down checked in again. |
slow |
A run finished, but took long enough to be worth flagging. |
escalation |
A monitor has stayed down and the escalation schedule fired again. |
Subscribing to down without recovery is a common mistake. You get told the
job broke and never told it came back, so the only way to find out is to go and
look — which is the habit the product exists to remove.
Environments
Section titled “Environments”An environment is a label a monitor belongs to — production, staging,
internal. Monitors carry one, and routes can target it.
The reason it exists is that alerting rules are almost never per-job; they’re per-tier. “Production wakes the on-call, staging goes to a Slack channel nobody gets paged by” is one rule about two environments, not a rule repeated across sixty monitors. Move a job from staging to production and its alerting follows, because the routing was never attached to the job in the first place.
This is also the cleanest way to give one noisy job different treatment: put it in its own environment rather than hanging a special-case route off the monitor.
Routing is additive
Section titled “Routing is additive”Routes can be attached at three scopes:
- Per monitor — this job alerts here.
- Per environment — everything in staging alerts here.
- Workspace-wide — everything alerts here.
These combine; they do not override. A monitor’s alerts go to the union of its own routes, its environment’s routes, and the workspace-wide routes.
This is worth internalising because the intuition usually runs the other way. Adding a per-monitor route to a noisy job does not stop the workspace-wide route from also firing — you’ll get both. If you want one job to go somewhere else instead, the workspace-wide route is the one to narrow, or the monitor is the one to move to its own environment.
The upside of the union model is that it fails safe. A misconfigured per-monitor route can’t silently detach a job from the alerting everything else gets.
Email verification
Section titled “Email verification”An email channel doesn’t receive anything until the address is confirmed with a code sent to it. Until then the channel exists, shows as unverified, and is skipped when alerts are dispatched.
This is deliberate, and it’s about the failure mode of a typo. An unverified address that silently accepted alerts would look configured, sit in the list next to the working ones, and send every page into nowhere — and you’d find out during the incident it was supposed to catch.
Testing a channel
Section titled “Testing a channel”Every channel can send a test message. Use it after creating one, and again after rotating a credential.
Test results are per channel, not a single pass/fail: if you test several and one fails, you’re told which one. A green “sent” that quietly covered a failed delivery would defeat the purpose of testing at all.
Disabling
Section titled “Disabling”A channel can be disabled without being deleted. Its routes stay attached and stop delivering, which is the right tool when a Slack workspace is being migrated and you want the configuration to survive.
Deleting a channel removes its routes with it.