Alertly.Net

Developers

Webhooks

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: alert is null and alerts lists the grouped alerts. url points at the Alerts page. There's no acknowledge_url or ai_hint.
  • report.daily and report.weekly: severity and alert are null, and a report object holds the period (from, until) and its figures, such as alerts, critical_alerts, open_alerts, mean_minutes_to_resolve, uptime, missed_heartbeats and incidents.
  • test: severity and alert are null.

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", ""))

Still stuck? Read troubleshooting.