Levain LabsLevain Labs
Embedded Agents

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

EventWhen it fires
run.succeededA run in any org workspace finished successfully.
run.failedA run errored. data.error carries a short excerpt.
run.cancelledA run was cancelled.
credential.revokedA 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.

On this page