Webhooks
The exact JSON Drumbeats sends your endpoint, and what happens when your endpoint is down.
This is what lands on your endpoint when a monitor goes down:
{
"alertId": "0f2b8c31-6d4e-4a9f-8c11-2b7e5a1d9f30",
"projectId": "8a1c4f22-9e3b-4d17-b5a8-6c2f0d4e7b19",
"monitorId": "3d9e7b14-5c82-4f61-a0d3-7e1b9c5a2f48",
"incidentId": "c71a3e58-2b64-4d90-9f7c-1a5e8d3b06f2",
"event": "DOWN",
"status": "down",
"timestamp": "2026-08-01T02:07:14.882Z",
"monitor": {
"name": "Nightly backup",
"slug": "nightly-backup",
"description": "pg_dump to S3",
"url": "https://drumbeats.io/monitors/3d9e7b14-5c82-4f61-a0d3-7e1b9c5a2f48"
},
"incident": {
"event": "MISSED",
"cause": "No check-in received within grace period",
"startedAt": "2026-08-01T02:05:00.000Z"
},
"schedule": {
"expected": "0 2 * * *",
"gracePeriodSeconds": 300,
"timezone": "Europe/Amsterdam",
"lastPing": "2026-07-31T02:00:11.204Z"
}
}{
"alertId": "0f2b8c31-6d4e-4a9f-8c11-2b7e5a1d9f30",
"projectId": "8a1c4f22-9e3b-4d17-b5a8-6c2f0d4e7b19",
"monitorId": "3d9e7b14-5c82-4f61-a0d3-7e1b9c5a2f48",
"incidentId": "c71a3e58-2b64-4d90-9f7c-1a5e8d3b06f2",
"event": "DOWN",
"status": "down",
"timestamp": "2026-08-01T02:07:14.882Z",
"monitor": {
"name": "Nightly backup",
"slug": "nightly-backup",
"description": "pg_dump to S3",
"url": "https://drumbeats.io/monitors/3d9e7b14-5c82-4f61-a0d3-7e1b9c5a2f48"
},
"incident": {
"event": "MISSED",
"cause": "No check-in received within grace period",
"startedAt": "2026-08-01T02:05:00.000Z"
},
"schedule": {
"expected": "0 2 * * *",
"gracePeriodSeconds": 300,
"timezone": "Europe/Amsterdam",
"lastPing": "2026-07-31T02:00:11.204Z"
}
}Use a webhook to bridge Drumbeats into Opsgenie, Datadog, an internal incident tool, or anything else that accepts inbound HTTP. For PagerDuty, use the dedicated PagerDuty channel instead. It maps the lifecycle for you.
Set one up#
- Open Settings → Notification Channels and click Add Channel → Webhook.
- Set the DOWN URL and the RECOVERY URL. They can be the same endpoint. Branch on the
eventfield. - Leave the mode on Simple unless the receiving system needs a specific body shape.
- Click Test. Drumbeats sends a sample DOWN and a sample RECOVERY to your endpoints.
- Add the channel to a notification group and assign the group to a monitor.
Read the payload#
Both events share a base. The type-specific block is what differs.
| Field | Type | Notes |
|---|---|---|
alertId | string | The delivery's own ID. Use it to deduplicate retries |
projectId | string | Project the monitor belongs to |
monitorId | string | Monitor UUID |
incidentId | string or null | Set on DOWN. Null on RECOVERY |
event | string | DOWN or RECOVERY |
status | string | down or up |
timestamp | string | ISO 8601, the time the payload was built |
monitor.name | string | Display name |
monitor.slug | string or null | Stable identifier, if you set one |
monitor.description | string or null | |
monitor.url | string | Deep link to the monitor in the dashboard |
On a DOWN event you also get an incident object:
| Field | Type | Notes |
|---|---|---|
incident.event | string or null | MISSED, FAILED, DURATION_HIGH, DURATION_LOW, or SSL_EXPIRING |
incident.cause | string or null | Human-readable reason |
incident.startedAt | string or null | ISO 8601 |
On a RECOVERY event you get a recovery object instead:
{
"event": "RECOVERY",
"status": "up",
"recovery": {
"recoveredAt": "2026-08-01T03:14:02.115Z",
"downSince": "2026-08-01T02:05:00.000Z",
"resolvedIncidentCount": 1,
"failuresBeforeRecovery": 0
}
}{
"event": "RECOVERY",
"status": "up",
"recovery": {
"recoveredAt": "2026-08-01T03:14:02.115Z",
"downSince": "2026-08-01T02:05:00.000Z",
"resolvedIncidentCount": 1,
"failuresBeforeRecovery": 0
}
}A schedule object is attached to either event when the monitor has scheduling context, carrying expected, gracePeriodSeconds, timezone, and lastPing.
Certificate expiry adds an ssl block#
When incident.event is SSL_EXPIRING, the payload carries certificate detail. Match these key names exactly as written:
{
"incident": { "event": "SSL_EXPIRING", "cause": "Certificate expires in 14 days", "startedAt": "2026-08-01T02:07:14.882Z" },
"ssl": {
"expires_at": "2026-08-15T09:31:00.000Z",
"issuer": "Let's Encrypt",
"days_remaining": 14,
"threshold_days": 14
}
}{
"incident": { "event": "SSL_EXPIRING", "cause": "Certificate expires in 14 days", "startedAt": "2026-08-01T02:07:14.882Z" },
"ssl": {
"expires_at": "2026-08-15T09:31:00.000Z",
"issuer": "Let's Encrypt",
"days_remaining": 14,
"threshold_days": 14
}
}Region events add a region block#
Multi-location uptime monitors emit REGION_UNREACHABLE and REGION_RECOVERED events with a region object holding slug, display_name, failure_kind, and other_regions_up.
Build a custom body#
Switch the channel to Advanced mode when the receiving system needs a fixed shape. You then control the URL, the headers, and the body, and Drumbeats substitutes placeholders into all three.
{
"title": "$MONITOR_NAME is $STATUS",
"severity": "critical",
"source": "drumbeats",
"dedup_key": "$MONITOR_ID",
"details": $JSON
}{
"title": "$MONITOR_NAME is $STATUS",
"severity": "critical",
"source": "drumbeats",
"dedup_key": "$MONITOR_ID",
"details": $JSON
}| Placeholder | Expands to |
|---|---|
$ALERT_ID | Delivery ID |
$PROJECT_ID | Project UUID |
$MONITOR_ID | Monitor UUID |
$INCIDENT_ID | Incident UUID, empty on recovery |
$EVENT | DOWN or RECOVERY |
$STATUS | down or up |
$NOW | Current time, ISO 8601 |
$MONITOR_NAME | Monitor display name |
$MONITOR_SLUG | Monitor slug |
$MONITOR_DESCRIPTION | Monitor description |
$INCIDENT_EVENT | MISSED, FAILED, DURATION_HIGH, DURATION_LOW, SSL_EXPIRING |
$INCIDENT_CAUSE | Human-readable reason |
$STARTED_AT | Incident start, ISO 8601 |
$RECOVERED_AT | Recovery time, ISO 8601 |
$DOWN_SINCE | Outage start, ISO 8601 |
$FRONTEND_URL | Dashboard base URL |
$MONITOR_URL | Deep link to the monitor |
$JSON | The entire Simple-mode object, inlined as raw JSON |
Drumbeats checks whether the rendered body parses as JSON. If it does not, the request is still sent as-is and a warning is logged, so a broken template shows up as a confusing 400 from your own endpoint rather than an error in Drumbeats.
Configure the request#
| Setting | Default | Range |
|---|---|---|
| Method | POST | GET, POST, or PUT |
| Timeout | 10 000 ms | 1000 to 60 000 ms |
| Headers | none | One Header-Name: value per line |
| Body | Simple-mode payload | Advanced mode only |
GET requests send no body. Put what you need in the URL with placeholders.
Headers are where authentication goes. Authorization: Bearer <token> and X-API-Key: <key> both work, and placeholders are substituted in header values too.
What happens when your endpoint is down#
A delivery counts as failed when your endpoint returns a non-2xx status, the request times out, or the connection fails. What Drumbeats does next depends on the other channels in the group.
If every channel in the group failed, Drumbeats retries the delivery a few times over the following minute or so, with a widening gap between attempts. That is enough to ride out a brief blip at your end. After that it stops.
If any other channel in the group succeeded, the delivery is not retried. Your webhook failure is recorded and the alert moves on.
The incident itself is never affected by a delivery failure. It stays open in Drumbeats and resolves on the monitor's next success regardless of who got paged.
Per-delivery results are recorded, so you can see whether a webhook was attempted and what your endpoint returned.
Test before you rely on it#
Every webhook channel has a Test button that fires a sample DOWN and a sample RECOVERY at your configured URLs. The test payload uses incidentEvent: "TEST" so it is distinguishable from a real alert.
Send a test when you create the channel, and again every time you change the target system. A webhook that worked six months ago is not evidence it works now.
Next#
Notification channels for the other seven channel types. Groups for routing. Alert logic for what decides a delivery happens at all.