Server URL & Webhooks

Receive your agent's tool calls and phone/SMS lifecycle events on your own server.

Overview — two kinds of webhook

There are two ways the platform calls out to a URL you control. Both POST JSON, both can carry auth headers you set, and both are guarded so they can only ever reach a public address (never an internal/metadata host).

KindFires whenConfigured on
Tool webhookThe model decides to call one of your webhook tools during a call/chatA tool in the Tool Library (per-tool Server URL)
Phone Server URLA call or SMS thread reaches a lifecycle moment (started, ended, recording ready)A phone number's Server URL (per-number)
Note · Tool webhooks are request/response — your reply is fed back to the model. Phone Server URL events are fire-and-forget notifications — we don't read the response, and a slow or failing endpoint never affects the live call.

Tool webhooks — payload

When the model calls one of your webhook tools, we POST to that tool's Server URL. The body always contains the model-chosen `args` plus the call context so you can attribute the request:

POST https://your-api.com/lookup        ← the tool's Server URL
Content-Type: application/json
X-Your-Auth: <any header you configured>

{
  "tool": "lookup_order",          // the tool name
  "args": { "order_number": "SBX-2291" },   // arguments the model chose
  "arguments": { "order_number": "SBX-2291" }, // legacy alias of args
  "callId": "<correlation id of this turn/call>",
  "agentId": "<the agent's id>",
  "businessId": "<your workspace id>",
  "timestamp": "2026-06-22T14:00:00.000Z"
}

— your server replies —

200 OK
{ "status": "ready", "items": ["Grande Latte"], "pickup": "Front counter" }

Reply with HTTP 200 and a small JSON object — the agent reads its fields and speaks/writes them back to the customer. A non-200 response (or a timeout) is surfaced to the model as a tool error so it can recover gracefully.

Important · Webhook tools time out after 10 seconds. Keep your endpoint fast, or return a short 'working on it' result and follow up another way.
Tip · Both `args` and `arguments` carry the same object — `args` is the current name; `arguments` is kept as a legacy alias so older endpoints keep working.

Phone Server URL — events & payloads

Set a Server URL on a phone number and we POST a JSON notification at each lifecycle moment. Every body starts with `type`, `timestamp`, `callId`, `agentId`, and `phoneNumber`, then adds event-specific fields:

EventFires whenChannel
call.startedAn inbound voice call connects to the agentvoice
recording.completedA call's recording is captured and storedvoice
message.startedA new inbound SMS thread beginssms
message.completedAn inbound SMS turn has been answeredsms
POST https://your-server.com/events     ← the number's Server URL
Content-Type: application/json
X-Your-Auth: <any header you configured>

// call.started (voice)
{
  "type": "call.started",
  "timestamp": "2026-06-22T14:00:00.000Z",
  "callId": "CA…",            // Twilio Call SID for voice
  "agentId": "<agent id>",
  "phoneNumber": "+18555294709",
  "phoneNumberId": "<number id>",
  "callSid": "CA…",
  "from": "+1…", "to": "+1…",
  "channel": "voice"
}

// recording.completed (voice)
{
  "type": "recording.completed", "timestamp": "…",
  "callId": "<our call id>", "agentId": "…", "phoneNumber": "+1…",
  "callSid": "CA…", "recordingSid": "RE…", "recordingDuration": 42,
  "channel": "voice"
}

// message.started / message.completed (sms)
{
  "type": "message.completed", "timestamp": "…",
  "callId": "<our thread id>", "agentId": "…", "phoneNumber": "+1…",
  "from": "+1…", "channel": "sms"
}
Note · These are best-effort notifications: we don't read your response, and any error or timeout is logged on our side without affecting the call. Reply 200 quickly and do real work asynchronously.
Tip · The live voice pipeline runs on a separate real-time server; call.started fires from the inbound webhook and recording.completed from the recording callback, so events flow as soon as calls do.

Auth headers — verifying requests are from us

Both webhook kinds let you attach static headers that we send on every request, so your endpoint can confirm the call really came from your CustomWeb workspace. Add one or more key/value pairs (for example a shared secret) and check it server-side:

// what we send (the header you configured):
X-Webhook-Secret: a-long-random-string-you-generated

// what your endpoint does:
if (req.headers["x-webhook-secret"] !== process.env.MY_SECRET) {
  return res.status(401).end();   // reject anything without your secret
}
  • Use a long, random value — treat it like a password.
  • Prefer a custom header (e.g. X-Webhook-Secret) or an Authorization: Bearer … header.
  • Always serve your endpoint over HTTPS so the header isn't sent in the clear.
Tip · Auth headers are stored encrypted and never shown to the browser after saving. Rotate them by editing the tool / number and saving a new value.

Static-IP allowlisting

If your endpoint sits behind a firewall that only allows known sources, enable the static-IP option on the phone number's Server URL. Our outbound requests then originate from a fixed, allowlistable IP range so you can lock your firewall down to just us — instead of (or in addition to) checking an auth header.

Tip · Use static-IP allowlisting and an auth header together for defense in depth: the IP range proves the network source, the header proves it's your workspace.
Note · Both webhook kinds are additionally protected by an SSRF guard on our side: a Server URL can only point at a public host. URLs resolving to localhost, private ranges (10/8, 192.168/16, 172.16/12), link-local, or cloud-metadata addresses are rejected before any request is sent.

How to integrate — tool webhook

  1. Build an endpoint that accepts a POST with a JSON body and returns a small JSON object (see the payload above). A no-code automation (Zapier / Make / n8n) works too — use its Webhook trigger URL.
  2. Dashboard → Tools → New Tool. Give it a snake_case name and a clear description (the model reads the description to decide when to call it).
  3. Paste your endpoint into the Server URL field.
  4. Define the Parameters (JSON Schema) so the model knows what arguments to send — see the Tools page for the schema format.
  5. Add an auth header if your endpoint verifies a secret.
  6. Attach the tool to an agent (Tools tab) and click Test to fire a real request and inspect the live response, latency, and any error before going live.

How to integrate — phone Server URL

  1. Build an endpoint that accepts a POST and replies 200 quickly (do heavy work asynchronously).
  2. Dashboard → Phone Numbers → open the number → find the Server URL field.
  3. Paste your endpoint URL.
  4. Set the timeout (default 20s, allowed 1–300s), add any auth headers, and toggle static IP if your firewall needs it.
  5. Save. Events then POST to your URL the next time a call connects, a recording completes, or an SMS thread starts/finishes on that number.

Where to find the info you'll need

You needWhere it is
Tool Server URL fieldDashboard → Tools → New/Edit Tool → Server URL
Tool auth headersSame Tool editor (add key/value header rows)
Phone Server URL fieldDashboard → Phone Numbers → open a number → Server URL
Server URL timeout / auth headers / static IPSame Server URL panel on the phone number
Agent ID (agentId in payloads)Open the agent → Overview tab → click the ID to copy
Phone number (phoneNumber in payloads)Dashboard → Phone Numbers (the number itself, E.164 format)
Workspace / business ID (businessId)Sent in every tool-webhook payload; also visible in Settings
Tip · callId is the correlation id for the turn/call: for voice it's the Twilio Call SID, for SMS it's our thread id. Use it to tie a webhook to the matching entry in Logs.