Webhooks let Zespan push alert notifications to your own infrastructure in real time. When an alert rule fires, Zespan immediately sends a signed HTTP POST to your configured endpoint. Use webhooks to integrate with Slack, PagerDuty, OpsGenie, custom dashboards, or any HTTP endpoint.
Webhooks are available on the Scale plan. On Pro and Team, email notifications are available but webhook delivery is not.
Configuring a webhook URL
Webhook URLs are set per alert rule. When creating or editing an alert rule in Dashboard → Alerts, enter your endpoint URL in the Webhook URL field. The URL must:
- Use HTTPS
- Accept POST requests
- Respond with a 2xx status code within 10 seconds
Payload schema
When an alert fires, Zespan sends a POST request with Content-Type: application/json and the following body:
{
"event": "alert.fired",
"alert_id": "alert_abc123",
"rule_name": "Error rate above 5%",
"metric": "error_rate",
"condition": ">",
"threshold": 0.05,
"current_value": 0.12,
"window_min": 15,
"project_id": "proj_xyz789",
"project_name": "acme-production",
"organization_id": "org_def456",
"organization_name": "AcmeCorp",
"fired_at": "2026-04-20T14:32:00Z",
"dashboard_url": "https://app.zespan.com/acme/acme-production/traces",
"signature": "sha256=a4b3c2d1..."
}
Always "alert.fired" for alert webhooks.
The ID of the alert rule that triggered.
The human-readable name of the alert rule, as set in the dashboard.
The metric that crossed the threshold: error_rate, cost_usd, latency_ms, request_count, or token_count.
The comparison that triggered: ">" or "<".
The configured threshold value.
The actual metric value at the time of firing.
The aggregation window in minutes.
ISO 8601 UTC timestamp when the alert fired.
Direct link to the traces page for this project — include this in your Slack message for one-click investigation.
HMAC-SHA256 signature for verifying the payload. See below.
Verifying the signature
Every webhook payload includes a signature field in the format sha256=<hex_digest>. You should verify this before processing the payload to protect against spoofed requests.
The signature is computed as:
HMAC-SHA256(webhook_secret, raw_request_body)
Where webhook_secret is the signing secret shown once when you configure the webhook URL in the dashboard. Store it as an environment variable.
import crypto from "crypto";
import { Request, Response } from "express";
const WEBHOOK_SECRET = process.env.LUMIQTRACE_WEBHOOK_SECRET!;
export function handleWebhook(req: Request, res: Response) {
const rawBody = req.body; // must be the raw buffer, not parsed JSON
const signature = req.headers["x-zespan-signature"] as string;
const expected = "sha256=" + crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(rawBody.toString());
// Process payload...
return res.status(200).json({ received: true });
}
Use the raw request body (Buffer) for signature verification, not the parsed JSON object. Parsing changes whitespace and field ordering, which invalidates the signature.
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["LUMIQTRACE_WEBHOOK_SECRET"]
@app.post("/webhooks/zespan")
def handle_webhook():
raw_body = request.get_data()
signature = request.headers.get("X-Zespan-Signature", "")
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
payload = request.get_json(force=True)
# Process payload...
return {"received": True}
Delivery guarantees
| Property | Value |
|---|
| Timeout | 10 seconds per attempt |
| Retries | 3 attempts with exponential backoff (5s, 25s, 125s) |
| Ordering | Not guaranteed — events may arrive out of order |
| Deduplication | Use alert_id + fired_at to deduplicate |
If all 3 delivery attempts fail, the webhook is marked as failed and no further retries are made. You can see failed deliveries in Dashboard → Alerts → Webhook history.
Integration examples
Slack
Post a formatted message to a Slack channel using an incoming webhook:
export async function POST(req: Request) {
const rawBody = await req.arrayBuffer();
// ... verify signature ...
const alert = JSON.parse(Buffer.from(rawBody).toString());
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `🚨 *Zespan Alert: ${alert.rule_name}*`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*${alert.rule_name}*\n${alert.metric} is ${alert.current_value} (threshold: ${alert.threshold})`,
},
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "View traces" },
url: alert.dashboard_url,
},
],
},
],
}),
});
return Response.json({ ok: true });
}
Create a PagerDuty incident from an alert:
await fetch("https://events.pagerduty.com/v2/enqueue", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Token token=${process.env.PAGERDUTY_API_KEY}`,
},
body: JSON.stringify({
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: "trigger",
payload: {
summary: `Zespan: ${alert.rule_name}`,
source: alert.project_name,
severity: alert.current_value > alert.threshold * 2 ? "critical" : "warning",
custom_details: {
metric: alert.metric,
current_value: alert.current_value,
threshold: alert.threshold,
window_min: alert.window_min,
dashboard: alert.dashboard_url,
},
},
}),
});
Testing your webhook endpoint
Use the Test webhook button in the alert rule editor to send a sample payload to your endpoint. The test payload uses current_value equal to threshold + 1 so your handler can distinguish it from real alerts if needed (the fired_at timestamp will also be clearly recent).
Alternatively, use a tool like webhook.site to inspect raw payloads during development before wiring up your real handler.