Webhooks

Webhooks are how paused agents wake up. Register an endpoint in Dashboard → Webhooks and every state change in your workspace is POSTed to it, signed, within moments of happening.

Events

EventFires when
request.createdA request was persisted and the approver notified.
request.approvedA human approved — possibly with an edited payload.
request.rejectedA human rejected. The note, if any, is included.
request.expiredThe TTL elapsed with no decision.

Delivery payload

POST <your endpoint>
{
  "event": "request.approved",
  "createdAt": "2026-07-15T20:05:00.000Z",
  "data": {
    "id": "clx…",
    "status": "APPROVED",
    "summary": "Refund $10,000 to customer #4821",
    "payload":          { "action": "refund", "amount": 10000 },
    "modifiedPayload":  { "action": "refund", "amount": 10.00 },
    "effectivePayload": { "action": "refund", "amount": 10.00 },
    "metadata": { "runId": "run_442" },
    "agentName": "SupportBot",
    "approverEmail": "finance@company.com",
    "resolvedAt": "2026-07-15T20:05:00.000Z",
    "resolvedByEmail": "finance@company.com",
    "resolutionNote": "Fixed the amount.",
    "expiresAt": "2026-07-16T20:00:00.000Z",
    "createdAtRequest": "2026-07-15T20:00:00.000Z"
  }
}

Deliveries also carry three headers:

HeaderContents
X-Confirm-EventThe event name, duplicated for cheap routing.
X-Confirm-Delivery-IdStable per delivery — use it as an idempotency key.
X-Confirm-Signaturet=<unix_ts>,v1=<hex> — the HMAC described below.

Verifying signatures

Each endpoint has a whsec_… secret, shown once at creation. The signature is HMAC-SHA256 over "<timestamp>.<raw body>". Verify with a timing-safe comparison and reject stale timestamps to block replays:

verify.ts
import { createHmac, timingSafeEqual } from "crypto";

export function verifyConfirmSignature(
  secret: string,
  header: string,      // X-Confirm-Signature
  rawBody: string,     // the unparsed request body
  toleranceSec = 300,
) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Sign-check the raw body string, before any JSON parsing. Frameworks that re-serialize the body will change whitespace and break the HMAC.

Retries & reliability

  • Any 2xx from your endpoint counts as delivered.
  • Non-2xx responses and timeouts (10s) are retried up to 3 attempts with backoff.
  • Every attempt is recorded — endpoint, event, HTTP status, error — and visible on the request's detail page in the dashboard.
  • Design your handler to be idempotent using X-Confirm-Delivery-Id; retries mean you can occasionally see the same delivery twice.

Webhooks are the recommended integration, but they're optional — polling GET /v1/requests/:id gives identical data with more latency.