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

# Per-call variables and the call initiation webhook

> Give every call its own values: from the API, from Node, from the test panel, or from your server when a call comes in.

The `{{variables}}` of your agent have a **default value** you set in the Builder. On a call you can replace them with that call's values: the customer's name, their CRM contact ID, the branch they called. The agent hears them in its prompt, says them in its first message and passes them to its tools.

## Where the values come from

| Call | How to send the values |
| - | - |
| Outbound, through the API | The `metadata` object of [`POST /v1/calls`](https://docs.ryvo.so/api-reference/calls/trigger-an-outbound-call). The CLI sends it with `--var name=value` and the MCP server with `create_call`. |
| Outbound, from Node | Ask Node to place the call with the values; they travel the same way. |
| Tests in the Builder | The `{}` button of the test panel. Its values apply to **Test LLM**, **Test audio** and **Start call** (in Channels). They are never saved to the agent. |
| Inbound phone calls | The **call initiation webhook**: before answering, Ryvo asks your server who is calling (see below). |

When the same name has several values, this order wins: system variables (`system__*`, filled by Ryvo), then the call's value, then the agent's default. A variable with no value at all stays with its braces, so you can spot it in the transcript.

## What they reach

* The prompt, including every stage of a flow.
* The first message and the voicemail message, which the agent says out loud.
* The description of **End call**.
* Tool parameters whose value comes from a **dynamic variable**. The name does not have to appear in the prompt: a tool can use `tenant_slug` even if the prompt never mentions it.

On calls you also get `{{system__call_id}}`: the call's ID in Ryvo, the same one your webhooks receive, handy to log the call in your CRM from a tool.

## Limits and how values are cleaned

* Names: letters, digits and underscore, up to 64 characters, never starting with `system__`. In `POST /v1/calls`, keys with other names (for example `deal-id`) stay as opaque metadata: they travel in the `conversation.started` webhook but never reach the agent.
* Up to **50 variables** per call (more is a `400 invalid_body` in the API), up to 2000 characters per value and 8 KB in total.
* Numbers and booleans become text (`"42"`, `"true"`). `null` keeps the default.
* Control characters are removed, and `{{` or `}}` inside a value become a single brace, so a value can never create a new variable.

> [!WARNING]
>
> These values go into the prompt as written. Treat them as data from your end customer: if a value comes from a public form, someone can try to give the agent instructions through it.

## The call initiation webhook

For **inbound phone calls**. Before answering, Ryvo sends your server who is calling and uses the variables you return. Meanwhile, the caller hears the phone ringing.

**1. Create an endpoint**

In **Settings › Webhooks** (or from the Builder), create an endpoint with your server's HTTPS URL. Its signing secret is the one that signs these requests too.

**2. Turn it on in the agent**

In the Builder, open **Call settings › Call initiation webhook**, turn on **Ask my server when a call comes in** and pick the endpoint.

**3. Adjust the wait**

**Maximum wait time** goes from 0.5 to 4 seconds (2 by default). Turn on **Let it change the first message** if your response may bring the agent's first message.

**4. Publish**

Inbound calls use the published version.

## The request

A `POST` with JSON, signed with `X-Ryvo-Signature` exactly like the events (see [Verify signatures](https://docs.ryvo.so/webhooks/signature-verification.md)), plus `X-Ryvo-Event: conversation.initiation` and `X-Ryvo-Event-Id`. The endpoint receives it whatever events it is subscribed to.

```json theme={null}
{
  "type": "conversation_initiation",
  "agent_id": "3c1f2a4e-5b6d-4c7e-8f90-a1b2c3d4e5f6",
  "agent_version": 7,
  "call_sid": "CA0123456789abcdef0123456789abcdef",
  "caller_id": "+525512345678",
  "called_number": "+528121885950",
  "phone_number_id": "9d2e4f60-1a3b-4c5d-8e7f-0a1b2c3d4e5f",
  "channel": "phone",
  "direction": "inbound",
  "timestamp": "2026-10-26T18:00:00Z"
}
```

`caller_id` and `called_number` keep the names ElevenLabs uses. There is no session ID yet; the stable key is `call_sid`, and the same `call_sid` can arrive more than once if Twilio retries.

## The response

Answer with a `2xx` and this JSON (the same shape ElevenLabs accepts):

```json theme={null}
{
  "type": "conversation_initiation_client_data",
  "dynamic_variables": { "lead_name": "Ana", "is_inbound": "true" },
  "conversation_config_override": {
    "agent": { "first_message": "Hi Ana, this is Karen from Kolonus." }
  }
}
```

* `dynamic_variables` follows the limits above.
* `first_message` is used only if **Let it change the first message** is on, the agent has no flow and it already speaks first. Up to 1000 characters, and it can use variables too.
* Any other override (prompt, language, voice) is ignored.
* A body over 8 KB, invalid JSON or a different `type` counts as a failure.

## When it fails

If your server does not answer in time, answers something other than `2xx`, or the response is invalid, **the call is answered anyway** with the default values. The result (ok, timeout, error or invalid, with the HTTP status and the latency, never the body) is stored with the call. You can turn the webhook off at any time from the same section.


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