Webhooks reference
Outbound webhook payloads, delivery headers, signature verification, and retry behavior.
HowlOps can deliver real-time event notifications to an HTTP endpoint you control. Configure a webhook notification channel under Settings > Notification channels > Webhook.
Delivery
- Method:
POST - Content-Type:
application/json - Retries: failed deliveries are retried through a durable queue with exponential backoff (base 1 min, doubling each attempt, capped at 2 h), up to 3 attempts by default. After the final attempt fails, the delivery is marked
failed.
Delivery headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-HowlOps-Signature | HMAC-SHA256 of the raw request body, hex-encoded (see below). Present when the channel has a secret. |
The event and timestamp fields are carried in the JSON body, not in headers.
Payload shape
Every webhook delivery uses the same flat envelope:
{
"version": "1",
"event": "monitor.down",
"monitor_name": "Production API",
"url": "https://api.example.com/health",
"details": "Connection timed out after 10s",
"timestamp": "2026-05-13T10:00:00Z",
"tenant_id": "uuid",
"monitor_id": "uuid",
"heartbeat_id": "",
"incident_id": "uuid",
"severity": "critical",
"status_code": 503,
"response_ms": 10000,
"region": "eu-west-1",
"monitor_type": "http",
"tags": ["production", "api"],
"responder": "[email protected]",
"runbook_url": "https://runbooks.example.com/production-api"
}
affected_count is also included when a mass-outage event affects more than one check.
| Field | Description |
|---|---|
version | Payload schema version. Currently "1". |
event | One of monitor.down, monitor.up, monitor.ssl_expiry, test. |
monitor_name | Display name of the monitor or heartbeat. |
url | Checked URL for HTTP monitors, ping://<target> for ping checks, the ping target for heartbeats. |
details | Human-readable diagnostic string. |
timestamp | RFC 3339 UTC timestamp of the event. |
tenant_id | Workspace (tenant) UUID. |
monitor_id | Monitor UUID. Empty string for heartbeat-sourced events. |
heartbeat_id | Heartbeat UUID. Empty string for monitor-sourced events. |
incident_id | UUID of the associated incident, if one was opened. |
severity | Severity of the event (critical, warning, info). |
status_code | HTTP status code returned by the check. 0 when unknown. |
response_ms | Response time in milliseconds. -1 when unknown. |
region | Prober region that performed the check (e.g. eu-west-1). |
monitor_type | Monitor type (http, ping, heartbeat, etc.). |
tags | Array of tag strings attached to the monitor. |
responder | On-call responder identifier, if assigned. |
runbook_url | Runbook URL configured on the monitor. |
affected_count | Number of affected checks for mass-outage events (omitted when <= 1). |
For heartbeat events, heartbeat_id is set, monitor_id is empty, and url is ping://<target>.
Signature verification
We sign every webhook so you can verify it's genuinely from HowlOps and wasn't modified in transit. The signature travels in the X-HowlOps-Signature header. The snippet below recomputes it from the request body and compares it safely, so you don't have to write that logic yourself.
Compute the HMAC-SHA256 of the raw request body using your webhook secret as the key, hex-encoded. Compare it to the X-HowlOps-Signature header value (raw hex, no sha256= prefix).
import hmac
import hashlib
def verify(secret: str, body: bytes, header: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
import crypto from "node:crypto";
function verify(secret: string, body: Buffer, header: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
Always use a constant-time comparison to prevent timing attacks.
Event catalog
monitor.down
Fired when a monitor (or heartbeat) opens an incident (the check is failing).
{
"version": "1",
"event": "monitor.down",
"monitor_name": "Production API",
"url": "https://api.example.com/health",
"details": "HTTP 503 Service Unavailable",
"timestamp": "2026-05-13T10:00:00Z",
"tenant_id": "uuid",
"monitor_id": "uuid",
"heartbeat_id": "",
"incident_id": "uuid",
"severity": "critical",
"status_code": 503,
"response_ms": 10000,
"region": "eu-west-1",
"monitor_type": "http",
"tags": ["production"],
"responder": "",
"runbook_url": ""
}
monitor.up
Fired when a monitor recovers (the check is passing again).
{
"version": "1",
"event": "monitor.up",
"monitor_name": "Production API",
"url": "https://api.example.com/health",
"details": "Recovered after 20m10s",
"timestamp": "2026-05-13T10:20:00Z",
"tenant_id": "uuid",
"monitor_id": "uuid",
"heartbeat_id": "",
"incident_id": "uuid",
"severity": "info",
"status_code": 200,
"response_ms": 142,
"region": "eu-west-1",
"monitor_type": "http",
"tags": ["production"],
"responder": "",
"runbook_url": ""
}
monitor.ssl_expiry
Fired when a monitored endpoint's TLS certificate is approaching expiry.
{
"version": "1",
"event": "monitor.ssl_expiry",
"monitor_name": "Production API",
"url": "https://api.example.com/health",
"details": "Certificate expires in 7 days",
"timestamp": "2026-05-13T10:00:00Z",
"tenant_id": "uuid",
"monitor_id": "uuid",
"heartbeat_id": "",
"incident_id": "",
"severity": "warning",
"status_code": 0,
"response_ms": -1,
"region": "eu-west-1",
"monitor_type": "http",
"tags": [],
"responder": "",
"runbook_url": ""
}
Testing webhooks
Send a test delivery from Settings > Notification channels > select channel > Test. The test event uses event: "test" with the same flat envelope.
For local development of your own webhook receiver, forward deliveries to localhost with a tool such as Smee.io (no HowlOps affiliation).
See also
Was this page helpful?