Skip to content

Docs

Webhooks

Markdown version

Outbound: events sent to you

Add an endpoint in Settings → Webhooks (or POST /v1/webhooks) and choose the events it receives, or * for all. The signing secret (whsec_…) is shown once. Endpoints must use https and a public address.

Events: account.dormant, account.declining, nps.answered, invoice.overdue, record.created, record.updated, record.archived, deal.won, deal.lost, deal.stage_changed, activity.created, meeting.recorded, meeting.upcoming, insight.captured, run.finished, approval.needed, account.state_changed, signal.fired, sequence.replied, booking.no_show, quote.stalled, call.completed.

Each delivery is a POST with a JSON body, signed per Standard Webhooks:

  • webhook-id: the event's id (use it to drop duplicates);
  • webhook-timestamp: seconds since 1970;
  • webhook-signature: v1, and the base64 HMAC-SHA256 of id.timestamp.body, keyed with the secret after whsec_. During the 24 hours after you rotate the secret, both secrets sign, separated by a space.
import { createHmac, timingSafeEqual } from "node:crypto";

// body: the raw request body, as a string, before any JSON parsing.
function verify(secret, headers, body) {
  const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"];
  if (!id || !ts || !sigs) return false;
  const t = Number(ts);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // more than five minutes off
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const want = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest();
  return sigs.split(" ").some((sig) => {
    const got = Buffer.from(sig.replace(/^v1,/, ""), "base64");
    return got.length === want.length && timingSafeEqual(got, want);
  });
}

Answer with any 2xx within 15 seconds. A failed delivery is tried again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours (7 more attempts), then marked failed. Deliveries are kept for 30 days (GET /v1/webhooks/{id}/deliveries) and can be sent again (POST /v1/webhook-deliveries/{id}/resend). Pause an endpoint with enabled: false.

Inbound: start an agent from another system

Give an agent a webhook trigger (POST /v1/agents/{id}/triggers, or in the agent's settings). Its URL and secret are shown once. The other system posts JSON to that URL with Authorization: Bearer <secret>. The trigger maps the body's fields to the agent's inputs; the body is data for the run, never instructions. Send an Idempotency-Key header so a retried call doesn't start a second run. Zapier, Make and n8n work the same way. Recent firings, including duplicates and ones refused because the agent isn't live, are at GET /v1/triggers/{id}/fires; POST /v1/triggers/{id}/rotate-secret replaces the secret.

Approvals, limits and what an agent may do alone: Approvals and policy.

Send feedback

Tell us what's broken, confusing or missing on this page, or an idea. It goes to the team that builds the product.

Please don't include passwords or card numbers.
Only to reply about this. Never added to any mailing list.