Webhooks
Signed event deliveries for run outcomes and credential health.
Runs are asynchronous, and polling per customer doesn't scale to a fleet. Register a webhook endpoint and Levain POSTs org events to your backend as they happen — signed, retried, and replayable.
Register an endpoint
curl -X POST https://api.levainlabs.com/api/v1/org/webhooks \
-H "Authorization: Bearer $LEVAIN_ORG_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.your-product.com/levain/events",
"event_types": ["run.succeeded", "run.failed"]
}'The response includes the signing secret once, on creation only — store it
next to the endpoint config in your secret store. An empty event_types
subscribes to everything, and an org can hold up to 10 active endpoints. URLs
must be HTTPS on a publicly resolvable host.
Events
| Event | When it fires |
|---|---|
run.succeeded | A run in any org workspace finished successfully. |
run.failed | A run errored. data.error carries a short excerpt. |
run.cancelled | A run was cancelled. |
credential.revoked | A run found a pushed credential no longer works — time to re-authorize the customer. |
Run events carry the workspace_id, so one handler routes outcomes back to the
right customer:
{
"id": "5f0c9d8a-…",
"type": "run.succeeded",
"created_at": "2026-08-26T09:14:02+00:00",
"data": {
"run_id": "…",
"session_id": "…",
"workspace_id": "…",
"agent_id": "…",
"recipe": "support-triage",
"status": "succeeded",
"error": null,
"started_at": "2026-08-26T09:12:40+00:00",
"finished_at": "2026-08-26T09:14:01+00:00"
}
}Verify signatures
Every delivery carries an x-levain-signature header of the form
t=<unix>,v1=<hex>, where v1 is the HMAC-SHA256 of "{t}.{body}" under your
endpoint's secret. Verify before trusting a payload, and reject stale timestamps
to bound replay:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = int(parts["t"]), parts["v1"]
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)The x-levain-event-id and x-levain-event-type headers repeat the envelope's
id and type for cheap routing and deduplication — an event delivered to two
endpoints, or redelivered after a timeout, keeps the same id.
Retries and replay
A delivery counts as successful on any 2xx response. Anything else is retried
on a backoff schedule — 1 minute after the first failure, stretching to 12 hours
— until it lands or the delivery is marked dead.
Inspect what happened per endpoint with
GET /api/v1/org/webhooks/{endpoint_id}/deliveries
(the 50 most recent, with response codes and errors), and re-send any delivery —
dead or delivered — with
POST …/deliveries/{delivery_id}/replay
once your receiving end is fixed.
Deleting an endpoint deactivates it and keeps its delivery history; pending
deliveries are marked dead.