---
title: "Per-call variables and context"
description: "Give each call its own details, as named variables for the prompt or a free-form brief, without changing the agent."
---

Each call can carry two kinds of per-call input. Use either one, or both
together:

- **`variables`**: named values that fill `{{placeholders}}` in the prompt. Use
  them for fields the prompt refers to by name, such as the caller's name, an
  account number or an appointment slot.
- **`context`**: free-form text added after the prompt. Use it for a brief that
  does not fit a template, such as a description of the task for this call or
  notes collected earlier.

Both work with a saved agent and with an inline configuration, on every way of
starting a call described below. Neither changes the saved agent, its version
history or its other settings.

```json
{
  "agent_id": 12,
  "variables": {"name": "Ada", "appointment_slot": "Tuesday at 10 AM"},
  "context": "Ada rescheduled twice last month. She prefers mornings and asked us not to call her work number."
}
```

## Variables

Put placeholders in your saved agent's prompt, then supply a `variables`
object when starting a call. ThunderPhone renders the text before sending the
call configuration to the voice runtime.

When no values are needed, omit `variables`, do not send `null` (rejected with `400`).

### Placeholders and defaults

```text
You are calling {{name|Friend}} about account {{account_id}}.
The available appointment is {{ appointment_slot }}.
```

Names are case-sensitive and follow `[A-Za-z_][A-Za-z0-9_]*`. Whitespace around
the name is allowed; whitespace after `|` is part of the default and is
preserved. `{{name|Friend}}` uses `Friend` when `name` is missing or
`null`; an empty string is an explicitly supplied value. Missing values without
a default become empty strings and their names appear in `unresolved_variables`.
Text between double braces that is not a valid placeholder is removed. Double-braced
text inside each supplied value is removed independently; a value cannot remove
surrounding prompt text or another value. Unmatched double-brace delimiters are
removed too. JSON examples in prompts must not use `{{`.
Values are plain text, never evaluated
as code or recursively expanded as templates.

Variables can also appear in acknowledgement prompts, outbound voicemail
messages, and the consent announcement text when that field is sent for a
phone call. The agent has no separate `first_message` field: put its opening
instructions in the prompt. Existing voicemail `{agent_name}` and `{org_name}`
placeholders continue to work.

### Values and limits

Values may be strings, numbers, booleans, or `null`; booleans render as `true`
and `false`. Unicode control (`Cc`) characters except newline (`\n`), tab (`\t`),
and carriage return (`\r`), format (`Cf`) characters except the zero-width
non-joiner and joiner (U+200C, U+200D, which spell words in Indic and
Arabic-script languages), and surrogate (`Cs`) code points are removed; `\r\n`
is normalized to `\n`. Each value
is limited to 2,000 characters when rendered. Supplied strings are cleaned and
truncated before storage as well. The original object must fit in **32 KB of
UTF-8 JSON**; larger objects receive `400` on call/session requests, while
campaign imports report invalid rows individually. Arrays and nested objects are not
accepted as values. Nonmatching metadata keys (for example a CSV header with a
space) are retained and echoed but cannot be referenced by a placeholder.

## Context

`context` is a string of up to **20,000 characters**. ThunderPhone adds it after
the prompt, once the prompt's variables are filled in, as a delimited block:

```text
<your prompt, with its variables filled in>

Details for this call:
<call_context>
Ada rescheduled twice last month. She prefers mornings and asked us not to call her work number.
</call_context>
```

The agent reads it together with its instructions, so write it the way you
would brief a colleague before the call. Context is not a template:
`{{name}}` inside it stays as written. If the text needs a value, put the value
in yourself before sending.

- `null`, an empty string, or only whitespace means no context.
- A value that is not a string, or is longer than 20,000 characters, is
  rejected with `400` on call and session requests; from a configuration
  webhook it fails the exchange like invalid `variables` (see below).
- Leading and trailing whitespace is trimmed, and the same control and format
  characters as in variable values are removed.
- Context is part of the call's prompt, so it counts toward the call's
  [prompt size](/guides/pricing#prompt-size).

On a [steps](/guides/agent-steps) agent, the context follows the prompt of
every agent in the call, not each step's instructions.

## Where values come from

### Outbound API

Send `variables` and `context` on `POST /v1/call`:

```json
{
  "from_number": "+15551234567",
  "to_number": "+14155552671",
  "agent_id": 12,
  "variables": {
    "name": "Ada",
    "account_id": "A-17",
    "appointment_slot": "Tuesday at 10 AM"
  },
  "context": "Ada asked to be called back about the late fee on her March invoice."
}
```

Both also work with the phone number's default outbound agent, or with an inline
`config.prompt`. With an inline config, `context` can also go inside `config`,
the same field a configuration webhook returns; if you send both, they must be
the same text. An idempotency key cannot be reused with different variables or
a different context.

### Campaign CSV

Non-phone CSV columns are already stored as contact variables. Each dial now
uses them automatically. Use headers such as `name`, `account_id`, and
`appointment_slot` to match your placeholders. The existing name mapping can
combine first and last name columns into the variable `name`. Campaign
contacts have no per-contact context.

### Dynamic configuration webhook

On the blocking configuration webhook path, return a saved agent in your
organization plus any per-call values:

```json
{
  "agent_id": 12,
  "variables": {"name": "Ada", "account_id": "A-17"},
  "context": "Ada called yesterday about a leak in unit 4B; a plumber is booked for Friday."
}
```

The response's variables overwrite request-level variables, while other request
keys remain. A response value of `null` selects the placeholder's default.
The merged object must also fit in 32 KB. Saved-agent responses accept only
`agent_id`, `variables`, `context` and a per-call
[`timezone`](/guides/dynamic-call-config#set-the-time-zone-per-call); return an inline configuration when you need to
replace the prompt or other settings. A response containing `prompt` always uses inline
configuration: any `agent_id` in that response is ignored, including null or
non-integer metadata. The inline prompt still must pass normal validation.
Inline webhook responses can include `variables` and `context` too. A `context`
that is not a string or is too long fails the exchange: a phone call is
rejected, and a widget session request fails with `400`. Saved-agent webhook
responses use the agent's deployed A/B split on both phone and widget calls;
variables render after variant selection. On inbound phone calls, use a number with no assigned inbound
agent and configure its phone-number or organization webhook; widget keys use
`mode="webhook"`. Endpoint-system incoming notifications do not supply blocking
configuration responses.

### Agent call setup webhook

To keep a number assigned to its agent and fill in only the call's values,
give the agent a [call setup webhook](/webhooks/call-incoming#agent-call-setup-webhook).
Before each inbound call the agent answers on a number, the webhook gets the
call and the agent's id, name and version, and answers with the same
`variables` and `context` as above, without needing an `agent_id`. An agent
whose settings use a placeholder with no default needs this webhook to answer
a phone number: the call does not start when the webhook fails. Placeholders
with a default, like `{{name|there}}`, let the agent answer without it.

### SIP headers

Calls that reach an agent over SIP (a [SIP address](/guides/sip-addresses), or
a number on a manual SIP connection) can carry context in `X-*` and
`User-to-User` headers. Each one becomes a `sip_` variable, such as
`{{sip_x_amzn_connectcontactid}}` or `{{sip_uui}}`, with the lowest
precedence: a value of the same name from the API or a webhook wins. See
[Pass call context in SIP headers](/guides/sip-addresses#pass-call-context-in-sip-headers).
Header values are set by the sender, so treat them as untrusted, like any
browser-supplied value.

### Widget session API

`POST /v1/widget/session` accepts a top-level `variables` object. Its publishable
key selects the saved agent. Webhook-mode keys forward these values to the
configuration webhook and merge the response as described above.
Browser-supplied widget `variables` are client-controlled, forwarded
verbatim in `web.incoming` after the validation and string cleanup described
above, and echoed into completion webhooks and call history. Do not treat them
as trusted identity or authorization data.

The widget's own browser `context` field is different: it carries text from the
page the visitor is on, which the agent treats as website content (see
[Embed the web widget](/guides/embed-web-widget)). A widget call's per-call
context comes from a webhook-mode key's configuration webhook, as above. When a
call has both, the per-call context comes first, then the page content.

### Realtime session API

`POST /v1/realtime/sessions` accepts top-level `variables` and `context`
alongside `agent_id` or an inline `config`:

```json
{
  "agent_id": 12,
  "variables": {"name": "Ada"},
  "context": "Ada is calling from the front desk about a guest's lost key card."
}
```

`context` goes at the top level only; an inline `config` rejects it as an
unknown field.

### Realtime WebSocket

With a saved agent, the saved agent's deployed prompt stays its prompt.
`session.instructions` becomes the call's context, and `session.prompt.variables`
(or `session.config.variables`) become its variables. Choose the agent with
`session.prompt.id` in the first `session.update`, which starts the session once
that update arrives:

```json
{
  "type": "session.update",
  "session": {
    "type": "realtime",
    "prompt": {"id": "12", "variables": {"name": "Ada"}},
    "instructions": "Ada asked for a callback about her delivery window."
  }
}
```

`?agent_id=12` on the connection URL starts the agent as soon as the socket
connects, before any `session.update`, so it carries no per-call input. Add
`&wait_for_session_update=1` to wait for the first `session.update` and use its
instructions and variables.

In an inline session (no saved agent), `session.instructions` is the prompt
itself, as before. Builder mic and simulated test calls resolve defaults and
missing placeholders but have no per-call variables or context input.

## After the call

`GET /v1/calls`, `GET /v1/calls/{call_id}`, `telephony.complete`, and `web.complete` include the
final merged `variables` and `unresolved_variables`. Legacy completion payloads
that include `data.history` include them too:

```json
{
  "variables": {"name": "Ada", "account_id": "A-17"},
  "unresolved_variables": ["appointment_slot"]
}
```

The call's `prompt_snapshot` (`GET /v1/calls/{call_id}`) shows the agent's
prompt with its variables filled in and the context block added.

Store your CRM or task identifier in the variables object to join the completed
call back to its source record. Variables and context are retained with the call
record; only send information appropriate to retain in call history and
webhooks.

Replays of a call, in [validation sets](/api-reference/validation-sets),
[replay runs](/api-reference/replay-runs) and
[investigations](/api-reference/investigations), use that call's variables and
context with the prompt under test, so a replay tests what the agent on that
call would have been told.

## Existing prompt compatibility

Rendering also applies to existing saved-agent and A/B variant prompts, inline
outbound and realtime configurations, and prompts returned by configuration
webhooks. Unknown `{{name}}` placeholders become blank text, even when no
`variables` are supplied. Check existing prompts before rollout, including
externally supplied inline/webhook prompts that ThunderPhone cannot inventory.
Builder mic and simulation calls apply the same default/blank behavior.
