A webhook channel sends alerts to your own HTTPS endpoint as JSON. It follows the same rules as your other channels: quiet hours, per-rule channels and escalation delays. Use it to open tickets, page through another tool, or trigger automatic fixes.
Set up
Open Channels → Add channel → Webhook, then enter:
- URL: a public
https://address. - Signing secret (recommended): at least 16 characters. Alertly.Net signs each request with it so you can verify it came from us.
Use Send test to receive a sample test event. It's delivered right away, without retries, and shows the HTTP status if your endpoint rejects it.
Request
Each event is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Alertly-Event |
The event name, for example alert.triggered. |
X-Alertly-Signature |
sha256= followed by the HMAC-SHA256 of the raw body, keyed with your signing secret. Only sent when a secret is set. |
Respond with a 2xx status within 10 seconds (3 seconds to connect). Redirects are not followed, so give the final URL. Errors (4xx, 5xx), timeouts and connection failures are retried twice, after 10 seconds and after one minute.
Events
| Event | When |
|---|---|
alert.triggered |
A new alert fired. |
alert.escalated |
Nobody acknowledged it in time and an escalation channel is notified. |
alert.resolved |
The problem cleared on its own. Alerts resolved by hand don't send this event. |
alerts.grouped |
Several alerts fired at once and were summarised. The alerts array lists them. |
report.daily / report.weekly |
The team's summary report, if turned on. |
test |
You pressed Send test. |
Payload
{
"event": "alert.triggered",
"title": "[Critical] Disk almost full on web-01",
"message": "Disk usage is 96% (threshold 95%).",
"ai_hint": null,
"url": "https://alertly.net/servers/7",
"acknowledge_url": "https://alertly.net/alerts/42/ack?expires=…&signature=…",
"severity": "critical",
"alert": {
"id": 42,
"type": "threshold",
"status": "triggered",
"severity": "critical",
"title": "Disk almost full on web-01",
"message": "Disk usage is 96% (threshold 95%).",
"value": 96.2,
"subject": { "type": "server", "name": "web-01", "url": "https://alertly.net/servers/7" },
"triggered_at": "2026-10-11T09:30:00+00:00",
"resolved_at": null
},
"sent_at": "2026-10-11T09:30:04+00:00"
}
Alert type is one of threshold, offline, monitor_down, ssl_certificate, domain_expiring, heartbeat_missed, heartbeat_failed, service_down, container_down, disk_filling, new_port, firewall_off, account_change, system_error or integration_problem.
The top-level title is [Critical] … for new alerts, [Critical] Escalated: … for escalations and Resolved: … for recoveries. Escalation messages end with how long nobody has acknowledged the alert.
acknowledge_url is a signed link to a page where the alert can be acknowledged without signing in. It works for 3 days, so treat it like a password. It is null on alert.resolved. url points at the affected server, monitor or heartbeat.
ai_hint is a short likely cause written by AI when your team has AI features on. Otherwise, and on alert.resolved, it's null.
Other events
alerts.grouped:alertisnullandalertslists the grouped alerts.urlpoints at the Alerts page. There's noacknowledge_urlorai_hint.report.dailyandreport.weekly:severityandalertarenull, and areportobject holds the period (from,until) and its figures, such asalerts,critical_alerts,open_alerts,mean_minutes_to_resolve,uptime,missed_heartbeatsandincidents.test:severityandalertarenull.
Verify the signature
Compute the HMAC of the raw request body (before parsing the JSON) and compare it in constant time.
PHP
$expected = 'sha256='.hash_hmac('sha256', file_get_contents('php://input'), $secret);
if (! hash_equals($expected, $_SERVER['HTTP_X_ALERTLY_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
Node.js
import crypto from 'node:crypto';
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = Buffer.from(req.headers['x-alertly-signature'] ?? '');
const valid = received.length === Buffer.byteLength(expected)
&& crypto.timingSafeEqual(Buffer.from(expected), received);
Python
import hashlib, hmac
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, request.headers.get("X-Alertly-Signature", ""))