Skip to content

Webhooks

Generic output: on alert Vesana POSTs JSON to a URL of your choice. Lets you connect anything — own ticketing, pager, IFTTT, n8n, Zapier.

Configuration

/notification-channelsNew channel → type Webhook:

Field Value
Name „Pager Webhook"
URL https://pager.example.com/incoming/...
Method POST (default)
Auth None / Basic / Bearer
Headers Custom headers for auth etc.
HMAC secret optional for signature validation
Payload template default or custom Jinja

Default payload

{
  "version": 1,
  "alert_id": "uuid",
  "rule_name": "Disk Full Critical",
  "rule_id": "uuid",
  "status": "CRITICAL",
  "trigger_at": "2026-04-25T10:15:30Z",
  "is_recovery": false,
  "host": {
    "id": "uuid",
    "name": "web01.acme.local",
    "ip_address": "10.10.5.13",
    "tenant": { "id": "uuid", "name": "Acme GmbH" },
    "tags": ["production","web"],
    "url": "https://vesana.example.com/hosts/uuid"
  },
  "service": {
    "id": "uuid",
    "display_name": "Disk /var",
    "check_type": "agent_disk",
    "value": 96.4,
    "message": "CRITICAL - /var at 96%",
    "perfdata": { "used_percent": 96.4 }
  },
  "previous_status": "WARNING",
  "duration_seconds": 320
}

HMAC signing

When webhook_secret is set:

X-Vesana-Signature: sha256=<hex-digest>
X-Vesana-Timestamp: 1714039000

Signature: HMAC-SHA256(secret, timestamp + "." + body). Receiver should sign and compare:

import hmac, hashlib

def valid(body: bytes, ts: str, sig: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        f"{ts}.".encode() + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(sig, expected)

Replay protection: reject timestamps older than 5 min.

Custom payload template

When the receiver expects a specific format (e.g. PagerDuty events):

{
  "routing_key": "{{ env.PAGERDUTY_KEY }}",
  "event_action": "{% if alert.is_recovery %}resolve{% else %}trigger{% endif %}",
  "dedup_key": "vesana:{{ alert.service.id }}",
  "payload": {
    "summary": "{{ alert.host.name }} — {{ alert.service.display_name }}: {{ alert.message }}",
    "severity": "{{ alert.status | lower }}",
    "source": "{{ alert.host.name }}",
    "custom_details": {
      "tenant": "{{ alert.host.tenant.name }}",
      "value": {{ alert.service.value }}
    }
  }
}

So you can connect Vesana to PagerDuty, OpsGenie, VictorOps, etc.

Retry

On receiver error (timeout, 5xx):

  • 3 retries with backoff: 5 s, 30 s, 5 min
  • After third failure: failed entry in notification log

4xx responses are not retried — server takes the receiver's „no" at face value.

Group payloads

With grouping in alert rule, an array arrives:

{
  "version": 1,
  "group_id": "uuid",
  "rule_name": "Disk Full Critical",
  "alerts": [
    { "host": {...}, "service": {...} },
    { "host": {...}, "service": {...} },
    ...
  ],
  "alert_count": 12
}

Receiver should handle both cases (single + group).

Test

/notification-channels → Channel → Test:

  • Sends fake alert payload
  • UI shows HTTP status, response body, latency
  • With HMAC: signature is included, receiver can test validation

Audit

Every webhook notification (success or fail) appears in audit log with action = notification.send.

Next