Webhooks
Receive signed JSON from SutramX for incidents, recoveries, expiry reminders and maintenance. Payload reference, signature verification, retries and testing.
A webhook sends every SutramX alert to your own HTTPS endpoint as a signed JSON POST. Use it to open tickets, trigger automation, feed a chat bot or log incidents in your own systems. A Zapier connection uses the same incident payload, so you can reach thousands of other apps without writing code.
Webhooks (and Zapier) depend on your plan. See Plans & limits.
Set up a webhook#
- Go to Alerts → Channels & API in the sidebar.
- On the Webhook card, click Connect Webhook.
- Enter your Endpoint URL, for example
https://example.com/webhooks/sutramx. - Click Connect.
- Copy the signing secret from the dialog that opens and store it securely. It is shown only once.
When you connect in the dashboard, SutramX sends a signed test event right away. Only the workspace owner can connect, edit or remove webhooks in the dashboard. Through the API, a key with the Automation access level can also create, edit and remove them, but only the owner can rotate the signing secret (see REST API access levels).
| Field | What it does | Rules |
|---|---|---|
| Endpoint URL | Where SutramX sends events | Must start with https://, resolve to a public internet address and contain no username or password |
| Name | Label for the connection | Named automatically; change it later with Edit, up to 60 characters |
You can add up to 10 webhook connections per workspace, each with its own URL, secret and routing.
The request#
Every delivery is a single HTTP request:
POST /webhooks/sutramx HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: SutramX-Webhooks/1.0
X-SutramX-Event: monitor.down
X-SutramX-Delivery: 3f2b6a8e-9a51-4c1e-8d0f-2a7e5c9b1d44
X-SutramX-Timestamp: 1790932443
X-SutramX-Signature: sha256=8c0e5d2f...
{"event":"monitor.down","title":"Checkout API is DOWN",...}| Header | Contents |
|---|---|
X-SutramX-Event | The event name (also in the event field of the body) |
X-SutramX-Delivery | A unique ID for this delivery. Retries reuse the same ID |
X-SutramX-Timestamp | Unix time in seconds when the request was signed |
X-SutramX-Signature | sha256= followed by the hex HMAC-SHA256 signature |
Events#
| Event | Sent when |
|---|---|
monitor.down | A monitor went down (an incident opened), or an escalation step targets the webhook |
monitor.up | The monitor recovered (the incident resolved automatically) |
incident.resolved | Someone resolved the incident manually; the monitor has not been confirmed back up |
ssl.expiring | An SSL certificate is close to expiry or recently expired |
domain.expiring | A domain registration is close to expiry or recently expired |
maintenance.started | A maintenance window started (alerts are paused) |
maintenance.ended | A maintenance window ended |
site_health.issues | A website health crawl found new problems, Lighthouse scores dropped, or the crawl couldn't finish (for sites with issue notifications on) |
third_party.incident | A followed third-party provider reported an incident or degraded component |
third_party.resolved | That provider resolved the incident or the component recovered |
third_party.maintenance | That provider started or completed maintenance (if you follow its maintenance) |
last_mile.unreachable | Checks from one or more Indian ISP networks keep failing while every data-centre region reaches the monitor. No incident is opened |
last_mile.recovered | Those ISP networks reach the monitor again |
test | You clicked Send test (or just connected) |
Incident, expiry and maintenance events follow the connection's routing: expiry and monitor- or group-scoped maintenance events only go to connections whose routing covers an affected monitor. Website health, third-party and last-mile events go to every webhook connection. Escalation steps ignore routing (see below).
Incident payloads#
monitor.down, monitor.up, incident.resolved and test share one schema:
| Field | Type | Description |
|---|---|---|
event | string | monitor.down, monitor.up, incident.resolved or test |
title | string | "<monitor> is DOWN", "<monitor> has RECOVERED" or "<monitor>: incident resolved manually" |
monitor_name | string | The monitor's name |
monitor_url | string | The monitored URL or host |
incident_id | string | The incident's ID; the same for the down and up events of one incident |
region | string or null | Region that reported the failure (down events) |
started_at | string or null | ISO 8601 time the incident started (down, up and resolved events) |
duration_seconds | number or null | Total downtime in seconds (up and resolved events) |
resolution | string or null | recovered (monitor.up), manual (incident.resolved), null otherwise |
reason | string or null | Failure reason (down events) |
incident_url | string or null | Link to the incident in the dashboard |
explanation | string or null | The one-line "why this alert" verdict (down events). null on other events or when no verdict is available. See Why this alert |
monitor.down#
{
"event": "monitor.down",
"title": "Checkout API is DOWN",
"monitor_name": "Checkout API",
"monitor_url": "https://api.example.com/health",
"incident_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"region": "FRA1 (Frankfurt, Germany)",
"started_at": "2026-10-02T09:14:03.120Z",
"duration_seconds": null,
"reason": "<failure reason>",
"resolution": null,
"incident_url": "https://app.sutramx.com/dashboard/incidents/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"explanation": "Down from 2 of 3 regions: HTTP 503 server error (Arizona still up)"
}monitor.up#
{
"event": "monitor.up",
"title": "Checkout API has RECOVERED",
"monitor_name": "Checkout API",
"monitor_url": "https://api.example.com/health",
"incident_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"region": null,
"started_at": "2026-10-02T09:14:03.120Z",
"duration_seconds": 252,
"reason": null,
"resolution": "recovered",
"incident_url": "https://app.sutramx.com/dashboard/incidents/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"explanation": null
}incident.resolved#
Sent instead of monitor.up when someone resolves an incident by hand while the monitor may still be failing. The payload has the same fields as monitor.up, with "event": "incident.resolved", "resolution": "manual" and the title "<monitor>: incident resolved manually". If you close tickets on monitor.up, close them on incident.resolved too.
Escalation steps#
When an escalation policy step targets webhooks, every webhook connection receives another monitor.down for the same incident_id, with reason set to "Escalation step 2: still down" (with the step number) and started_at set to when the incident started.
test#
{
"event": "test",
"title": "SutramX test alert is DOWN",
"monitor_name": "SutramX test alert",
"monitor_url": "https://example.com",
"incident_id": "0b6d5d1e-3f61-4f5e-9a3c-7d1f2e8a9b10",
"region": null,
"started_at": "2026-10-02T09:20:00.000Z",
"duration_seconds": null,
"reason": "This is a test message for your Webhook integration. No action needed.",
"resolution": null,
"incident_url": null,
"explanation": null
}Notice payloads#
Notice events carry their own fields plus event, title and url (a link to the details).
ssl.expiring and domain.expiring#
{
"monitor_id": "2d1c7a52-8f0e-4c3b-9b7a-5e6f1a2b3c4d",
"monitor_name": "Checkout API",
"monitor_url": "https://api.example.com/health",
"kind": "ssl",
"expires_at": "2026-10-09T23:59:59.000Z",
"days_remaining": 7,
"stage": "pre_7d",
"event": "ssl.expiring",
"title": "SSL certificate for Checkout API expires in 7 days",
"url": "https://app.sutramx.com/dashboard/2d1c7a52-8f0e-4c3b-9b7a-5e6f1a2b3c4d"
}| Field | Values |
|---|---|
kind | ssl or domain |
days_remaining | Whole days left; negative once expired |
stage | pre_15d, pre_7d, pre_2d, pre_24h, post_1d, post_2d, post_3d, or pre_<N>d for an early reminder at the monitor's own threshold |
maintenance.started and maintenance.ended#
{
"maintenance_id": "5a3f9c1e-2b4d-4e6f-8a0b-1c2d3e4f5a6b",
"maintenance_title": "Database upgrade",
"description": "Upgrading the primary database.",
"starts_at": "2026-10-04T01:00:00.000Z",
"ends_at": "2026-10-04T02:00:00.000Z",
"impact": "medium",
"scope": "monitor",
"monitor_ids": ["2d1c7a52-8f0e-4c3b-9b7a-5e6f1a2b3c4d"],
"group_ids": [],
"event": "maintenance.started",
"title": "Maintenance started: Database upgrade",
"url": "https://app.sutramx.com/dashboard/maintenance"
}impact is low, medium or high. scope is global, monitor or group. A maintenance.ended event has the title "Maintenance completed: <title>".
site_health.issues#
| Field | Description |
|---|---|
site_id, run_id | The website health site and crawl run |
site_url | The crawled site |
status | The run's status |
broken_links, mixed_content | Counts from this run |
new_errors | Up to 50 new problems, each with kind and target_url |
score_drops | Lighthouse score drops, each with url, category, from and to |
url | Link to the report |
third_party.incident, third_party.resolved, third_party.maintenance#
{
"provider_id": "heroku",
"provider_name": "Heroku",
"kind": "incident_opened",
"severity": "major",
"detail": "We are investigating elevated error rates.",
"components": ["Dynos"],
"status_page_url": "https://status.heroku.com",
"incident_url": "https://status.heroku.com/incidents/12345",
"event": "third_party.incident",
"title": "Heroku reports an incident: Elevated error rates",
"url": "https://status.heroku.com/incidents/12345"
}| Field | Values |
|---|---|
kind | incident_opened, incident_updated, incident_resolved, maintenance_started, maintenance_completed, component_degraded, component_recovered |
severity | operational, maintenance, minor, major or critical |
last_mile.unreachable and last_mile.recovered#
| Field | Description |
|---|---|
monitor_id, monitor_name | The affected monitor |
notice_id | The ISP outage; the same for its unreachable and recovered events |
isps | Names of the affected ISP networks |
affected | One entry per ISP with isp, name, vantage_points, failures, failing_since and error_classes |
url | Link to the monitor |
The title reads, for example, "Unreachable for Jio users: Checkout API" or "Reachable again for Jio users: Checkout API". See Last-mile checks.
Verify the signature#
The signature is an HMAC-SHA256 of the timestamp, a dot, and the exact raw request body, keyed with your signing secret:
X-SutramX-Signature = "sha256=" + hex( HMAC_SHA256(secret, X-SutramX-Timestamp + "." + rawBody) )To verify a delivery:
- Read the raw body bytes before any JSON parsing.
- Recompute the signature with your secret and compare it to
X-SutramX-Signaturein constant time. - Reject requests whose
X-SutramX-Timestampis more than 5 minutes old.
Node.js:
import crypto from 'node:crypto';
export function verifySutramX(headers: Record<string, string>, rawBody: string, secret: string): boolean {
const timestamp = headers['x-sutramx-timestamp'];
const signature = headers['x-sutramx-signature'] || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return fresh
&& expected.length === signature.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Python:
import hashlib, hmac, time
def verify_sutramx(headers, raw_body: bytes, secret: str) -> bool:
timestamp = headers.get("X-SutramX-Timestamp", "")
signature = headers.get("X-SutramX-Signature", "")
message = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
fresh = timestamp.isdigit() and abs(time.time() - int(timestamp)) < 300
return fresh and hmac.compare_digest(expected, signature)Rotate the secret#
Open the webhook connection's menu, choose Rotate signing secret and confirm with Rotate secret. The new secret is shown once. The old secret stops working immediately, so update your receiver right away. Editing the endpoint URL keeps the current secret.
Responding, timeouts and retries#
| Behaviour | Detail |
|---|---|
| Success | Any 2xx response. The response body is ignored. |
| Timeout | 10 seconds per attempt |
| Redirects | Not followed. A 3xx response fails the delivery: use the final URL. |
| Retried | Timeouts, network errors, 408, 409, 425, 429 and 5xx responses |
| Not retried | Other 4xx responses, which SutramX treats as permanent |
| Attempts | Up to 6 in total, waiting about 5, 10, 20, 40 and 80 seconds between them |
Retry-After | Honoured on 429 and 503 responses when it asks for longer, up to 15 minutes |
Respond quickly with 200 or 204 and do any slow work in the background.
Each attempt is signed again with a fresh timestamp. Retries of the same delivery reuse the same X-SutramX-Delivery ID, so a handler can de-duplicate on that header. Incident payloads also include incident_id.
After 3 deliveries in a row fail, the connection is marked Failing on Alerts → Channels & API, with the last error, and the workspace owner gets an email about it (at most once a day per connection). The connection is never disabled: it keeps receiving events, and a successful delivery clears the flag.
Test your endpoint#
Open the connection's menu and choose Send test. SutramX sends a signed test event and shows the result: "Test message delivered", or the status code and a short excerpt of your endpoint's error.
To inspect payloads before writing a handler, point a webhook at a request inspector you control, send a test, then switch the URL to your real endpoint with Edit.
Zapier#
- In Zapier, create a Zap with the trigger Webhooks by Zapier → Catch Hook and copy the hook URL.
- In SutramX, click Connect Zapier on the Zapier card, paste it into Link from Zapier, and click Connect.
- Click Send test, then use the test event in Zapier to map fields.
Zapier receives the same JSON as monitor.down, monitor.up, incident.resolved and test above. It doesn't receive notices (expiry, maintenance, website health, third-party or last-mile events), and its requests aren't signed: they have no X-SutramX-Timestamp or X-SutramX-Signature header. The link must be a Zapier hook URL, such as https://hooks.zapier.com/hooks/catch/….
Troubleshooting#
- "Enter a full URL starting with https://": plain
http://endpoints aren't accepted. - "This address isn't reachable from the internet": the host resolves to a private or local address. Expose a public HTTPS endpoint.
- "We couldn't find that host": check the hostname for typos and that its DNS record exists.
- Signature mismatch: make sure you sign the raw body, use the current secret, and include the timestamp and the dot.
- "Endpoint redirected to …": use the final URL (for example add or remove a trailing slash, or switch to the canonical host).
Related
- How alerting works
- Slack, Teams & chat apps
- Escalation & on-call
- GitHub, OTel & more
- Features: Alerting & Escalation
- Integrations: Webhooks alerts setup and GitHub issues alerts setup
- Use cases: Monitoring for Developers & Indie Hackers
- More from SutramX: Developers & API
Last updated . Something unclear or missing on this page? Tell us at support@sutramx.com.