Webhooks
Webhooks are how Emban delivers notifications to your systems. They are not a separate resource — a webhook is just a URL attached to an alert or a scheduled report. When that rule triggers, Emban POSTs to the URL and records the attempt in the webhook_deliveries table.
/v1/alerts and /v1/reports. The Admin → Webhooks page is a read-only dashboard that aggregates deliveries across all alert and report rules in your org.Two delivery profiles
Emban sends two fundamentally different kinds of webhook, and the retry/timeout profile reflects that:
| Source | Payload | Retries | Per-attempt timeout | Backoff |
|---|---|---|---|---|
alert | JSON body | 3 attempts | 10 s | 0s, 30s, 120s |
report | Raw artifact bytes (CSV/PNG/PDF) | 1 attempt | 20 s | — |
Alerts retry because they are small, time-sensitive signals where eventual delivery matters more than the exact minute. Reports do not retry because the artifact is large, the cadence is periodic (daily/weekly/monthly), and it is usually less work to wait for the next scheduled run than to re-deliver a stale one.
Alert payload
POST https://your-receiver.example.com/emban
Content-Type: application/json
{
"alert_id": 42,
"name": "API error rate too high",
"dashboard_id": "dash_abc123",
"widget_id": "w_api_calls",
"condition": {
"type": "threshold",
"operator": "gt",
"value": 50
},
"state": "firing",
"fired_at": "2026-04-24T14:30:00Z"
}
The body identifies the alert, the widget it evaluates, and the exact condition that fired. A 2xx response stops retries; 4xx, 5xx, connection errors, and timeouts trigger the next attempt. After 3 failed attempts the delivery is given up and logged to webhook_deliveries for inspection.
Report payload
Reports POST the artifact bytes directly as the body, with the correct Content-Type and three identification headers:
POST https://your-receiver.example.com/emban-reports
Content-Type: text/csv | image/png | application/pdf
X-Emban-Report-Id: 17
X-Emban-Report-Name: Weekly usage summary
X-Emban-Dashboard-Id: dash_abc123
<raw artifact bytes>
Because reports don't retry, your receiver should be prepared to either persist the artifact immediately or fail fast. A slow receiver that times out loses that run's artifact — but the next run will still fire on schedule with fresh data.
SSRF protection
Every webhook URL is validated before the first attempt. Emban blocks URLs that could be used to probe internal infrastructure:
- Non-HTTP(S) schemes are rejected (
file://,gopher://, etc.) - Hostnames
localhost,metadata.google.internal, and any*.localare blocked - The hostname is DNS-resolved and any IP in loopback (
127.0.0.0/8), private (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), or link-local (169.254.0.0/16) ranges is blocked
Authenticating incoming webhooks
Emban does not yet sign webhook payloads with an HMAC. That means your receiver cannot cryptographically verify that a given POST came from Emban. Until signing lands, use one of these patterns:
https://hooks.example.com/emban/<random-token>). Your receiver rejects any POST whose path token doesn't match. Simple and effective as long as the URL isn't leaked into logs.
X-Emban-Report-Id / X-Emban-Dashboard-Id headers can be cross-checked against the report ID you created. A POST with an unknown ID is silently dropped. Useful as a lightweight spam filter.
alert_id / report_id against what you've created, drop anything you don't recognize, and log the rest.
Observability
Every delivery attempt — successful, failed, retried — is persisted to the webhook_deliveries table:
| Field | Description |
|---|---|
org_id | Your organization ID. All queries are org-scoped. |
source | alert or report. |
source_id | The alert_id or report_id this delivery was for. |
webhook_url | The URL that was POSTed to. |
attempt | Attempt number (1-3 for alerts, always 1 for reports). |
status_code | HTTP response code. 0 means a connection-level failure (DNS, TCP, TLS, timeout). |
error | Truncated error string if the request never completed. |
response_excerpt | First 512 bytes of the response body. Useful for diagnosing 4xx from a misbehaving receiver. |
duration_ms | End-to-end duration of the attempt. |
created_at | Attempt timestamp. |
Admin dashboard
The Admin → Webhooks page aggregates the last 24 hours of deliveries per alert/report:
- Endpoint — the webhook URL, truncated with full value on hover
- Source + Name — which alert or report it belongs to, clickable through to the rule
- Success 24h — count of 2xx responses
- Fail 24h — count of non-2xx, timeouts, and connection errors
- Last response — status code of the most recent attempt with a color-coded pill (green for 2xx, red otherwise)
This is enough to spot a receiver that's been silently 500-ing for a day without anyone noticing. If you want historical context beyond 24 hours, query webhook_deliveries directly through the Query Builder.
Design patterns
alert_id + fired_at (or report_id + started_at) as an idempotency key and no-op on replays.
response_excerpt — "missing X-Correlation-Id" is far more debuggable than a bare "bad request".
no_data alert on the events emitted by your webhook receiver. If Emban ever stops POSTing entirely — or if your receiver stops receiving — you get paged from your own system.
name. One URL per alert (or per category) makes the admin dashboard readable and makes it easy to pause one channel without touching others.
webhook_url.