Webhooks
Instead of polling a run until it finishes, register a webhook endpoint and LoopLlama will POST a signed JSON event to you the moment a run changes state.
Overview#
A webhook endpoint is a URL you own plus a list of event types you care about. You can scope an endpoint to a single workflow or receive events for all of them. Manage endpoints from Dashboard → Webhooks or the Webhooks API.
When you create an endpoint you receive a signing secret (whsec_…) exactly once. Every delivery is signed with it so you can reject forged requests.
Event types#
run.startedeventoptionalrun.completedeventoptionaldata.run.output holds the final agent's output.run.failedeventoptionaldata.run.error explains why.run.waiting_inputeventoptionaldata.run.pending describes the question or the action awaiting approval; answer it via Respond to a paused run.webhook.testeventoptionalPayload#
Every delivery is a JSON body with a stable envelope. For run.* events, data.run has the same fields as the run object (without steps) plus workflow_name.
{
"id": "evt_cm1x9q2…",
"type": "run.completed",
"created_at": "2026-09-02T14:03:11.412Z",
"data": {
"run": {
"id": "cm1x9pz…",
"workflow_id": "cm1x8aa…",
"workflow_name": "Brief writer",
"status": "completed",
"input": "Draft a 200-word brief on a new feature-flag system.",
"output": "# Feature flags: a brief\n…",
"error": null,
"trigger": "api",
"total_steps": 2,
"tokens_in": 1840,
"tokens_out": 612,
"started_at": "2026-09-02T14:02:58.001Z",
"finished_at": "2026-09-02T14:03:11.380Z",
"created_at": "2026-09-02T14:02:57.900Z",
"pending": null
}
}
}Headers
LoopLlama-Eventheaderoptionalrun.completed.LoopLlama-DeliveryheaderoptionalLoopLlama-Signatureheaderoptionalt=<unix seconds>,v1=<hex HMAC-SHA256>. See verification below.User-AgentheaderoptionalLoopLlama-Webhooks/1.0Verifying signatures#
The signature is an HMAC-SHA256 over the string `${t}.${rawBody}` using your endpoint secret, hex-encoded. Verify it against the raw request body — re-serializing parsed JSON changes whitespace and breaks the check. Reject timestamps more than five minutes old to block replays.
import { constructWebhookEvent } from "@loopllama/sdk";
const event = constructWebhookEvent(
rawBody, // string, exactly as received
req.headers["loopllama-signature"], // "t=…,v1=…"
process.env.LOOPLLAMA_WEBHOOK_SECRET!,
); // throws LoopLlamaError(400) if invalidimport { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (!parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
}import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Responding#
Return any 2xx status within 10 seconds. Do the real work after responding — queue it, or respond first and process asynchronously. Redirects are not followed.
Retries & auto-disable#
- A non-2xx response, a timeout, or a network error is retried with the same
LoopLlama-Deliveryid after 30 s, 2 min, and 10 min (4 attempts total). - After 10 consecutive events fail all of their attempts, the endpoint is disabled automatically. Re-enable it from the dashboard once the receiver is fixed; the failure counter resets.
- Every attempt is recorded — the dashboard shows status code, latency, and the response body (first 1 KB) so you can debug your receiver, and lets you redeliver any event by hand.
LoopLlama-Delivery (or the event id) as an idempotency key. Retries and manual redeliveries carry the same id, so a receiver that stores processed ids will never double-handle an event.Local development#
In production, endpoint URLs must be https:// and publicly reachable. To receive events on your laptop, expose a local port with a tunnel (ngrok, Cloudflare Tunnel, or similar) and register the tunnel URL. A self-hosted LoopLlama running in development mode also accepts http://localhost URLs.