> ## Documentation Index
> Fetch the complete documentation index at: https://docs.verbeo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Personalize inbound calls with a webhook

> Configure an inbound call webhook on a phone number so your backend can supply dynamic variables and choose which agent answers, before the call connects.

Outbound calls are easy to personalize: you pass `dynamic_variables` when you [create the call](/api-reference/calls/create), and the agent's prompt resolves `{{first_name}}` to the right name.

Inbound calls are harder — the caller arrives unannounced. Without a webhook, an agent whose prompt says `Hi {{first_name}}` has nothing to fill that placeholder with, and the caller hears the placeholder read out literally.

The **inbound call webhook** closes that gap. Set a URL on a phone number, and Verbeo calls it once per inbound call — before the agent picks up. Your backend looks the caller up in your own CRM and replies with two things:

| Field | What it does |
| - | - |
| `dynamic_variables` | Values for the `{{placeholders}}` in the agent's prompt |
| `override_agent_id` | Routes this one call to a different agent than the number's default |

That second field is what lets a single number serve more than one agent — route existing customers to a support agent and unknown numbers to a sales agent, decided per call.

## Set the webhook URL

<Steps>
  <Step title="Open the Phone Numbers page">
    In the Verbeo dashboard, go to **Phone Numbers** and click the pencil icon on the number you want to configure.
  </Step>

  <Step title="Fill in the webhook URL">
    Under **Inbound Call Webhook**, set **Webhook URL (POST)** to your HTTPS endpoint.
  </Step>

  <Step title="Save">
    Click **Update**. The webhook takes effect on the next inbound call — there is nothing to redeploy.
  </Step>
</Steps>

<Note>
  The field appears on numbers connected through SIP trunking. If you don't see it, your number is on the legacy Twilio path — see [Import a phone number from Twilio](/guides/import-twilio-number) to move it onto a trunk.
</Note>

## What Verbeo sends

Verbeo `POST`s this JSON to your URL:

```json theme={null}
{
  "event": "call_inbound",
  "event_timestamp": 1780012672105,
  "call_inbound": {
    "agent_id": "3f2b9c8e-94f2-4c9d-a8e0-5d9f6c2a1b34",
    "from_number": "+12137771234",
    "to_number": "+12137771235"
  }
}
```

<ParamField body="event" type="string" required>
  Always `"call_inbound"`. Branch on this field — more event types may be added later.
</ParamField>

<ParamField body="event_timestamp" type="integer" required>
  Unix timestamp in **milliseconds** when Verbeo sent the request.
</ParamField>

<ParamField body="call_inbound.agent_id" type="string" required>
  UUID of the agent currently assigned to this number for inbound calls. This is the agent that will answer unless you override it.
</ParamField>

<ParamField body="call_inbound.from_number" type="string" required>
  The caller's number in E.164 format. This is your lookup key.
</ParamField>

<ParamField body="call_inbound.to_number" type="string" required>
  The Verbeo number that was dialed, in E.164 format.
</ParamField>

<Note>
  There is no `call_id` in this payload. The call does not exist yet — Verbeo is still deciding how to answer it. Correlate with the `call_id` that arrives in the [`call_started` webhook](/api-reference/webhooks/events) moments later.
</Note>

## What you return

Reply with `200 OK` and a JSON body. **Every field is optional** — returning `{}` means "answer normally," which is exactly what you should do for a caller you don't recognize.

```json theme={null}
{
  "call_inbound": {
    "dynamic_variables": {
      "first_name": "John",
      "plan": "premium",
      "last_order_id": "ORD-98765"
    },
    "override_agent_id": "7c1d5a20-1e44-4b8a-9f31-2c6b7d8e9a01"
  }
}
```

<ResponseField name="call_inbound.dynamic_variables" type="object">
  Key-value pairs substituted into the agent's system prompt wherever it references `{{key}}`. Values should be strings; numbers and booleans are converted for you. Nested objects and arrays are dropped.

  Up to 100 variables, each value up to 2000 characters. Keys beginning with `__` are reserved by Verbeo and ignored.
</ResponseField>

<ResponseField name="call_inbound.override_agent_id" type="string">
  UUID of a different agent to answer this call. Must belong to the same workspace as the phone number — an agent from another workspace is ignored and the default agent answers instead.
</ResponseField>

<Warning>
  **Respond within 5 seconds.** The caller is hearing ringback for the entire time your endpoint takes to reply, so Verbeo enforces a hard 5s timeout and does not retry. That budget comfortably covers a cold-start serverless function, but aim for well under it — every millisecond is ring the caller sits through before your agent says a word.
</Warning>

## When something goes wrong

The webhook never blocks a call. If it times out, refuses the connection, returns a non-`2xx` status, or returns a body Verbeo cannot parse, the call **connects normally** using the number's assigned inbound agent with no dynamic variables.

That fail-open behaviour means a broken endpoint degrades personalization, never availability — but it also means a silently failing webhook looks like "the variables stopped working." Check your endpoint first when callers start hearing placeholders.

<Note>
  If you are migrating from another platform that retries this webhook, note that Verbeo does not. A single 5s attempt either answers or falls open — the call is never held up waiting on a second try.
</Note>

## Missing variables

If your prompt references `{{first_name}}` and your response does not include it, the placeholder is passed to the model **verbatim** — the caller may hear it read aloud. Verbeo does not blank out unresolved placeholders, matching the behaviour of other platforms.

Two ways to stay safe:

* Always return every variable your prompt uses, falling back to a neutral value (`"first_name": "there"`) when you have no data.
* Or write prompts that read naturally when a value is generic, rather than depending on a name being present.

## Example handler

```javascript Express theme={null}
app.post("/verbeo-inbound", async (req, res) => {
  const { from_number } = req.body.call_inbound;

  // Keep this fast — you have 5 seconds total.
  const customer = await crm.lookupByPhone(from_number);

  if (!customer) {
    return res.json({}); // Unknown caller: answer normally.
  }

  res.json({
    call_inbound: {
      dynamic_variables: {
        first_name: customer.firstName ?? "there",
        plan: customer.plan,
      },
      // Existing customers get the support agent instead of sales.
      override_agent_id: customer.isActive ? SUPPORT_AGENT_ID : undefined,
    },
  });
});
```

<Warning>
  Verbeo does not currently sign this request, so your endpoint cannot cryptographically verify it came from Verbeo. Treat the URL as a secret, and check `to_number` against your own numbers before acting on a request.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.