Skip to main content

How-to — Set up webhooks

Subscribe to platform events from your own service. Webhooks fire on the same event bus as the portal's real-time UI.

Add a webhook

From the portal

Settings → Webhooks → Add endpoint.

Fill in:

  • URLhttps://your-app.com/eden/webhook
  • Events — pick from the catalog below
  • Secret — used to sign the payload (HMAC-SHA256)
  • Description — for your own bookkeeping

The portal sends a test event on save.

From the API

curl -X POST https://api.edenobservability.com/orgs/org_abc/webhooks \
-H "Authorization: Bearer sk_eden_admin_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/eden/webhook",
"events": ["trace.completed", "judge.threshold_breach"],
"secret": "whsec_..."
}'

Events

EventFires when
trace.startedFirst event of a new trace
trace.completedTerminal event (outcome set, span closed)
judge.threshold_breachA judge score crosses the configured threshold
judge.regressionA regression alert fires
cost.threshold_breachA cost budget crosses a configured threshold
cost.anomalyA cost anomaly is detected
budget.hard_exceededThe org hit a hard cost limit
incident.createdA new incident cluster is identified
incident.resolvedAn incident is auto-resolved or manually closed
agent.registeredA new agent is registered in the org
agent.deprecatedAn agent is auto-deprecated (drift detection)
pii.matchA PII redaction occurred (sampled, 1 in 1000)
audit.api_key.createA new API key was issued
audit.api_key.revokeAn API key was revoked

The full catalog with payload schemas is in Reference → Webhooks.

Signature verification

Every webhook includes an X-Eden-Signature header:

X-Eden-Signature: t=<unix-timestamp>,v1=<hex-digest>

Verify with the secret:

import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = parts["t"]
digest = parts["v1"]

# Replay protection: reject anything older than 5 minutes.
if abs(time.time() - int(timestamp)) > 300:
return False

signed = f"{timestamp}.".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, digest)

The t= field is independent of the body so the signature is robust to header reordering.

Idempotency

Every webhook carries an X-Eden-Event-Id header. Store this as the dedupe key — Eden retries failed deliveries (5xx, timeout) with exponential backoff for up to 24 hours, so the same event may arrive more than once.

Retry policy

ResponseRetry?
2xxNo
4xx (except 408, 429)No — fix your endpoint
408, 429Yes, exponential backoff
5xxYes, exponential backoff (1s → 60s, max 24h)

The X-Eden-Attempt header tells you which retry this is (0-indexed).

Dead-letter queue

If delivery fails after 24 hours, the event lands in the Webhooks → Dead-letter queue tab. From there you can:

  • Replay the event (re-POST with the original payload)
  • Inspect the request and response logs from each attempt
  • Disable the endpoint to stop further retries

Local development

Use a tunnel:

# ngrok
ngrok http 3000
# Paste the https://... URL into Settings → Webhooks

# Cloudflare tunnel
cloudflared tunnel --url http://localhost:3000

Or use the webhook inspector:

# webhook.site
# 1. Visit https://webhook.site, copy the URL
# 2. Paste into Settings → Webhooks
# 3. Trigger an event in Eden
# 4. See the payload on webhook.site

Code example

A minimal Express handler:

import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/eden/webhook", (req, res) => {
const header = req.headers["x-eden-signature"];
const secret = process.env.EDEN_WEBHOOK_SECRET;

const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2))
);
const signed = `${parts.t}.`.encode() + req.body;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
return res.status(401).send("bad signature");
}

const event = JSON.parse(req.body);
switch (event.type) {
case "trace.completed":
// your handler
break;
case "judge.threshold_breach":
// your handler
break;
}

res.status(200).end();
});