From cd27a895250a6a2854d2e925c25885f6c0c1faa8 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Fri, 9 Oct 2026 14:02:32 -0700 Subject: [PATCH 1/4] Updated how to transfer data between squad members --- fern/squads/handoff/context-and-variables.mdx | 288 +++++------------- .../passing-data-between-assistants.mdx | 188 ++---------- 2 files changed, 105 insertions(+), 371 deletions(-) diff --git a/fern/squads/handoff/context-and-variables.mdx b/fern/squads/handoff/context-and-variables.mdx index ee57233ec..d655166a7 100644 --- a/fern/squads/handoff/context-and-variables.mdx +++ b/fern/squads/handoff/context-and-variables.mdx @@ -1,34 +1,30 @@ --- title: Pass context and variables -subtitle: Control which conversation history and extracted variables reach the next assistant -description: Choose how much conversation history the receiving assistant sees, and extract variables so structured data follows the caller into the next assistant. +subtitle: Control the history and values available after a handoff +description: Choose the conversation history a destination receives and pass variables through handoff arguments, tool results, or extraction. slug: squads/handoff/context-and-variables --- -When a call moves to another assistant, you control what travels with it. Context engineering decides how much conversation history the receiving assistant can see, and variable extraction captures specific values and forwards them explicitly. +A handoff carries **conversation messages** and **variable values** separately. Set `contextEngineeringPlan` on the destination to choose which earlier messages its model sees. Values already collected in the call remain available to later assistants even if the destination receives no earlier messages. The destination gets its own system prompt in every mode. ## Context engineering -Control what conversation history transfers to the next assistant or squad. Set **contextEngineeringPlan** on any destination. +Set `contextEngineeringPlan` on an assistant or squad destination. It filters the messages sent to that destination; it does not clear saved variables. -### All messages (default) +| Type | Earlier messages the destination receives | +| --- | --- | +| `all` (default) | All earlier conversation messages, including tool calls and results. The previous system prompt is replaced by the destination's system prompt. | +| `lastNMessages` | The most recent `maxMessages` messages. Use a positive value: `0` currently selects all messages. | +| `userAndAssistantMessages` | User and assistant messages; tool calls and tool results are excluded. | +| `previousAssistantMessages` | Messages from before the current assistant received its incoming handoff. On the first handoff, there is no earlier assistant context to carry. | +| `none` | No earlier conversation messages. | -Transfers the entire conversation history: - -```json -{ - "contextEngineeringPlan": { - "type": "all" - } -} -``` - -### Last N messages - -Transfers only the most recent N messages. Use this to limit context size for performance: +For example, this destination receives the ten most recent messages: ```json { + "type": "assistant", + "assistantName": "Specialist", "contextEngineeringPlan": { "type": "lastNMessages", "maxMessages": 10 @@ -36,226 +32,104 @@ Transfers only the most recent N messages. Use this to limit context size for pe } ``` -### User and assistant messages only - -Transfers only user and assistant messages, filtering out system messages, tool calls, and tool results. This gives the next assistant a clean view of the conversation without internal implementation details: - -```json -{ - "contextEngineeringPlan": { - "type": "userAndAssistantMessages" - } -} -``` - - -Use `userAndAssistantMessages` when the destination assistant does not need to see tool call history or system prompts from the previous assistant. This produces a cleaner context and reduces token usage. - - -### Previous assistant messages - -Transfers only the conversation history from **before** the current assistant's session. This excludes the current assistant's own messages, tool calls, and tool results entirely, forwarding only the context that existed when the current assistant first received the call. + +`previousAssistantMessages` and `none` filter the destination model's message history. They do not erase values already in the variable bag or remove messages from a full-history call artifact. If you handle sensitive information, control what is collected, stored, extracted, and passed in variables as well as message history. + -```json -{ - "contextEngineeringPlan": { - "type": "previousAssistantMessages" - } -} -``` - -This mode is particularly useful when the current assistant handles sensitive data (such as payment card numbers in a PCI-compliant flow). By excluding the current assistant's session from the forwarded context, you prevent sensitive tool call results from reaching the next assistant. - - -Use `previousAssistantMessages` when handing off from a sensitive assistant (e.g., one collecting payment data) to a non-sensitive assistant. It preserves useful conversation context from earlier in the call while ensuring the sensitive assistant's tool call data is not forwarded. See the [PCI Compliance - Handoff Context Configuration](/security-and-privacy/pci#handoff-context-configuration) guide for a complete walkthrough. - - -### No context - -Starts the next assistant with a blank conversation: +## Variable extraction -```json -{ - "contextEngineeringPlan": { - "type": "none" - } -} -``` +Choose how a value reaches the destination based on its source: -## Variable extraction +| Value source | When it runs | What reaches the destination | +| --- | --- | --- | +| Standard call and time values | Vapi makes supported values available for the call or channel. See [Dynamic variables](/assistants/dynamic-variables). | Available values such as `customer.number`, `call.id`, and `now` can render in the prompt. | +| Initial custom values | Supply `assistantOverrides.variableValues` when starting the call. | Those named values can render in the destination prompt. | +| Earlier tool response | Configure `variableExtractionPlan.aliases` on the **result-producing tool**; it selects fields when the tool returns. | Only selected alias values are saved. A JSON response alone is not copied into the variable bag. | +| Handoff arguments | Define custom `function.parameters` on the handoff tool; the model fills them in the outgoing handoff tool call. | Every argument except `destination` is added to the bag. | +| Conversation content | Define `variableExtractionPlan.schema` on the destination; a separate model request runs at handoff. | Top-level properties in the extracted object are added to the bag if extraction succeeds. | +| Existing bag value | Liquid runs when the destination prompt is rendered. | A reference such as `{{customerName}}` reads the current value; it does not extract one. | -Extract and pass structured data during handoff. Variables extracted by the Handoff tool are available to all subsequent assistants in the conversation chain. When a handoff extracts a variable with the same name as an existing one, the new value replaces the previous value. +At handoff, Vapi adds tool arguments, then any schema-extracted values, then the chosen destination's `assistantOverrides.variableValues` or `squadOverrides.variableValues`. If the same key is set more than once, the later value wins. -### Extraction via `variableExtractionPlan` in destinations +### Handoff arguments -This extraction method makes an OpenAI structured output request to extract variables. Use this when you have multiple destinations, each with different variables that need to be extracted. +Use `function.parameters` when the outgoing assistant can provide a value as it decides to hand off. This tool passes `customerIntent` to `Specialist`: ```json { - "tools": [ + "type": "handoff", + "destinations": [ { - "type": "handoff", - "destinations": [ - { - "type": "assistant", - "assistantName": "order-processing-assistant", - "description": "customer is ready to place an order", - "variableExtractionPlan": { - "schema": { - "type": "object", - "properties": { - "customerName": { - "type": "string", - "description": "Full name of the customer" - }, - "email": { - "type": "string", - "format": "email", - "description": "Customer's email address" - }, - "productIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of product IDs customer wants to order" - }, - "shippingAddress": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" }, - "zipCode": { "type": "string" } - } - } - }, - "required": ["customerName", "productIds"] - } - } + "type": "assistant", + "assistantName": "Specialist", + "description": "Handle billing questions" + } + ], + "function": { + "name": "handoff_to_specialist", + "parameters": { + "type": "object", + "properties": { + "destination": { + "type": "string", + "enum": ["Specialist"] + }, + "customerIntent": { + "type": "string", + "description": "The caller's reason for contacting billing" } - ] + }, + "required": ["destination", "customerIntent"] } - ] + } } ``` -### Variable access patterns - -Once extracted, variables are accessible using Liquid template syntax (`{{variableName}}`). The access pattern depends on the schema structure: +The destination can use `{{customerIntent}}` in its prompt. `destination` selects the target and is not saved as a variable. For fixed destinations, Vapi derives the allowed destination values from the configured destinations. Dynamic routing can leave `destination` optional so the webhook can resolve it. Since the model supplies custom arguments, verify critical IDs against a trusted source. -| Schema type | Access pattern | Example | -|-------------|---------------|---------| -| Simple property | `{{propertyName}}` | `{{customerName}}` | -| Nested object | `{{object.property}}` | `{{name.first}}`, `{{name.last}}` | -| Array item | `{{array[index]}}` | `{{zipCodes[0]}}`, `{{zipCodes[1]}}` | -| Array of objects | `{{array[index].property}}` | `{{people[0].name}}`, `{{people[0].age}}` | -| Nested array | `{{array[index].nestedArray[index]}}` | `{{people[0].zipCodes[1]}}` | +### Destination schema extraction - -Top-level object properties are extracted as direct global variables. For example, a schema with properties `name` and `age` produces `{{name}}` and `{{age}}` -- not `{{root.name}}`. - - -### Variable aliases - -Use **aliases** to create additional variables derived from extracted values. Aliases support Liquid template syntax for transformations and compositions. +Use `variableExtractionPlan.schema` on the destination when you need a separate request to extract information from the outgoing assistant's currently available conversation: ```json { + "type": "assistant", + "assistantName": "Specialist", "variableExtractionPlan": { "schema": { "type": "object", "properties": { - "firstName": { - "type": "string", - "description": "Customer's first name" - }, - "lastName": { + "customerName": { "type": "string", - "description": "Customer's last name" - }, - "company": { - "type": "string", - "description": "Customer's company name" + "description": "Name the caller gave during the conversation" } - } - }, - "aliases": [ - { - "key": "fullName", - "value": "{{firstName}} {{lastName}}" - }, - { - "key": "greeting", - "value": "Hello {{firstName}}, welcome to {{company}}!" }, - { - "key": "customerCity", - "value": "{{addresses[0].city}}" - } - ] + "required": ["customerName"] + } } } ``` -Each alias creates a new variable accessible as `{{key}}` during the call and stored in `call.artifact.variableValues` after the call. Alias keys must start with a letter and contain only letters, numbers, or underscores (max 40 characters). +The destination can read the top-level property as `{{customerName}}`. A nested object can be read with a path such as `{{shippingAddress.city}}`. An earlier history filter can limit what the extraction request sees; it does not recover messages removed from the outgoing assistant's context. -### Extraction via `tool.function` +Extraction is best effort. Without a `schema`, the plan adds nothing. If the request fails or returns a non-object, the handoff continues without new extracted values. A required schema property is not guaranteed to be available in the destination prompt. -You can also extract variables through the LLM tool call parameters (in addition to sending these parameters to your server in a `handoff-destination-request` for dynamic handoffs). Include the **destination** parameter with the assistant names or IDs in `enum` -- Vapi uses this to determine where to hand off the call. The `destination` parameter itself is not extracted as a variable. Add `destination` and all other required variables to the schema's `required` array. +Use [tool-result aliases](/tools/static-variables-and-aliases/extract-and-chain) on the tool that produces a response when you need exact fields from that response. A handoff destination's `variableExtractionPlan` uses its **`schema`** for conversation extraction; aliases there are not applied by the current handoff path. -```json -{ - "tools": [ - { - "type": "handoff", - "destinations": [ - { - "type": "assistant", - "assistantName": "order-processing-assistant", - "description": "customer is ready to place an order" - } - ], - "function": { - "name": "handoff_to_order_processing_assistant", - "parameters": { - "type": "object", - "properties": { - "destination": { - "type": "string", - "description": "The destination to handoff the call to.", - "enum": ["order-processing-assistant"] - }, - "customerName": { - "type": "string", - "description": "Full name of the customer" - }, - "email": { - "type": "string", - "format": "email", - "description": "Customer's email address" - }, - "productIds": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of product IDs customer wants to order" - }, - "shippingAddress": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" }, - "zipCode": { "type": "string" } - } - } - }, - "required": ["destination", "customerName", "email"] - } - } - } - ] -} -``` +### Reuse values after another handoff + +Suppose assistant A passes `customerIntent` as a handoff argument to B. B extracts `customerName`, then hands off to C with `contextEngineeringPlan.type` set to `none`. C receives no earlier conversation messages, but can still use `{{customerIntent}}` and `{{customerName}}` from the shared variable bag. If B sets `customerIntent` again, its later value replaces A's value. + +## Configuration paths and verification + +The history and variable rules above apply to a completed assistant or squad handoff. Check how the handoff is configured: + +| Path | What to check | +| --- | --- | +| Saved squad, API or Dashboard | Use a handoff tool on the outgoing assistant, or configure a squad member's Handoff Tools. For custom `function.parameters`, configure the tool itself; the squad member Handoff Tools setting creates a tool without those parameters. | +| Saved handoff tool in the Dashboard | The Tools page exposes function parameters for the handoff tool. Save the tool and attach it to the outgoing assistant. | +| Standalone assistant in a live call | A destination can identify a saved assistant by `assistantId`, or provide an inline assistant. Verify the destination resolves in the call. | +| `/chat` API | Assistant destinations require a squad context with more than one assistant. A standalone assistant's handoff tool call can return a handoff result without activating its destination. | +| Dynamic destination | A `handoff-destination-request` webhook chooses the destination at runtime. Its returned destination can include context and extraction plans. See [Dynamic handoffs](/squads/handoff/destinations#dynamic-handoffs). | + +To confirm a handoff, inspect the call or chat transcript for the outgoing handoff tool call and the next assistant's response. For calls, use `call.id` to correlate records and inspect `artifact.variableValues` for values retained in the call. The active conversation used by analysis may differ from the artifact after context filtering; set [`artifactPlan.fullMessageHistoryEnabled`](/assistants/call-recording) if you need the full cross-assistant message history in the call artifact. diff --git a/fern/squads/passing-data-between-assistants.mdx b/fern/squads/passing-data-between-assistants.mdx index 94c099f9e..ab1051241 100644 --- a/fern/squads/passing-data-between-assistants.mdx +++ b/fern/squads/passing-data-between-assistants.mdx @@ -1,188 +1,48 @@ --- title: Passing data between assistants -subtitle: Three approaches for forwarding context to the next assistant in a squad — when to use each, and what each one costs +subtitle: Choose how values reach the next assistant in a handoff slug: squads/passing-data-between-assistants -description: Forward context to the next assistant in a Vapi squad using three handoff mechanisms, with a decision guide on their latency, accuracy, and trade-offs. +description: Compare handoff arguments, destination extraction, tool-result aliases, and Liquid variables for passing data between assistants. --- -When an assistant in a squad hands off to another assistant, you usually need to forward something — the caller's name, an extracted intent, an upstream tool's result, a session ID. Vapi gives you **three different mechanisms** to do this. Each one trades off latency, accuracy, and where the value comes from. Picking the wrong one is the single most common reason squad handoffs feel slow or unreliable. +Assistants in a call share variable values across completed handoffs. A destination can read those values even when its `contextEngineeringPlan` excludes earlier conversation messages. Start with the value's source: -This page is a decision guide. For end-to-end configuration of the handoff itself, see the [Handoff tool](/squads/handoff) page. +| If the value comes from... | Use... | What happens | +| --- | --- | --- | +| The handoff decision, such as classified intent | [Handoff arguments](#approach-1-handoff-arguments) | The outgoing model fills custom `function.parameters` in the handoff tool call. Every argument except `destination` is saved as a variable. | +| Conversation content that needs a dedicated extraction request | [Destination schema extraction](#approach-2-variable-extraction) | Vapi extracts an object from the outgoing assistant's currently available conversation and saves its properties. | +| A previous tool's JSON response | [Tool-result aliases](/tools/static-variables-and-aliases/extract-and-chain) | Aliases on the result-producing tool select fields to save. The raw response is not automatically saved as variables. | +| A standard call value or a value already saved | [Liquid in the destination prompt](#approach-3-liquid-templating) | The destination reads it with `{{variableName}}`; Liquid does not create a value. | -## The three approaches at a glance - -| Approach | Where the value comes from | LLM involved? | Latency | Hallucination risk | Best for | -| -------- | -------------------------- | ------------- | ------- | ------------------ | -------- | -| **Handoff arguments** (`function.parameters` on the handoff tool) | The model decides, inline with the same tool call that triggers the handoff | Yes — piggybacks on the LLM call already happening | Zero added | Yes (model fills the value) | Classifications, summaries, sentiment, intent — anything the model has to derive from the live conversation | -| **Variable extraction** (`variableExtractionPlan.schema` on the destination) | The model extracts from the full conversation transcript | Yes — separate dedicated LLM call | Full LLM round-trip (hundreds of ms) | Yes | Structured extraction with a dedicated prompt — e.g. pulling `dateOfBirth`, `appointmentTime` from the user's last few utterances | -| **Liquid templating in the destination's prompt** | Already in the variable bag (call data, prior tool results, prior extractions) | No — pure template substitution | Sub-millisecond per render | No (deterministic) | Forwarding values that already exist — caller phone number, prior `lookupPatient` result, time variables | +For the history modes, value precedence, configuration paths, and a two-hop example, see [Pass context and variables](/squads/handoff/context-and-variables). ## Approach 1: Handoff arguments -Define `function.parameters` on the handoff tool. The LLM that's already generating the handoff tool call also fills in your custom arguments as part of the same call — no extra round-trip. - - -**Availability today:** - -- **API:** Fully supported. Send the JSON below via `POST /tool` or as part of your assistant's `model.tools[]` via `POST /assistant` / `PATCH /assistant`. -- **Dashboard — Tools page:** UX for defining `function.parameters` on a handoff tool is shipping soon. Use the API in the meantime. -- **Dashboard — Squad builder:** Configuring a handoff via the squad member's **Handoff Tools** section does NOT currently carry `function.parameters` through to the runtime tool (backend synthesizes the tool without the `function` field). Until that's fixed, put the handoff tool directly on the assistant's `model.tools[]` (via the API or the Tools page) instead of defining it per squad-member destination. - - - - -```json -{ - "type": "handoff", - "function": { - "name": "handoff_to_specialist", - "description": "Hand off to the specialist when the customer is ready", - "parameters": { - "type": "object", - "required": ["destination", "customerIntent", "customerSentiment"], - "properties": { - "destination": { - "type": "string", - "enum": ["specialist"] - }, - "customerIntent": { - "type": "string", - "enum": ["new-customer", "existing-customer", "billing-issue"], - "description": "What the customer is calling about" - }, - "customerSentiment": { - "type": "string", - "enum": ["positive", "neutral", "frustrated"], - "description": "Caller's overall sentiment" - } - } - } - }, - "destinations": [ - { - "type": "assistant", - "assistantName": "Specialist" - } - ] -} -``` - -The next assistant receives `customerIntent` and `customerSentiment` in the variable bag and can reference them as `{{customerIntent}}` / `{{customerSentiment}}` in its prompts. - -**Use this when** the value only exists "in the model's head" — it has to be derived from the live conversation, but you don't need a separate dedicated extraction call. - -**Avoid this when** the value already exists somewhere structured (a prior tool result, the call's `customer.number`, etc.) — the model could mishear or paraphrase it. Use Approach 3 for those. - -## Approach 2: Variable extraction (`variableExtractionPlan.schema`) - -Define a `variableExtractionPlan.schema` on the handoff destination. After the handoff fires, Vapi makes a dedicated LLM call against the full conversation transcript to fill the schema, then merges the result into the variable bag for the next assistant. - -```json -{ - "type": "assistant", - "assistantName": "Scheduler", - "variableExtractionPlan": { - "schema": { - "type": "object", - "required": ["preferredDate", "preferredTime"], - "properties": { - "preferredDate": { - "type": "string", - "description": "The date the caller asked to schedule for, in YYYY-MM-DD format" - }, - "preferredTime": { - "type": "string", - "description": "The time of day the caller asked for, in 24-hour HH:MM format" - } - } - } - } -} -``` +Define custom `function.parameters` on the handoff tool when the outgoing assistant should supply a value as it routes the caller. The model fills these arguments in the same tool call that initiates the handoff. For example, a `customerIntent` argument becomes `{{customerIntent}}` for the destination; `destination` only selects where the handoff goes. -**Use this when** the value lives across several user utterances and needs a dedicated extraction prompt to get reliably. Schema validation gives you typed output and lets you constrain values via JSON-schema `enum` / `pattern`. +Configure custom parameters on a saved or inline handoff tool through the API or the Dashboard Tools page. A squad member's **Handoff Tools** destination setting creates a handoff tool without custom `function.parameters`, so configure the tool itself when you need arguments. See the [handoff argument example](/squads/handoff/context-and-variables#handoff-arguments). -**Avoid this when** zero added latency matters — this path adds a full LLM round-trip per handoff (typically a few hundred ms). For high-traffic flows where the value is something the model can fill inline, Approach 1 is faster. +The model produces these values. For an ID from a database or another trusted system, extract the exact field from the result-producing tool with an alias instead of asking the model to repeat it. -For full configuration details — multiple destinations, dynamic handoffs, context engineering — see the [Variable extraction section of the Handoff tool page](/squads/handoff/context-and-variables#variable-extraction). +## Approach 2: Variable extraction -## Approach 3: Liquid templating in the destination's prompt +Put `variableExtractionPlan.schema` on the handoff destination when a value must be assembled from conversation content. Vapi makes a separate extraction request against the outgoing assistant's available conversation and adds the returned top-level properties before the destination starts. -The variable bag is **shared across every assistant in the squad** for the lifetime of the call. Anything that's been put into it — by Approach 1, Approach 2, by a prior tool call returning JSON, by call-level data like `customer.number` and `phoneNumber.number`, by time variables like `now` and `year` — is reachable from any subsequent assistant's prompt via Liquid syntax. No extra wiring required. +This path adds a model request at handoff time. Extraction is best effort: if the request fails or returns a non-object, the handoff continues without new extracted values. An earlier history filter can limit what the extraction request sees. See the [destination schema example](/squads/handoff/context-and-variables#destination-schema-extraction). -```text -You are the scheduling specialist. The caller is {{customer.name}}, calling -from {{customer.number}}. Their patient ID is {{patientId}} (looked up earlier -this call). They want a {{preferredAppointmentType}} appointment. - -Today is {{currentDateTime}}. -``` +## Approach 3: Liquid templating -If `customer.name`, `patientId`, etc. are in the bag, they render. If they're not, they render as the literal token `{{patientId}}` (so the caller might hear "patientId" spoken — worth handling defensively in your prompt). - -**Use this when** the value is already in the bag — there's no reason to re-extract via LLM what you already have structurally. Sub-millisecond, deterministic, free. - -**Avoid this when** the value isn't in the bag yet. Liquid can't extract from the conversation; it can only forward what's already there. - - -**Sensitive fields are sanitized.** Vapi automatically redacts credential-like keys (`twilioAuthToken`, `twilioApiSecret`, `serverUrlSecret`, `accountSid`, `callToken`, `credentialId`, etc.) from the variable bag before any prompt rendering. References like `{{phoneNumber.twilioAuthToken}}` will render as `[REDACTED]` rather than leaking the actual credential. - - -## Decision flowchart +Use Liquid when the value is already available. This includes supported [standard call and time variables](/assistants/dynamic-variables), custom values supplied at call start, handoff arguments, earlier schema extraction, and values selected from tool results with aliases. ```text -What do you want the next assistant to know? -│ -├─ "Something the model just heard / classified / summarized" -│ └─→ Approach 1: Handoff arguments -│ Zero added latency, model fills inline. -│ -├─ "Something the user explicitly said and I want a dedicated, schema-validated extraction" -│ └─→ Approach 2: variableExtractionPlan.schema -│ Adds an LLM round-trip, but you get structured output and a focused -│ extraction prompt. -│ -└─ "Something I already have — call data, prior tool result, prior extraction" - └─→ Approach 3: Reference it via Liquid in the destination's prompt - No extra cost. Use {{customer.number}}, {{patientId}}, etc. directly. +You are the scheduling specialist. The caller's number is {{customer.number}}. +Their patient ID is {{patientId}} and their requested date is {{preferredDate}}. ``` -## Common patterns - -### Pattern: "Forward an extracted ID after a database lookup" - -A `lookupPatient` tool returned `{patientId: "p_42", dob: "1990-01-15"}` on assistant A. Assistant B needs `patientId`. - -Use **Approach 3** — it's already in the bag. Assistant B's prompt: `The patient ID is {{patientId}}.` Don't re-extract it via schema; the model could mishear digits. - -### Pattern: "Categorize what the caller wants and route on it" - -Caller spent two turns describing a problem. Assistant A needs to classify the intent and hand off to a specialist who knows about that intent. - -Use **Approach 1** — handoff arguments with an `enum` for `intent`. The classifying assistant's tool call carries the intent inline; the destination assistant reads `{{intent}}`. - -### Pattern: "Pull a structured booking request out of free-form speech" - -Caller said "I want to come in next Tuesday around 2 PM, maybe earlier if there's something". Assistant A needs `{preferredDate, preferredTime, alternativesOK}` as structured fields. - -Use **Approach 2** — `variableExtractionPlan.schema` with the destination. The dedicated extraction prompt + schema validation catches the structure better than inline arguments. - -### Pattern: "Mix and match" - -You can combine all three on a single handoff. Common shape: handoff arguments for the LLM-classified intent, schema extraction for one structured field that needs the dedicated prompt, and the destination's system prompt directly references prior tool results via Liquid. - -## What if extraction fails? - -Vapi's handoff path is failure-isolated: - -- An empty `variableExtractionPlan` (`{}`) is a graceful no-op — the handoff proceeds without extraction. -- A schema-extraction LLM failure (5xx, timeout, rate limit) is logged and the handoff proceeds with no extracted variables — it does not bail the handoff. -- A schema-extraction result that isn't a plain object (an array, a primitive, `null`) is dropped before merge — it does not corrupt the variable bag. +The prompt only reads these values. If `patientId` came from a `lookupPatient` response, configure an alias on that tool to save the exact response field first. Check that a value exists before depending on it in the destination prompt. -So extraction is best-effort; if values are critical for the next assistant to function, prefer **Approach 1** (handoff arguments — required by the function schema, blocks the LLM call until provided) or **Approach 3** (reference values you already have). +## Combining approaches -## Next steps +An A → B → C flow can use more than one method. A can hand off to B with a model-generated `customerIntent` argument. B can run a lookup tool with an alias that saves `patientId`, then hand off to C with `contextEngineeringPlan.type` set to `none`. C gets a fresh message history but can still render `{{customerIntent}}` and `{{patientId}}` from the shared variable bag. Later values with the same key replace earlier ones. -- [Handoff tool](/squads/handoff) — full configuration reference for the handoff tool itself. -- [Static variables and aliases](/tools/static-variables-and-aliases) — how the variable bag is built and what's available in scope. -- [Dynamic variables](/assistants/dynamic-variables) — set initial variables when starting a call. +For destination setup, see the [Handoff tool](/squads/handoff) and [Configure handoff destinations](/squads/handoff/destinations). From e24b77f329bc6ca290f1a2f5a28074c52da48ff6 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Mon, 12 Oct 2026 12:50:53 -0700 Subject: [PATCH 2/4] docs: refine handoff context copy and cross-link --- fern/squads/handoff/context-and-variables.mdx | 4 +++- fern/tools/static-variables-and-aliases/extract-and-chain.mdx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fern/squads/handoff/context-and-variables.mdx b/fern/squads/handoff/context-and-variables.mdx index d655166a7..5fdf696d6 100644 --- a/fern/squads/handoff/context-and-variables.mdx +++ b/fern/squads/handoff/context-and-variables.mdx @@ -5,7 +5,9 @@ description: Choose the conversation history a destination receives and pass var slug: squads/handoff/context-and-variables --- -A handoff carries **conversation messages** and **variable values** separately. Set `contextEngineeringPlan` on the destination to choose which earlier messages its model sees. Values already collected in the call remain available to later assistants even if the destination receives no earlier messages. The destination gets its own system prompt in every mode. +A handoff carries **conversation messages** and **variable values** separately. Set `contextEngineeringPlan` on the destination to choose which earlier messages its model sees. The destination gets its own system prompt in every mode. + +Values already collected in the call remain available to later assistants even if the destination receives no earlier messages. ## Context engineering diff --git a/fern/tools/static-variables-and-aliases/extract-and-chain.mdx b/fern/tools/static-variables-and-aliases/extract-and-chain.mdx index ce7535746..df1badca2 100644 --- a/fern/tools/static-variables-and-aliases/extract-and-chain.mdx +++ b/fern/tools/static-variables-and-aliases/extract-and-chain.mdx @@ -185,7 +185,7 @@ Static `parameters` is not a field on the Handoff tool itself -- handoff doesn't 2. **Server-trusted derived data flows forward via the variable bag.** Aliases extracted by an earlier assistant's `variableExtractionPlan` (from a server-trusted source -- for example, an `apiRequest` keyed on `{{ customer.number }}`) persist across handoffs and remain referenceable as Liquid variables in the next assistant's tools. 3. **Static handoff-time injection via `destination.assistantOverrides.variableValues`.** Defined statically in the handoff configuration, merged into the variable bag at handoff time, bypasses the LLM entirely. Use this for per-destination config the next assistant should know about (`{ "tier": "premium" }`, `{ "slaWindowSeconds": 30 }`). -For full coverage of the three approaches, when to choose each, and the latency/accuracy tradeoffs, see [Passing data between assistants](/squads/passing-data-between-assistants). +For a source-based comparison of handoff arguments, destination extraction, tool-result aliases, and Liquid values, see [Passing data between assistants](/squads/passing-data-between-assistants). **Threat-model note for security-sensitive values.** The squads guide's *Approach 1: Handoff arguments* (using `function.parameters` on the Handoff tool) is correct for **LLM-derived** values like classifications, summaries, sentiment, intent. It is **not** a security boundary -- the model fills those args, and prompt injection can corrupt them. For **signaling-derived** trusted values like the verified caller-ID, only the call-level Liquid variables (Approach 3 in the squads guide) keep the LLM out of the chain. From c0e48a23a42c6cf33603dcfc584d283b8f3d6e87 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Mon, 19 Oct 2026 15:17:43 -0700 Subject: [PATCH 3/4] Checking in draft changes Co-Authored-By: Claude Opus 5.5 --- fern/calls/assistant-based-warm-transfer.mdx | 18 +- fern/calls/call-ended-reason.mdx | 3 +- .../troubleshoot-call-forwarding-drops.mdx | 254 ++++-------------- fern/tools/transfer-call.mdx | 28 +- fern/tools/transfer-call/blind-transfer.mdx | 2 + 5 files changed, 97 insertions(+), 208 deletions(-) diff --git a/fern/calls/assistant-based-warm-transfer.mdx b/fern/calls/assistant-based-warm-transfer.mdx index ae8c4ddfd..d32c1c47b 100644 --- a/fern/calls/assistant-based-warm-transfer.mdx +++ b/fern/calls/assistant-based-warm-transfer.mdx @@ -9,6 +9,8 @@ Assistant-based warm transfer places the customer on hold while a transfer assis Use this mode when the destination must accept the call or receive context before speaking with the customer. For a direct transfer without an operator conversation, use the [Transfer Call tool](/tools/transfer-call) with its default blind-transfer mode. +Assistant-based warm transfer works on Twilio phone numbers when the destination is a phone number, and on Vapi numbers and bring-your-own SIP trunks for phone-number and SIP destinations. On a Twilio call, a SIP destination is transferred without the transfer assistant. + ## Prerequisites Before you configure the transfer, prepare: @@ -26,7 +28,7 @@ When the original assistant invokes the transfer call tool: 2. Vapi calls the destination and starts the transfer assistant. 3. The transfer assistant speaks with the operator and uses the previous conversation as context by default. 4. The transfer assistant calls `transferSuccessful` to connect the parties or `transferCancel` to return the customer to the original assistant. -5. If the destination is busy, unreachable, or not human, the fallback plan determines what the customer hears and whether the call ends. +5. With `transferAssistant` configured, a busy or unanswered destination rolls back to the configured fallback. The transfer assistant can also cancel when an operator declines or it reaches voicemail or an automated system. A transfer that doesn't finish within `transferAssistant.maxDurationSeconds` (120 seconds by default) rolls back. The `transferSuccessful` and `transferCancel` tools are always available to the transfer assistant. You can customize them through the API, but you do not need to add them to a basic configuration. @@ -155,7 +157,7 @@ Use these fields to control the transfer assistant and the two call legs. See th | `transferAssistant.model` | Model and instructions used for the operator conversation | Required when you configure `transferAssistant` | | `holdAudioUrl` | MP3 or WAV audio played to the customer while on hold | Default hold audio | | `transferCompleteAudioUrl` | MP3 or WAV audio played to the destination after the introduction | No custom completion audio | -| `fallbackPlan` | Customer message and end-call behavior when the transfer fails | Default fallback behavior when omitted | +| `fallbackPlan` | Customer message and end-call behavior after a failed warm transfer | If omitted, "Could not transfer the call." plays and the original assistant continues | | `summaryPlan` | Operator summary content and generation timeout | Configure with `enabled`, `messages`, and `timeoutSeconds` | | `contextEngineeringPlan` | Conversation context provided to the transfer assistant | All previous messages | @@ -166,7 +168,7 @@ Use these fields to control the transfer assistant and the two call legs. See th Keep the transfer assistant's system message focused on the operator conversation. Define when to use each built-in tool: - Call `transferSuccessful` after a human operator confirms that they will take the call -- Call `transferCancel` for voicemail, a busy signal, no answer, an IVR or automated hold queue, or an operator who declines +- Call `transferCancel` when the assistant hears voicemail, an interactive voice response (IVR) system, an automated hold queue, or an operator declining the call - Answer questions about the transfer or give a brief customer summary - Avoid unrelated conversation and complete the decision before `maxDurationSeconds` @@ -176,7 +178,11 @@ For example: Call transferCancel if the destination plays an IVR message, automated queue announcement, or hold message. Call transferSuccessful only after a live operator responds and accepts the call. ``` -The transfer assistant receives the previous conversation by default. Set `contextEngineeringPlan.type` to `none` when the operator must not receive that context, or use `lastNMessages` to limit it. +Busy and no-answer call statuses can trigger rollback before the transfer assistant speaks. The transfer assistant receives the previous conversation by default. Set `contextEngineeringPlan.type` to `none` when the operator must not receive that context, or use `lastNMessages` to limit it. + +When you configure the transfer through the API, set `fallbackPlan.endCallEnabled`. `false` plays the fallback message and returns the caller to the original assistant. `true` plays the message and ends the call. If omitted, the caller stays on the call with the original assistant. + +Keep `transferPlan.transferAssistant` configured when you depend on no-answer fallback or a transfer timeout. In the current Dashboard flow, disabling Summary generation can remove that configuration and prevent a no-answer event from starting the fallback. Keep Summary generation enabled or configure `transferAssistant` through the API, then verify the result with a controlled call. ## Verify the warm transfer @@ -187,7 +193,7 @@ Place a test call and trigger the transfer. Confirm that: 3. Accepting the call connects the customer and operator. 4. Declining the call or reaching voicemail returns the customer to the original assistant or ends the call according to `fallbackPlan`. -In [**Logs → Calls**](/observability/logs/call-logs), inspect the original call, destination call leg, transcript, and ended reason. To retrieve these details through the API, see the [Get Call API reference](/api-reference/calls/get). An `assistant-forwarded-call` ended reason confirms that the transfer was initiated; it does not confirm that the destination answered. +In [**Logs → Calls**](/observability/logs/call-logs), inspect the original call, any available destination-leg record, transcript, and ended reason. To retrieve call details through the API, see the [Get Call API reference](/api-reference/calls/get). An `assistant-forwarded-call` ended reason alone does not confirm that the destination answered. ## Troubleshooting @@ -196,7 +202,7 @@ In [**Logs → Calls**](/observability/logs/call-logs), inspect the original cal | The customer is transferred without an operator conversation | The destination uses a blind or non-assistant warm-transfer mode | Set `transferPlan.mode` to `warm-transfer-experimental`. | | The transfer assistant never speaks | `firstMessage` is omitted or `firstMessageMode` waits for the operator | Set `firstMessage` and use `assistant-speaks-first`, or let the operator speak first. | | The transfer assistant talks but never connects the parties | The prompt does not require `transferSuccessful` | Tell the transfer assistant to call `transferSuccessful` immediately after the operator accepts. | -| Voicemail, an IVR, a hold queue, or a declined transfer connects anyway | The prompt does not define cancellation conditions | Require `transferCancel` for non-human destinations, busy signals, no answer, and declined transfers. | +| Voicemail, an IVR, a hold queue, or a declined transfer connects anyway | The prompt does not define cancellation conditions | Require `transferCancel` for non-human destinations and declined transfers. Busy and no-answer call statuses can trigger rollback before the transfer assistant speaks. | | The customer hears summary text instead of the fallback message | The transfer produced unexpected `warm-transfer-experimental` behavior | Record the affected call ID, the message the customer heard, whether `summaryPlan` was enabled, and the `endedReason`, then contact support. | | The customer hears the wrong audio | Hold audio was configured as a tool message | Put customer hold audio in `transferPlan.holdAudioUrl`. Use `transferCompleteAudioUrl` for audio played to the destination after the introduction. | | The customer cannot continue after a failed transfer | `fallbackPlan.endCallEnabled` is `true` | Set it to `false` so the original assistant remains on the call. | diff --git a/fern/calls/call-ended-reason.mdx b/fern/calls/call-ended-reason.mdx index 4f9e3f793..369dbd52e 100644 --- a/fern/calls/call-ended-reason.mdx +++ b/fern/calls/call-ended-reason.mdx @@ -208,7 +208,8 @@ For SIP trunk setup issues, see [Troubleshoot SIP trunk credential errors](/adva - `call.ringing.hook-executed-transfer` — A transfer hook executed during ringing ended the call. - `call.ending.hook-executed-say` — A say hook executed during the ending phase. - `call.ending.hook-executed-transfer` — A transfer hook executed during the ending phase. -- `call.forwarding.operator-busy` — The operator was busy during call forwarding. +- `call.forwarding.operator-busy` — The destination was busy, or an assistant-based warm transfer failed and the fallback ended the call. +- `call.forwarding.no-answer` — A monitored SIP transfer destination did not answer. ## Other reasons diff --git a/fern/calls/troubleshoot-call-forwarding-drops.mdx b/fern/calls/troubleshoot-call-forwarding-drops.mdx index 4382d45d5..94b7a2fed 100644 --- a/fern/calls/troubleshoot-call-forwarding-drops.mdx +++ b/fern/calls/troubleshoot-call-forwarding-drops.mdx @@ -1,24 +1,21 @@ --- title: "Debug call forwarding drops" -subtitle: "Learn to troubleshoot calls that drop immediately after initiating transfer" -description: Troubleshoot Vapi calls that drop immediately after a transfer, finding why forwarding disconnects both parties and how to resolve the underlying cause. +subtitle: "Trace transfer attempts, failed connections, and caller recovery" +description: Diagnose transfer tool invocation, provider routing, busy and unanswered destinations, fallback behavior, and the evidence to send Support. --- -## Overview +An assistant saying it will transfer, Vapi starting a transfer, and the destination answering are separate events. Use the call record and provider logs to find where the attempt stopped. -When you initiate call forwarding, you expect the call to transfer to the destination. Instead, the call drops immediately after initiating the transfer, leaving both parties disconnected. +## Quick diagnosis -**In this guide, you'll learn to:** +| What you observe | Check next | Likely owner | +| --- | --- | --- | +| The assistant says it will transfer, but the call continues | Look for a `transferCall` invocation in the call's **Messages** tab | Assistant prompt or model decision | +| The `transferCall` tool runs, but the transfer does not start | Inspect the call's **Logs** tab for a Tool or Transfer error | Tool configuration or Vapi transfer initiation | +| The call ends with `assistant-forwarded-call`, but the destination does not answer | Match `phoneCallProviderId` to the provider's call record | Provider route, destination, or delegated call control | +| A warm transfer returns to the original assistant or ends after a fallback message | Check `transferPlan.mode`, `fallbackPlan.endCallEnabled`, and the transfer attempt | Configured fallback or transfer-assistant decision | -- Identify why calls drop during forwarding -- Check call logs and API responses systematically -- Analyze telephony provider logs for transfer failures -- Resolve configuration issues preventing successful transfers - - - This guide focuses on the specific scenario where calls drop immediately after - transfer initiation, not general call quality issues. - +`assistant-forwarded-call` means Vapi started or handed off the transfer. It does not prove that the destination rang, answered, or spoke with the caller. ## Prerequisites @@ -26,15 +23,16 @@ Before you start debugging, ensure you have: - **Call ID** from the dropped call - **API access** to make GET requests to Vapi -- **Admin access** to your telephony provider dashboard (Twilio, Vonage, Telnyx) -- **Wireshark** installed (for SIP-based calls only) +- **Access** to the call's **Messages** and **Logs** tabs, when logs were retained +- **Access** to the phone provider's call record, if the transfer reached that provider +- **A SIP trace or packet capture** when debugging a SIP transfer and one is available ## Troubleshooting workflow -First, verify whether Vapi successfully initiated the forwarding. +First, check the call's `endedReason` and the [Messages tab](/observability/logs/call-logs#messages). A spoken promise to transfer is not a `transferCall` invocation. ```bash title="cURL" @@ -47,149 +45,51 @@ curl -X GET "https://api.vapi.ai/call/{call_id}" \ import { VapiClient } from "@vapi-ai/server-sdk"; const client = new VapiClient({ token: process.env.VAPI_API_KEY }); -const call = await client.calls.get("ca123456xxxxx"); +const call = await client.calls.get("YOUR_CALL_ID"); console.log("End reason:", call.endedReason); ``` ```python title="Python SDK" +import os from vapi import Vapi client = Vapi(token=os.getenv("VAPI_API_KEY")) -call = client.calls.get("ca123456xxxxx") +call = client.calls.get("YOUR_CALL_ID") print(f"End reason: {call.ended_reason}") ``` -**Check the response:** - -```json title="Expected response" -{ - "id": "ca123456xxxxx", - "endedReason": "assistant-forwarded-call", - "status": "ended", - "destination": { - "type": "number", - "number": "+1234567890" - }, - "phoneCallProviderId": "twilio_call_abc123" -} -``` - -**What the `endedReason` tells you:** - -- `"assistant-forwarded-call"` → Vapi forwarded successfully, continue to Step 2 -- Any other value → Forwarding wasn't initiated, check your assistant configuration - - - - - -If you're handling call control through server messages, this can prevent Vapi from managing transfers. - -Check your assistant's `serverMessages` configuration: - - -```json title="Problematic configuration" -{ - "assistant": { - "serverMessages": ["phone-call-control", "status-update"] - } -} -``` - -```json title="Correct configuration" -{ - "assistant": { - "serverMessages": ["status-update"] - } -} -``` - - +**Interpret the result:** - - If `"phone-call-control"` is present, your server is overriding Vapi's call - control. Remove it unless you're implementing custom transfer logic. - +- No `transferCall` tool call: check the assistant prompt and tool assignment. The model did not invoke the transfer tool. +- A `transferCall` tool call followed by a Tool or Transfer error: inspect the destination, transfer plan, and [call logs](/observability/logs/call-logs#logs). +- `assistant-forwarded-call`: Vapi submitted the transfer to the phone provider or sent a `phone-call-control` request to your server. Check the downstream result before treating it as a connected call. +- `call.in-progress.error-transfer-failed`, `call.forwarding.operator-busy`, or `call.forwarding.no-answer`: inspect the transfer attempt and provider result. These reasons do not all mean the assistant failed to invoke the tool. - - -The `phoneCallProviderBypassEnabled` flag determines whether Vapi handles call control directly. + -**Recommended configuration:** +When `phoneCallProviderBypassEnabled` is `true`, or the assistant includes `phone-call-control` in `serverMessages`, Vapi sends a `phone-call-control` request to your server instead of directly transferring the provider call. Check your server's response and provider-side action before changing either setting. -```json title="Standard Vapi forwarding" -{ - "phoneCallProviderBypassEnabled": false -} -``` - - - Only set this to `true` if you're implementing custom call control through - your telephony provider. - +Use [Webhook logs](/observability/logs/webhook-logs) to inspect a recorded server-message delivery. If your application intentionally controls the provider call, its logs and provider call record are the source of the downstream outcome. -Not all transfer scenarios are supported. Verify your use case: - -| From | To | Supported | -| ---------- | ------------ | ------------------- | -| Phone call | Phone number | ✅ Yes | -| Web call | Phone number | ❌ No | -| Phone call | SIP number | ❌ No (PSTN to SIP) | -| SIP call | SIP number | ✅ Yes | -| SIP call | Phone number | ✅ Yes | +The built-in phone and SIP transfer path requires a supported phone transport. A web call's WebSocket transport rejects both phone-number and SIP transfer destinations. A phone call can transfer to a phone number or SIP URI when its provider supports the configured route. - - Web-to-phone transfers are not supported. The call will always drop in this - scenario. - +For a SIP destination, check the inbound provider, `transferPlan.sipVerb`, and the receiving private branch exchange (PBX). For a phone-number destination, verify E.164 format and call the number directly to confirm it is reachable. -If the call shows `"assistant-forwarded-call"` but still drops, the issue is likely with your telephony provider. +Use the call's `phoneCallProviderId` and timestamp to find the matching record in your phone provider's logs. Check whether the provider received the transfer command, dialed the destination, and recorded an answer, busy signal, no answer, rejection, or other failure. -**Get your telephony call ID** from the Vapi call object: - -```json title="Extract telephony call ID" -{ - "id": "ca123456xxxxx", - "endedReason": "assistant-forwarded-call", - "phoneCallProviderId": "CAabc123" -} -``` - -**Check your provider's dashboard:** - - - -1. Go to [Twilio Console > Call Logs](https://console.twilio.com/us1/monitor/logs/calls) -2. Search using `phoneCallProviderId` (e.g., `CAabc123`) -3. Look for transfer-related errors in the call timeline -4. Check TwiML execution logs for failed transfers - - - - 1. Access [Vonage API Dashboard](https://dashboard.nexmo.com/) 2. Navigate to - Call Logs 3. Search using the telephony call ID and timestamp 4. Review call - flow details for transfer failures - - - -1. Open [Telnyx Mission Control Portal](https://portal.telnyx.com/) -2. Go to Call Detail Records -3. Search using the telephony call ID -4. Examine call records for forwarding errors - - +A successful API command to the provider is not proof that the downstream call connected. Compare the provider record with the Vapi call's `endedReason`, [Messages](/observability/logs/call-logs#messages), and [Logs](/observability/logs/call-logs#logs). A missing provider record may indicate a Vapi initiation or configuration problem; a provider error identifies a downstream failure to investigate with that provider. @@ -197,14 +97,14 @@ If the call shows `"assistant-forwarded-call"` but still drops, the issue is lik For SIP trunking or bring-your-own-number setups, analyze the SIP signaling. -**Download the packet capture:** +**Check whether a packet capture is available:** -```bash title="Get PCAP file" +```bash title="Get the call record" curl -X GET "https://api.vapi.ai/call/{call_id}" \ -H "Authorization: Bearer YOUR_API_KEY" ``` -The response includes a `pcapUrl` field. Download this file and open it in Wireshark. +For an eligible Vapi or bring-your-own SIP phone call, the completed call can contain `artifact.pcapUrl` when packet capture was enabled and retained. If present, download that URL and open the capture in a SIP analysis tool. A missing URL does not prove that no transfer command was sent. **Filter for transfer packets:** @@ -214,93 +114,47 @@ sip.Method == "REFER" **What to look for:** -- **REFER packet present** → Vapi sent the transfer request to your SIP provider -- **REFER packet missing** → Transfer wasn't initiated by Vapi -- **202 Accepted response** → SIP provider accepted the transfer -- **Error responses (4xx, 5xx)** → SIP provider rejected the transfer +- **REFER request present:** The transfer request reached the SIP signaling path captured in this file. +- **REFER response present:** Check the response code and any later NOTIFY or referred-call status. Acceptance of a REFER request alone does not prove that the destination answered. +- **No REFER request:** Check whether the transfer used `sipVerb: "dial"` or `sipVerb: "bye"`, whether capture started before the transfer, and whether the tool was invoked. Vapi phone numbers always dial the destination, so look for the dial leg instead. - - **Important:** SIP transfers are handled by your telephony provider, not Vapi. - Once Vapi sends the REFER packet, your SIP provider manages the actual - transfer process. - +With `sipVerb: "dial"`, inspect the outbound dial leg instead of looking for a REFER. With `sipVerb: "bye"`, Vapi ends its leg without dialing the destination. For more details on SIP configuration, see our [SIP trunk documentation](https://docs.vapi.ai/advanced/sip/sip-trunk#inbound-call-test). -## Common solutions - -Based on your findings, here are the most frequent fixes: - -### Call shows successful forwarding but still drops - -**Root cause:** Telephony provider couldn't complete the transfer - -**Solution:** Check destination number format and availability - -- Verify the destination number includes country code -- Test calling the destination directly outside of Vapi -- Check if destination has call blocking enabled - -### endedReason is not 'assistant-forwarded-call' - -**Root cause:** Transfer wasn't initiated due to configuration +## Diagnose busy, no answer, rejection, voicemail, and timeout -**Solution:** Review assistant settings +Recovery depends on the transfer mode. Check `transferPlan.mode` before changing a fallback setting. -- Remove `"phone-call-control"` from `serverMessages` -- Set `phoneCallProviderBypassEnabled` to `false` -- Verify your transfer function is properly configured +| Mode or path | What to check | Caller recovery | +| --- | --- | --- | +| Blind transfer or non-assistant warm transfer | Provider call record for busy, no answer, rejection, voicemail, or a failed destination leg | On Twilio, Vapi hands the call to Twilio and ends with `assistant-forwarded-call`, even if the destination is busy or doesn't answer. The caller's call ends, and voicemail counts as answered. On Vapi numbers and SIP trunks, busy and no-answer can have their own ended reasons when outcome detection is enabled. Otherwise the call ends with `assistant-forwarded-call`. | +| `warm-transfer-experimental` | Operator-leg status, `transferAssistant` configuration, transfer-assistant tool call, and timeout | With `transferAssistant` configured, busy and no-answer statuses roll back. `transferCancel` can roll back a declined call or detected voicemail. The configured fallback message plays after rollback. | +| SIP cold transfer with organization-enabled fallback | Transfer outcome, `fallbackPlan`, and provider record | Recovery is conditional on organization enablement and the failure path. Confirm it with a controlled call before using it for a critical escalation. | -### SIP REFER packets missing in PCAP +For assistant-based warm transfer, set `fallbackPlan.endCallEnabled` explicitly. `false` keeps the original assistant on the call after the fallback message; `true` ends the call after the message. Voicemail or an automated queue requires the transfer assistant to cancel, or configured voicemail detection to identify a non-human destination. Do not assume that a standard message, summary, or TwiML introduction detects voicemail. -**Root cause:** Vapi didn't send transfer request to SIP provider - -**Solution:** Check Vapi configuration - -- Verify SIP endpoint configuration -- Ensure destination format matches SIP addressing -- Check for conflicting call control settings - -## Limitations - -### Transfer scenario limitations - -- **Web calls** cannot be transferred to phone numbers -- **PSTN-to-SIP** transfers are not supported -- Cross-provider transfers may have compatibility issues - -### Configuration dependencies - -- `phoneCallProviderBypassEnabled` must be `false` for Vapi-managed transfers -- `serverMessages` with `"phone-call-control"` overrides default behavior -- SIP configuration requires proper endpoint setup +A failed transfer command can end with `call.in-progress.error-transfer-failed`. On an organization-enabled SIP cold transfer, a configured `fallbackPlan` can also handle a recoverable transfer-request failure while the caller is still connected to Vapi. It cannot recover a call that has already ended. An operator-leg busy or no-answer outcome can produce a different reason or return the caller to the original assistant, depending on the mode and recovery configuration. Read the call's messages and logs with the ended reason rather than using that field alone to decide where the failure occurred. ## When to contact support -Escalate to support when you've completed all troubleshooting steps and: - -- Provider logs show successful transfers but calls consistently fail -- SIP packet analysis indicates Vapi-side transfer issues -- Multiple destinations fail with the same configuration -- Configuration changes don't resolve recurring failures +Contact Support when the tool was invoked with a supported configuration but the Vapi and provider records disagree, a transfer error has no clear cause, or the same failure persists across controlled calls. **Include in your support request:** -- Call ID and timestamp -- Complete troubleshooting results from this guide -- Telephony provider error codes and logs -- Screenshots of configuration settings +- Vapi call ID, timestamp, call type, and `endedReason` +- Transfer tool ID, mode, destination type, and a redacted destination +- Expected caller and operator outcome, plus what each party heard +- Relevant **Messages** and **Logs** entries, including the transfer tool call and fallback behavior +- `phoneCallProviderId`, provider call record, error code, and SIP trace or `artifact.pcapUrl` when available +- Whether `phoneCallProviderBypassEnabled` or `phone-call-control` delegates call control to your server ## Next steps -Now that you can debug call forwarding drops: - -- **Monitor call patterns:** Set up alerts for calls with unexpected `endedReason` values -- **Test systematically:** Verify transfers work across different destination types before production -- **Review SIP setup:** Ensure your SIP configuration follows our [advanced SIP guide](https://docs.vapi.ai/advanced/sip/sip-trunk) +Test the chosen transfer mode with a reachable operator, a busy destination, and an unanswered call before using it for production escalation. For SIP routing, compare the result with your [SIP trunk configuration](/advanced/sip/sip-trunk). diff --git a/fern/tools/transfer-call.mdx b/fern/tools/transfer-call.mdx index fe59b9d52..f29961ce5 100644 --- a/fern/tools/transfer-call.mdx +++ b/fern/tools/transfer-call.mdx @@ -193,11 +193,37 @@ The transfer mode controls what happens on the destination call leg before the c Use [Dynamic call transfers](/calls/call-dynamic-transfers) when the destination is selected at runtime. Dynamic destination selection is separate from the transfer mode. +## Understand transfer outcomes and fallback + +The `assistant-forwarded-call` ended reason means Vapi initiated a transfer or delegated call control to your server. By itself, it does not confirm that the destination answered or that the caller and destination spoke. Check the provider's call record to confirm the downstream result. + +| Transfer mode | Caller state before connection | Recovery behavior | +| --- | --- | --- | +| `blind-transfer` | The transfer starts without an operator introduction. | The transfer call tool does not wait for operator acceptance. A busy line, no answer, or voicemail does not by itself return the caller to the original assistant. | +| `blind-transfer-add-summary-to-sip-header` | The transfer starts with a generated summary in the `X-Transfer-Summary` SIP header. | The summary is not spoken. The transfer call tool does not wait for operator acceptance. | +| `warm-transfer-say-message` | The destination hears a fixed message before the caller is connected. | This mode does not run an assistant that can accept or cancel the transfer. | +| `warm-transfer-say-summary` | The destination hears a generated summary before the caller is connected. | This mode does not run an assistant that can accept or cancel the transfer. | +| `warm-transfer-twiml` | The destination call leg executes the configured TwiML before the caller is connected. | The TwiML controls the introduction; this mode does not run a transfer assistant. | +| `warm-transfer-wait-for-operator-to-speak-first-and-then-say-message` | The destination speaks before hearing the fixed message. | `transferPlan.timeout` limits the wait for destination speech. It is not a failed-transfer fallback. | +| `warm-transfer-wait-for-operator-to-speak-first-and-then-say-summary` | The destination speaks before hearing the generated summary. | `transferPlan.timeout` limits the wait for destination speech. It is not a failed-transfer fallback. | +| `warm-transfer-experimental` | The caller waits on hold while the destination call leg runs. | The transfer assistant can connect or cancel. A failed attempt can play the configured fallback message, then continue or end the caller's call. See [Assistant-based warm transfer](/calls/assistant-based-warm-transfer). | + +The non-assistant warm modes require supported Twilio telephony for the destination introduction. Test the selected mode with the same phone provider and destination used in production. For a transfer that requires an operator to accept the caller, configure [assistant-based warm transfer](/calls/assistant-based-warm-transfer) and an explicit `fallbackPlan`. + +## Choose an escalation pattern + +| Need | Configure | Expected result and recovery | +| --- | --- | --- | +| Escalate when the caller asks or the assistant reaches a capability boundary | Add a transfer destination and describe the trigger in the tool description and assistant prompt. Require caller agreement when appropriate. | The model must invoke `transferCall`; speech alone does not start a transfer. Use [call logs](/observability/logs/call-logs) to confirm invocation. | +| Route to a specialist | Give each stored destination a distinct description, or use a [lookup-based transfer](/calls/transfer-calls-with-api-lookup). | The selected destination receives the transfer attempt. Confirm the selected route in the tool call and provider record. | +| Give an operator context before connecting | Use a supported [warm transfer mode](/tools/transfer-call/warm-transfer) with a message or summary. | The destination hears the introduction before connection. These modes do not ask an assistant to confirm acceptance. | +| Require a person to accept a high-stakes escalation | Use [assistant-based warm transfer](/calls/assistant-based-warm-transfer). Prompt the transfer assistant to call `transferSuccessful` only after acceptance and `transferCancel` when it should decline. Set `fallbackPlan.endCallEnabled` explicitly. | The caller waits on hold during the operator conversation. A canceled attempt or handled operator-leg failure follows the configured fallback; test busy, no-answer, and voicemail outcomes with the production provider. | + ## Verify the transfer Place a test call and ask for the destination described in the tool. Confirm that the call log contains a `transferCall` tool invocation and that the destination rings or answers. -The `assistant-forwarded-call` ended reason confirms that Vapi initiated the transfer. It does not confirm that the downstream telephony provider completed it, so check the provider's call logs when the destination does not receive the call. +The `assistant-forwarded-call` ended reason confirms that Vapi initiated or delegated the transfer. It does not confirm that the downstream telephony provider completed it, so check the provider's call logs when the destination does not receive the call. ## Troubleshooting diff --git a/fern/tools/transfer-call/blind-transfer.mdx b/fern/tools/transfer-call/blind-transfer.mdx index d5e03d87a..05addd0af 100644 --- a/fern/tools/transfer-call/blind-transfer.mdx +++ b/fern/tools/transfer-call/blind-transfer.mdx @@ -20,6 +20,8 @@ Both blind modes transfer the caller immediately. The summary variant adds conte Blind transfer is the default. If `transferPlan` is omitted, Vapi uses `blind-transfer`. +Neither blind mode waits for an operator to accept the call. On a Vapi-controlled transfer, `assistant-forwarded-call` confirms that Vapi started the provider transfer, not that the destination answered. A busy line, no answer, or voicemail does not automatically return the caller to the original assistant through the transfer call tool. If your organization has SIP cold-transfer fallback enabled, its availability and recovery path depend on the transfer outcome; test that path before relying on it. See [Understand transfer outcomes and fallback](/tools/transfer-call#understand-transfer-outcomes-and-fallback). + ## Configure blind transfer Update the transfer plan on each destination that should use blind transfer. From 4c408438574b7f3fc90d7dbe72f39e14f33dfbd1 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Tue, 20 Oct 2026 10:48:33 -0700 Subject: [PATCH 4/4] Updated call transfer, recovery and troubleshooting pages --- fern/calls/assistant-based-warm-transfer.mdx | 20 +-- fern/calls/call-ended-reason.mdx | 8 +- fern/calls/troubleshoot-call-errors.mdx | 4 +- .../troubleshoot-call-forwarding-drops.mdx | 167 ++++++++++++++---- fern/docs.yml | 3 +- fern/tools/transfer-call.mdx | 30 ++-- fern/tools/transfer-call/blind-transfer.mdx | 8 +- fern/tools/transfer-call/warm-transfer.mdx | 4 +- 8 files changed, 173 insertions(+), 71 deletions(-) diff --git a/fern/calls/assistant-based-warm-transfer.mdx b/fern/calls/assistant-based-warm-transfer.mdx index 6142b9092..19abf9e19 100644 --- a/fern/calls/assistant-based-warm-transfer.mdx +++ b/fern/calls/assistant-based-warm-transfer.mdx @@ -13,13 +13,11 @@ Use this mode when the destination must accept the call or receive context befor GPT-Live assistants cannot use assistant-based warm transfer. GPT-Live supports only blind transfers, and only on Twilio and Vapi SIP calls. See [GPT-Live compatibility](/gpt-live/configuration#compatibility) for the full list of supported and unsupported features. -Assistant-based warm transfer works on Twilio phone numbers when the destination is a phone number, and on Vapi numbers and bring-your-own SIP trunks for phone-number and SIP destinations. On a Twilio call, a SIP destination is transferred without the transfer assistant. - ## Prerequisites Before you configure the transfer, prepare: -- A Vapi assistant that handles phone calls +- A Vapi assistant that handles phone calls on a Twilio number, a Vapi number, or a bring-your-own SIP trunk. On other phone providers, the call is transferred without the transfer assistant. - A destination phone number in E.164 format, for example, `+14155550100` - A [private Vapi API key](/security-and-privacy/api-keys) for the curl method - A publicly accessible MP3 or WAV file if you want custom hold or completion audio @@ -161,7 +159,7 @@ Use these fields to control the transfer assistant and the two call legs. See th | `transferAssistant.model` | Model and instructions used for the operator conversation | Required when you configure `transferAssistant` | | `holdAudioUrl` | MP3 or WAV audio played to the customer while on hold | Default hold audio | | `transferCompleteAudioUrl` | MP3 or WAV audio played to the destination after the introduction | No custom completion audio | -| `fallbackPlan` | Customer message and end-call behavior after a failed warm transfer | If omitted, "Could not transfer the call." plays and the original assistant continues | +| `fallbackPlan` | Customer message and end-call behavior after a failed warm transfer | If omitted, the original assistant says "Could not transfer the call." and continues the conversation | | `summaryPlan` | Operator summary content and generation timeout | Configure with `enabled`, `messages`, and `timeoutSeconds` | | `contextEngineeringPlan` | Conversation context provided to the transfer assistant | All previous messages | @@ -172,7 +170,7 @@ Use these fields to control the transfer assistant and the two call legs. See th Keep the transfer assistant's system message focused on the operator conversation. Define when to use each built-in tool: - Call `transferSuccessful` after a human operator confirms that they will take the call -- Call `transferCancel` when the assistant hears voicemail, an interactive voice response (IVR) system, an automated hold queue, or an operator declining the call +- Call `transferCancel` when it hears voicemail, an interactive voice response (IVR) system, an automated hold queue, or an operator declining the call - Answer questions about the transfer or give a brief customer summary - Avoid unrelated conversation and complete the decision before `maxDurationSeconds` @@ -184,9 +182,11 @@ Call transferCancel if the destination plays an IVR message, automated queue ann Busy and no-answer call statuses can trigger rollback before the transfer assistant speaks. The transfer assistant receives the previous conversation by default. Set `contextEngineeringPlan.type` to `none` when the operator must not receive that context, or use `lastNMessages` to limit it. -When you configure the transfer through the API, set `fallbackPlan.endCallEnabled`. `false` plays the fallback message and returns the caller to the original assistant. `true` plays the message and ends the call. If omitted, the caller stays on the call with the original assistant. +When you configure the transfer through the API, set `fallbackPlan.endCallEnabled`. With `false`, the original assistant says the fallback message and continues the conversation. With `true`, it says the message and ends the call. If omitted, the caller stays on the call with the original assistant. -Keep `transferPlan.transferAssistant` configured when you depend on no-answer fallback or a transfer timeout. In the current Dashboard flow, disabling Summary generation can remove that configuration and prevent a no-answer event from starting the fallback. Keep Summary generation enabled or configure `transferAssistant` through the API, then verify the result with a controlled call. + + Keep `transferPlan.transferAssistant` configured when you depend on no-answer fallback or a transfer timeout. In the Dashboard, turning off **Summary generation** can remove that configuration and prevent a no-answer event from starting the fallback. Keep **Summary generation** on or configure `transferAssistant` through the API, then verify the result with a controlled call. + ## Verify the warm transfer @@ -207,7 +207,7 @@ In [**Logs → Calls**](/observability/logs/call-logs), inspect the original cal | Symptom | Likely cause | Resolution | | --- | --- | --- | -| The customer is transferred without an operator conversation | The destination uses a blind or non-assistant warm-transfer mode | Set `transferPlan.mode` to `warm-transfer-experimental`. | +| The customer is transferred without an operator conversation | The destination uses a blind or non-assistant warm-transfer mode, the call uses an unsupported phone provider, or a Twilio call is transferring to a SIP address | Set `transferPlan.mode` to `warm-transfer-experimental`, and use a Twilio number, Vapi number, or bring-your-own SIP trunk. On Twilio, use a phone-number destination. | | The transfer assistant never speaks | `firstMessage` is omitted or `firstMessageMode` waits for the operator | Set `firstMessage` and use `assistant-speaks-first`, or let the operator speak first. | | The transfer assistant talks but never connects the parties | The prompt does not require `transferSuccessful` | Tell the transfer assistant to call `transferSuccessful` immediately after the operator accepts. | | Voicemail, an IVR, a hold queue, or a declined transfer connects anyway | The prompt does not define cancellation conditions | Require `transferCancel` for non-human destinations and declined transfers. Busy and no-answer call statuses can trigger rollback before the transfer assistant speaks. | @@ -216,7 +216,7 @@ In [**Logs → Calls**](/observability/logs/call-logs), inspect the original cal | The customer cannot continue after a failed transfer | `fallbackPlan.endCallEnabled` is `true` | Set it to `false` so the original assistant remains on the call. | | The transfer ends during a long operator interaction | A duration or silence timeout is too short | Increase `maxDurationSeconds` or `silenceTimeoutSeconds` within the supported ranges. | -Check the affected call's **Logs** tab in [**Logs → Calls**](/observability/logs/call-logs) for validation and transfer errors. If Vapi initiated the transfer but the destination never rings, inspect the telephony provider's call detail records and follow [Troubleshoot call forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). +Check the affected call's **Logs** tab in [**Logs → Calls**](/observability/logs/call-logs) for validation and transfer errors. If Vapi initiated the transfer but the destination never rings, inspect the telephony provider's call detail records and follow [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). ## API reference @@ -234,7 +234,7 @@ The [Create Tool API reference](/api-reference/tools/create) documents transfer Choose a transfer destination at runtime. - + Diagnose failed or incomplete transfers. diff --git a/fern/calls/call-ended-reason.mdx b/fern/calls/call-ended-reason.mdx index cc5f49608..c6f1ddd40 100644 --- a/fern/calls/call-ended-reason.mdx +++ b/fern/calls/call-ended-reason.mdx @@ -88,7 +88,7 @@ These indicate the assistant intentionally ended the call — not errors. - `assistant-ended-call-after-message-spoken` — The assistant ended the call after speaking its final message. - `assistant-ended-call-with-hangup-task` — The assistant ended the call using a hangup task. - `assistant-said-end-call-phrase` — The assistant said a phrase configured to trigger call termination. -- `assistant-forwarded-call` — The assistant transferred the call to another number or service. +- `assistant-forwarded-call` — The assistant started a transfer to another number or service. This reason doesn't confirm that the destination answered. - `assistant-join-timed-out` — The assistant failed to join the call within the expected timeframe. ## Customer actions @@ -160,7 +160,7 @@ Common transcriber error patterns: - `call.in-progress.error-warm-transfer-silence-timeout` — Silence timeout during a warm transfer. - `call.in-progress.error-warm-transfer-microphone-timeout` — Microphone timeout during a warm transfer. -For step-by-step transfer debugging, see [Debug forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). +For step-by-step transfer debugging, see [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). ## Transport and connectivity @@ -208,8 +208,8 @@ For SIP trunk setup issues, see [Troubleshoot SIP trunk credential errors](/adva - `call.ringing.hook-executed-transfer` — A transfer hook executed during ringing ended the call. - `call.ending.hook-executed-say` — A say hook executed during the ending phase. - `call.ending.hook-executed-transfer` — A transfer hook executed during the ending phase. -- `call.forwarding.operator-busy` — The destination was busy, or an assistant-based warm transfer failed and the fallback ended the call. -- `call.forwarding.no-answer` — A monitored SIP transfer destination did not answer. +- `call.forwarding.operator-busy` — The destination of a blind transfer was busy, or an assistant-based warm transfer failed and the fallback ended the call. For blind transfers, this reason appears only when transfer outcome detection is enabled. +- `call.forwarding.no-answer` — The destination of a blind transfer didn't answer. This reason appears only when transfer outcome detection is enabled. ## Other reasons diff --git a/fern/calls/troubleshoot-call-errors.mdx b/fern/calls/troubleshoot-call-errors.mdx index a3918bea6..722a2deef 100644 --- a/fern/calls/troubleshoot-call-errors.mdx +++ b/fern/calls/troubleshoot-call-errors.mdx @@ -247,7 +247,7 @@ If a voice error ends the call, follow [Troubleshoot voice fallback](/voice-fall For response-class guidance and checks across Vapi, your SIP provider, and your SIP infrastructure, see [Troubleshoot SIP response codes](/advanced/sip/troubleshoot-sip-response-codes). -For a detailed transfer debugging walkthrough, see [Debug forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). +For a detailed transfer debugging walkthrough, see [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). ## Call ended normally @@ -271,7 +271,7 @@ These are not errors — they indicate the call ended as expected. - **[Call end reasons](/calls/call-ended-reason):** Complete reference of every `endedReason` code. - **[Debugging voice agents](/debugging):** General debugging workflow using dashboard tools, logs, Simulations, and Evals. -- **[Debug forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops):** Deep dive into transfer failures. +- **[Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops):** Deep dive into transfer failures. - **[Troubleshoot SIP trunk errors](/advanced/sip/troubleshoot-sip-trunk-credential-errors):** Resolve SIP credential validation failures. - **[Troubleshoot SIP response codes](/advanced/sip/troubleshoot-sip-response-codes):** Identify where a SIP request failed and what to check next. - **[How to report issues](/issue-reporting):** Include your `call_id` and account email when contacting support. diff --git a/fern/calls/troubleshoot-call-forwarding-drops.mdx b/fern/calls/troubleshoot-call-forwarding-drops.mdx index 94b7a2fed..b6d062b23 100644 --- a/fern/calls/troubleshoot-call-forwarding-drops.mdx +++ b/fern/calls/troubleshoot-call-forwarding-drops.mdx @@ -1,31 +1,31 @@ --- -title: "Debug call forwarding drops" +title: "Troubleshoot call transfers" subtitle: "Trace transfer attempts, failed connections, and caller recovery" -description: Diagnose transfer tool invocation, provider routing, busy and unanswered destinations, fallback behavior, and the evidence to send Support. +description: Diagnose and troubleshoot transfer tool invocation, provider routing, busy and unanswered destinations, fallback behavior, and the evidence to send support. --- -An assistant saying it will transfer, Vapi starting a transfer, and the destination answering are separate events. Use the call record and provider logs to find where the attempt stopped. +Use this page when a call transfer fails or doesn't behave as expected. A transfer has three separate stages: the assistant decides to transfer, Vapi starts the transfer, and the destination answers. Each stage leaves different evidence, so check both the Vapi call record and your phone provider's call logs to find the stage where the attempt stopped. ## Quick diagnosis -| What you observe | Check next | Likely owner | +| What you observe | Check next | Likely cause | | --- | --- | --- | | The assistant says it will transfer, but the call continues | Look for a `transferCall` invocation in the call's **Messages** tab | Assistant prompt or model decision | | The `transferCall` tool runs, but the transfer does not start | Inspect the call's **Logs** tab for a Tool or Transfer error | Tool configuration or Vapi transfer initiation | | The call ends with `assistant-forwarded-call`, but the destination does not answer | Match `phoneCallProviderId` to the provider's call record | Provider route, destination, or delegated call control | | A warm transfer returns to the original assistant or ends after a fallback message | Check `transferPlan.mode`, `fallbackPlan.endCallEnabled`, and the transfer attempt | Configured fallback or transfer-assistant decision | -`assistant-forwarded-call` means Vapi started or handed off the transfer. It does not prove that the destination rang, answered, or spoke with the caller. +The `assistant-forwarded-call` ended reason means Vapi started or handed off the transfer. It does not prove that the destination rang, answered, or spoke with the caller. ## Prerequisites Before you start debugging, ensure you have: -- **Call ID** from the dropped call -- **API access** to make GET requests to Vapi -- **Access** to the call's **Messages** and **Logs** tabs, when logs were retained -- **Access** to the phone provider's call record, if the transfer reached that provider -- **A SIP trace or packet capture** when debugging a SIP transfer and one is available +- The call ID of the failed transfer +- A [private Vapi API key](/security-and-privacy/api-keys) to retrieve the call through the API +- Access to the call's **Messages** and **Logs** tabs, if logs were retained +- Access to your phone provider's call record, if the transfer reached that provider +- A SIP trace or packet capture, if you're debugging a SIP transfer ## Troubleshooting workflow @@ -60,18 +60,52 @@ print(f"End reason: {call.ended_reason}") +```json title="Example response (truncated)" +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "type": "inboundPhoneCall", + "status": "ended", + "endedReason": "assistant-forwarded-call", + "phoneCallProvider": "twilio", + "phoneCallTransport": "pstn", + "phoneCallProviderId": "CA0123456789abcdef0123456789abcdef", + "destination": { + "type": "number", + "number": "+14155550100" + } +} +``` + +For every field in the call object, see the [Get Call API reference](/api-reference/calls/get). + **Interpret the result:** -- No `transferCall` tool call: check the assistant prompt and tool assignment. The model did not invoke the transfer tool. -- A `transferCall` tool call followed by a Tool or Transfer error: inspect the destination, transfer plan, and [call logs](/observability/logs/call-logs#logs). +- No `transferCall` tool call: Check the assistant prompt and tool assignment. The model did not invoke the transfer tool. +- A `transferCall` tool call followed by a Tool or Transfer error: Inspect the destination, transfer plan, and [call logs](/observability/logs/call-logs#logs). - `assistant-forwarded-call`: Vapi submitted the transfer to the phone provider or sent a `phone-call-control` request to your server. Check the downstream result before treating it as a connected call. -- `call.in-progress.error-transfer-failed`, `call.forwarding.operator-busy`, or `call.forwarding.no-answer`: inspect the transfer attempt and provider result. These reasons do not all mean the assistant failed to invoke the tool. +- `call.in-progress.error-transfer-failed`, `call.forwarding.operator-busy`, or `call.forwarding.no-answer`: Inspect the transfer attempt and provider result. These reasons do not all mean the assistant failed to invoke the tool. -When `phoneCallProviderBypassEnabled` is `true`, or the assistant includes `phone-call-control` in `serverMessages`, Vapi sends a `phone-call-control` request to your server instead of directly transferring the provider call. Check your server's response and provider-side action before changing either setting. +When the assistant includes `phone-call-control` in `serverMessages`, Vapi sends a `phone-call-control` request to your server instead of transferring the call itself. Your server then decides whether and how to transfer. Check your server's response and your phone provider's call record before changing this setting. + + +```json title="Vapi transfers the call" +{ + "serverMessages": ["status-update", "end-of-call-report", "tool-calls"] +} +``` + +```json title="Your server controls the transfer" +{ + "serverMessages": ["status-update", "end-of-call-report", "tool-calls", "phone-call-control"] +} +``` + + +Remove `phone-call-control` unless your server handles transfers. Setting `serverMessages` replaces the default list, so include every message type your server needs. See [server events](/server-url/events) for the full list. Use [Webhook logs](/observability/logs/webhook-logs) to inspect a recorded server-message delivery. If your application intentionally controls the provider call, its logs and provider call record are the source of the downstream outcome. @@ -79,7 +113,13 @@ Use [Webhook logs](/observability/logs/webhook-logs) to inspect a recorded serve -The built-in phone and SIP transfer path requires a supported phone transport. A web call's WebSocket transport rejects both phone-number and SIP transfer destinations. A phone call can transfer to a phone number or SIP URI when its provider supports the configured route. +Confirm that the call type supports the destination type: + +| Call type | To a phone number | To a SIP address | +| --- | --- | --- | +| Phone call on Twilio, Telnyx, a Vapi number, or a SIP trunk | Yes | Yes | +| Phone call on Vonage | Yes | No | +| Web call | No | No | For a SIP destination, check the inbound provider, `transferPlan.sipVerb`, and the receiving private branch exchange (PBX). For a phone-number destination, verify E.164 format and call the number directly to confirm it is reachable. @@ -89,7 +129,28 @@ For a SIP destination, check the inbound provider, `transferPlan.sipVerb`, and t Use the call's `phoneCallProviderId` and timestamp to find the matching record in your phone provider's logs. Check whether the provider received the transfer command, dialed the destination, and recorded an answer, busy signal, no answer, rejection, or other failure. -A successful API command to the provider is not proof that the downstream call connected. Compare the provider record with the Vapi call's `endedReason`, [Messages](/observability/logs/call-logs#messages), and [Logs](/observability/logs/call-logs#logs). A missing provider record may indicate a Vapi initiation or configuration problem; a provider error identifies a downstream failure to investigate with that provider. + + + 1. Open [Twilio Console call logs](https://console.twilio.com/us1/monitor/logs/calls). + 2. Search for the call SID from `phoneCallProviderId`. It starts with `CA`. + 3. Check the call's status, its child calls, and any errors or warnings. + + + 1. Sign in to the [Vonage API Dashboard](https://dashboard.nexmo.com/). + 2. Find the call by the conversation UUID from `phoneCallProviderId` and the call's timestamp. + 3. Check the call's status and any transfer errors. + + + 1. Sign in to the [Telnyx Mission Control Portal](https://portal.telnyx.com/). + 2. Find the call's detail record by the call control ID from `phoneCallProviderId`. + 3. Check the call's status and any transfer errors. + + + There's no separate provider console for a Vapi number. Use the call's [Logs](/observability/logs/call-logs#logs) tab and, if available, the packet capture in `artifact.pcapUrl` (see the next step). For a bring-your-own SIP trunk, also check your SIP provider's call records. + + + +A successful API command to the provider is not proof that the downstream call connected. Compare the provider record with the Vapi call's `endedReason`, [Messages](/observability/logs/call-logs#messages), and [Logs](/observability/logs/call-logs#logs). A missing provider record can point to a problem starting the transfer or a configuration problem. A provider error identifies a downstream failure to investigate with that provider. @@ -97,14 +158,9 @@ A successful API command to the provider is not proof that the downstream call c For SIP trunking or bring-your-own-number setups, analyze the SIP signaling. -**Check whether a packet capture is available:** +**Check whether a packet capture is available:** Look for `artifact.pcapUrl` in the call record you retrieved in the first step. -```bash title="Get the call record" -curl -X GET "https://api.vapi.ai/call/{call_id}" \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -For an eligible Vapi or bring-your-own SIP phone call, the completed call can contain `artifact.pcapUrl` when packet capture was enabled and retained. If present, download that URL and open the capture in a SIP analysis tool. A missing URL does not prove that no transfer command was sent. +For an eligible Vapi or bring-your-own SIP phone call, the completed call can contain `artifact.pcapUrl` when packet capture was enabled and retained. If present, download that URL and open the capture in a SIP analysis tool. For example, Wireshark supports SIP. A missing URL does not prove that no transfer command was sent. **Filter for transfer packets:** @@ -115,12 +171,15 @@ sip.Method == "REFER" **What to look for:** - **REFER request present:** The transfer request reached the SIP signaling path captured in this file. -- **REFER response present:** Check the response code and any later NOTIFY or referred-call status. Acceptance of a REFER request alone does not prove that the destination answered. -- **No REFER request:** Check whether the transfer used `sipVerb: "dial"` or `sipVerb: "bye"`, whether capture started before the transfer, and whether the tool was invoked. Vapi phone numbers always dial the destination, so look for the dial leg instead. +- **202 Accepted response:** The SIP provider accepted the transfer request. Check any later NOTIFY or referred-call status, because acceptance alone doesn't prove that the destination answered. +- **Error response (4xx or 5xx):** The SIP provider rejected the transfer request. +- **No REFER request:** Check whether the transfer used `sipVerb: "dial"` or `sipVerb: "bye"`, whether capture started before the transfer, and whether the tool was invoked. Vapi numbers always dial the destination, so look for the dial leg instead. -With `sipVerb: "dial"`, inspect the outbound dial leg instead of looking for a REFER. With `sipVerb: "bye"`, Vapi ends its leg without dialing the destination. + + With `sipVerb: "refer"`, your SIP provider completes the transfer after it accepts the REFER request, so its logs show the outcome. With `sipVerb: "dial"`, Vapi calls the destination itself, so inspect the outbound dial leg instead of looking for a REFER. Vapi numbers always transfer this way. With `sipVerb: "bye"`, Vapi ends its leg without dialing the destination. + -For more details on SIP configuration, see our [SIP trunk documentation](https://docs.vapi.ai/advanced/sip/sip-trunk#inbound-call-test). +For SIP configuration details, see the [SIP trunk inbound call test](/advanced/sip/sip-trunk#inbound-call-test). @@ -131,17 +190,54 @@ Recovery depends on the transfer mode. Check `transferPlan.mode` before changing | Mode or path | What to check | Caller recovery | | --- | --- | --- | -| Blind transfer or non-assistant warm transfer | Provider call record for busy, no answer, rejection, voicemail, or a failed destination leg | On Twilio, Vapi hands the call to Twilio and ends with `assistant-forwarded-call`, even if the destination is busy or doesn't answer. The caller's call ends, and voicemail counts as answered. On Vapi numbers and SIP trunks, busy and no-answer can have their own ended reasons when outcome detection is enabled. Otherwise the call ends with `assistant-forwarded-call`. | -| `warm-transfer-experimental` | Operator-leg status, `transferAssistant` configuration, transfer-assistant tool call, and timeout | With `transferAssistant` configured, busy and no-answer statuses roll back. `transferCancel` can roll back a declined call or detected voicemail. The configured fallback message plays after rollback. | -| SIP cold transfer with organization-enabled fallback | Transfer outcome, `fallbackPlan`, and provider record | Recovery is conditional on organization enablement and the failure path. Confirm it with a controlled call before using it for a critical escalation. | +| Blind transfer or non-assistant warm transfer | Provider call record for busy, no answer, rejection, voicemail, or a failed destination leg | **Twilio:** Vapi hands the call to Twilio and ends with `assistant-forwarded-call`, even if the destination is busy or doesn't answer. The caller's call ends, and voicemail counts as answered.

**Vapi numbers and SIP trunks:** Busy and no-answer can have their own ended reasons when transfer outcome detection is enabled. Otherwise the call ends with `assistant-forwarded-call`. | +| `warm-transfer-experimental` | Operator-leg status, `transferAssistant` configuration, transfer-assistant tool call, and timeout | With `transferAssistant` configured, busy and no-answer statuses roll back. `transferCancel` can roll back a declined call or detected voicemail. After rollback, the original assistant says the configured fallback message. | +| Blind SIP transfer with SIP transfer fallback | Transfer outcome, `fallbackPlan`, and provider record | After a busy, unanswered, or failed transfer, the original assistant says the fallback message, then continues or ends the call. | + +For assistant-based warm transfer, set `fallbackPlan.endCallEnabled` explicitly. Set it to `false` to keep the original assistant on the call after the fallback message, or to `true` to end the call after the message. + +When the destination is voicemail or an automated queue, the transfer assistant must cancel the transfer, or configured voicemail detection must identify the non-human destination. Do not assume that a standard message, summary, or TwiML introduction detects voicemail. + +Read the call's messages and logs with the ended reason rather than using that field alone to decide where the failure occurred. + +A failed transfer command can end with `call.in-progress.error-transfer-failed`. On a blind SIP transfer with SIP transfer fallback enabled, a configured `fallbackPlan` can also handle a recoverable transfer-request failure while the caller is still connected to Vapi. It cannot recover a call that has already ended. + +An operator-leg busy or no-answer outcome can produce a different reason or return the caller to the original assistant, depending on the mode and recovery configuration. + +## Common solutions + +### The transfer started but the destination never answered + +- Use E.164 format with the country code, for example `+14155550100`. +- Call the destination directly from another phone to confirm it's reachable. +- Check whether the destination blocks or screens incoming calls, for example with call blocking or spam filtering. +- Find the call in your phone provider's logs to see why it didn't connect. + +### The assistant didn't transfer the call + +- Confirm the transfer tool is attached to the assistant and has a destination. +- Describe when to transfer in both the tool description and the assistant prompt. The assistant saying it will transfer doesn't start a transfer. +- Check that `phone-call-control` isn't in `serverMessages` unless your server handles transfers. +- Confirm the call is a phone call. Web calls can't transfer to a phone number or SIP address. + +### The packet capture has no REFER request + +- Check `transferPlan.sipVerb`. With `dial`, look for the outbound call to the destination instead. Vapi numbers always use `dial`. +- Confirm a SIP destination is a full SIP URI, for example `sip:support@example.com`. +- Check that `phone-call-control` isn't sending the transfer to your server. -For assistant-based warm transfer, set `fallbackPlan.endCallEnabled` explicitly. `false` keeps the original assistant on the call after the fallback message; `true` ends the call after the message. Voicemail or an automated queue requires the transfer assistant to cancel, or configured voicemail detection to identify a non-human destination. Do not assume that a standard message, summary, or TwiML introduction detects voicemail. +## Limitations -A failed transfer command can end with `call.in-progress.error-transfer-failed`. On an organization-enabled SIP cold transfer, a configured `fallbackPlan` can also handle a recoverable transfer-request failure while the caller is still connected to Vapi. It cannot recover a call that has already ended. An operator-leg busy or no-answer outcome can produce a different reason or return the caller to the original assistant, depending on the mode and recovery configuration. Read the call's messages and logs with the ended reason rather than using that field alone to decide where the failure occurred. +- **Web calls** can't transfer to a phone number or SIP address. The call ends with `call.in-progress.error-transfer-failed`. +- **Vonage calls** can't transfer to a SIP address. +- **Warm modes that speak a message or summary, or run TwiML,** work only on Twilio. Other providers transfer the call without the introduction. +- **Assistant-based warm transfer on Twilio** needs a phone-number destination. A SIP destination is transferred without the transfer assistant. +- **Transfer outcome detection and SIP transfer fallback** are available on Vapi numbers and SIP trunks. +- **Your server completes transfers** when `serverMessages` includes `phone-call-control`. Vapi sends transfers to your server instead of transferring the call itself. ## When to contact support -Contact Support when the tool was invoked with a supported configuration but the Vapi and provider records disagree, a transfer error has no clear cause, or the same failure persists across controlled calls. +Contact support when the tool was invoked with a supported configuration but the Vapi and provider records disagree, a transfer error has no clear cause, or the same failure persists across controlled calls. **Include in your support request:** @@ -150,11 +246,12 @@ Contact Support when the tool was invoked with a supported configuration but the - Expected caller and operator outcome, plus what each party heard - Relevant **Messages** and **Logs** entries, including the transfer tool call and fallback behavior - `phoneCallProviderId`, provider call record, error code, and SIP trace or `artifact.pcapUrl` when available -- Whether `phoneCallProviderBypassEnabled` or `phone-call-control` delegates call control to your server +- Whether `phone-call-control` in `serverMessages` sends transfers to your server +- Screenshots of the transfer tool and transfer plan settings in the Dashboard ## Next steps -Test the chosen transfer mode with a reachable operator, a busy destination, and an unanswered call before using it for production escalation. For SIP routing, compare the result with your [SIP trunk configuration](/advanced/sip/sip-trunk). +Test the chosen transfer mode with a reachable operator, a busy destination, and an unanswered call before using it for production escalation. For SIP routing, compare the result with your [SIP trunk configuration](/advanced/sip/sip-trunk). To catch transfer failures early, set up [monitoring](/observability/monitoring-quickstart) with an Insight that tracks transfer-related ended reasons, such as `call.in-progress.error-transfer-failed` and `call.forwarding.no-answer`. diff --git a/fern/docs.yml b/fern/docs.yml index a63911bab..c6df19bc6 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -515,7 +515,8 @@ navigation: path: calls/transfer-calls-with-api-lookup.mdx - page: On-hold specialist transfer path: calls/call-handling-with-vapi-and-twilio.mdx - - page: Debug forwarding drops + - page: Troubleshoot call transfers + slug: debug-forwarding-drops path: calls/troubleshoot-call-forwarding-drops.mdx icon: fa-light fa-bug - page: Call queue management diff --git a/fern/tools/transfer-call.mdx b/fern/tools/transfer-call.mdx index f29961ce5..ce1b41b7d 100644 --- a/fern/tools/transfer-call.mdx +++ b/fern/tools/transfer-call.mdx @@ -183,13 +183,13 @@ The transfer mode controls what happens on the destination call leg before the c | Dashboard mode | API value | What it does | | --- | --- | --- | | **Blind Transfer** | `blind-transfer` | Connects the customer without speaking to the destination. This is the default. See [Blind transfer](/tools/transfer-call/blind-transfer). | -| **Blind Transfer with Summary in SIP Header** | `blind-transfer-add-summary-to-sip-header` | Adds a generated summary to the `X-Transfer-Summary` SIP header, then transfers immediately. See [Blind transfer](/tools/transfer-call/blind-transfer). | -| **Warm Transfer with Message** | `warm-transfer-say-message` | Speaks a fixed message to the destination before connecting the customer. See [Warm transfer](/tools/transfer-call/warm-transfer). | -| **Warm Transfer with Summary** | `warm-transfer-say-summary` | Speaks a generated conversation summary to the destination before connecting. See [Warm transfer](/tools/transfer-call/warm-transfer). | -| **Warm Transfer with TwiML** | `warm-transfer-twiml` | Executes TwiML on the destination call leg before connecting. See [Warm transfer](/tools/transfer-call/warm-transfer). | -| **Warm Transfer - Wait for Operator, Then Say Message** | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-message` | Waits for destination speech, then delivers a fixed message. See [Warm transfer](/tools/transfer-call/warm-transfer). | -| **Warm Transfer - Wait for Operator, Then Say Summary** | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-summary` | Waits for destination speech, then delivers a generated summary. See [Warm transfer](/tools/transfer-call/warm-transfer). | -| **Warm Transfer - Experimental** | `warm-transfer-experimental` | Places the customer on hold and supports fallback, summaries, custom audio, or a transfer-assistant conversation. See [Assistant-based warm transfer](/calls/assistant-based-warm-transfer). | +| **Blind Transfer with Summary in SIP Header** | `blind-transfer-add-summary-to-sip-header` | Adds a generated summary to the `X-Transfer-Summary` SIP header, then transfers immediately. See [Blind transfer](/tools/transfer-call/blind-transfer). Vapi numbers and bring-your-own SIP trunks only. Other providers transfer without the header. | +| **Warm Transfer with Message** | `warm-transfer-say-message` | Speaks a fixed message to the destination before connecting the customer. See [Warm transfer](/tools/transfer-call/warm-transfer). Twilio only. Other providers transfer without the message. | +| **Warm Transfer with Summary** | `warm-transfer-say-summary` | Speaks a generated conversation summary to the destination before connecting. See [Warm transfer](/tools/transfer-call/warm-transfer). Twilio only. Other providers transfer without the summary. | +| **Warm Transfer with TwiML** | `warm-transfer-twiml` | Executes TwiML on the destination call leg before connecting. See [Warm transfer](/tools/transfer-call/warm-transfer). Twilio only. | +| **Warm Transfer - Wait for Operator, Then Say Message** | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-message` | Waits for destination speech, then delivers a fixed message. See [Warm transfer](/tools/transfer-call/warm-transfer). Twilio only. Other providers transfer without the message. | +| **Warm Transfer - Wait for Operator, Then Say Summary** | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-summary` | Waits for destination speech, then delivers a generated summary. See [Warm transfer](/tools/transfer-call/warm-transfer). Twilio only. Other providers transfer without the summary. | +| **Warm Transfer - Experimental** | `warm-transfer-experimental` | Places the customer on hold and supports fallback, summaries, custom audio, or a transfer-assistant conversation. See [Assistant-based warm transfer](/calls/assistant-based-warm-transfer). Works on Twilio (phone-number destinations), Vapi numbers, and bring-your-own SIP trunks. | Use [Dynamic call transfers](/calls/call-dynamic-transfers) when the destination is selected at runtime. Dynamic destination selection is separate from the transfer mode. @@ -197,20 +197,22 @@ Use [Dynamic call transfers](/calls/call-dynamic-transfers) when the destination The `assistant-forwarded-call` ended reason means Vapi initiated a transfer or delegated call control to your server. By itself, it does not confirm that the destination answered or that the caller and destination spoke. Check the provider's call record to confirm the downstream result. -| Transfer mode | Caller state before connection | Recovery behavior | +| Transfer mode | Caller state before connection | If the transfer fails | | --- | --- | --- | -| `blind-transfer` | The transfer starts without an operator introduction. | The transfer call tool does not wait for operator acceptance. A busy line, no answer, or voicemail does not by itself return the caller to the original assistant. | +| `blind-transfer` | The transfer starts without an operator introduction. | Doesn't wait for an answer. On Twilio, a busy or unanswered call ends. On Vapi numbers and bring-your-own SIP trunks, the caller can return to the assistant only if SIP transfer fallback is enabled and `fallbackPlan` is configured. | | `blind-transfer-add-summary-to-sip-header` | The transfer starts with a generated summary in the `X-Transfer-Summary` SIP header. | The summary is not spoken. The transfer call tool does not wait for operator acceptance. | | `warm-transfer-say-message` | The destination hears a fixed message before the caller is connected. | This mode does not run an assistant that can accept or cancel the transfer. | | `warm-transfer-say-summary` | The destination hears a generated summary before the caller is connected. | This mode does not run an assistant that can accept or cancel the transfer. | | `warm-transfer-twiml` | The destination call leg executes the configured TwiML before the caller is connected. | The TwiML controls the introduction; this mode does not run a transfer assistant. | | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-message` | The destination speaks before hearing the fixed message. | `transferPlan.timeout` limits the wait for destination speech. It is not a failed-transfer fallback. | | `warm-transfer-wait-for-operator-to-speak-first-and-then-say-summary` | The destination speaks before hearing the generated summary. | `transferPlan.timeout` limits the wait for destination speech. It is not a failed-transfer fallback. | -| `warm-transfer-experimental` | The caller waits on hold while the destination call leg runs. | The transfer assistant can connect or cancel. A failed attempt can play the configured fallback message, then continue or end the caller's call. See [Assistant-based warm transfer](/calls/assistant-based-warm-transfer). | +| `warm-transfer-experimental` | The caller waits on hold while the destination call leg runs. | The transfer assistant can connect or cancel. After a failed attempt, the original assistant says the fallback message, then continues or ends the call. See [Assistant-based warm transfer](/calls/assistant-based-warm-transfer). | -The non-assistant warm modes require supported Twilio telephony for the destination introduction. Test the selected mode with the same phone provider and destination used in production. For a transfer that requires an operator to accept the caller, configure [assistant-based warm transfer](/calls/assistant-based-warm-transfer) and an explicit `fallbackPlan`. +Test the selected mode with the same phone provider and destination used in production. For a transfer that requires an operator to accept the caller, configure [assistant-based warm transfer](/calls/assistant-based-warm-transfer) and an explicit `fallbackPlan`. -## Choose an escalation pattern +## Common transfer scenarios + +Each scenario pairs a common reason for handing a caller to a person with the destination, transfer mode, and fallback settings that fit it. The scenarios differ in how much control you keep. A blind transfer connects the caller fastest but can't bring them back if no one answers. Assistant-based warm transfer confirms that a person accepted before connecting the caller. Start from the scenario that matches why the caller needs a person, then check that your phone provider [supports the transfer mode](#choose-a-transfer-mode) it uses. | Need | Configure | Expected result and recovery | | --- | --- | --- | @@ -236,7 +238,7 @@ The `assistant-forwarded-call` ended reason confirms that Vapi initiated or dele | The assistant transfers when it should continue helping | The prompt permits broad invocation | Narrow the prompt conditions. For tools configured through the API, you can also add a [tool rejection plan](/tools/tool-rejection-plan). | | An assistant-to-assistant transfer stalls or behaves like an external call | The transfer call tool is being used for assistant routing | Use the [handoff tool](/squads/handoff) for transfers between standalone assistants or assistants in a Squad. | -For dropped calls, provider diagnostics, and SIP packet checks, follow [Troubleshoot call forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). The [call ended reasons reference](/calls/call-ended-reason) lists transfer-specific outcomes such as `call.in-progress.error-transfer-failed`. +For dropped calls, provider diagnostics, and SIP packet checks, follow [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). The [call ended reasons reference](/calls/call-ended-reason) lists transfer-specific outcomes such as `call.in-progress.error-transfer-failed`. ## API reference @@ -263,7 +265,7 @@ The [Create Tool API reference](/api-reference/tools/create) documents the publi Move the conversation to another assistant or Squad. - + Diagnose provider, SIP, and call-routing failures. diff --git a/fern/tools/transfer-call/blind-transfer.mdx b/fern/tools/transfer-call/blind-transfer.mdx index 05addd0af..730a4b7dc 100644 --- a/fern/tools/transfer-call/blind-transfer.mdx +++ b/fern/tools/transfer-call/blind-transfer.mdx @@ -20,7 +20,9 @@ Both blind modes transfer the caller immediately. The summary variant adds conte Blind transfer is the default. If `transferPlan` is omitted, Vapi uses `blind-transfer`. -Neither blind mode waits for an operator to accept the call. On a Vapi-controlled transfer, `assistant-forwarded-call` confirms that Vapi started the provider transfer, not that the destination answered. A busy line, no answer, or voicemail does not automatically return the caller to the original assistant through the transfer call tool. If your organization has SIP cold-transfer fallback enabled, its availability and recovery path depend on the transfer outcome; test that path before relying on it. See [Understand transfer outcomes and fallback](/tools/transfer-call#understand-transfer-outcomes-and-fallback). +Neither blind mode waits for an operator to accept the call. The `assistant-forwarded-call` ended reason means the transfer started, not that the destination answered. + +A busy line, no answer, or voicemail does not automatically return the caller to the original assistant through the transfer call tool. If SIP transfer fallback is enabled and the transfer has a `fallbackPlan`, the caller can return to the assistant after a busy, unanswered, or failed transfer. See [Understand transfer outcomes and fallback](/tools/transfer-call#understand-transfer-outcomes-and-fallback). ## Configure blind transfer @@ -146,7 +148,7 @@ An `assistant-forwarded-call` ended reason confirms that Vapi initiated the tran | The summary contains the wrong information | The summary prompt is broad or uses the wrong template variable | Give `summaryPlan.messages` a focused instruction and include `{{transcript}}` in the user message. | | Vapi shows `assistant-forwarded-call`, but the transfer failed | Vapi initiated the downstream transfer but did not observe its final result | Use the telephony provider's call detail record and SIP signaling as the source of truth for downstream completion. | -For additional provider diagnostics and packet capture guidance, follow [Troubleshoot call forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). +For additional provider diagnostics and packet capture guidance, follow [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). ## API reference @@ -164,7 +166,7 @@ The [Update Tool API reference](/api-reference/tools/update) documents the trans Let a transfer assistant speak with the destination. - + Diagnose incomplete transfers and SIP routing failures. diff --git a/fern/tools/transfer-call/warm-transfer.mdx b/fern/tools/transfer-call/warm-transfer.mdx index ba530473d..1f6148b1c 100644 --- a/fern/tools/transfer-call/warm-transfer.mdx +++ b/fern/tools/transfer-call/warm-transfer.mdx @@ -214,7 +214,7 @@ In [**Logs → Calls**](/observability/logs/call-logs), inspect the transfer his | The API rejects the TwiML | The value is invalid XML, too long, or uses unsupported verbs | Use valid XML under 4,096 characters and limit the instructions to the supported verbs. | | The TwiML behaves differently from the test | The destination interactive voice response system handles timing or input differently | Test the same TwiML against the production destination and simplify `Gather`, `Pause`, or audio timing. | -If the transfer is initiated but the destination never rings or the call drops, follow [Troubleshoot call forwarding drops](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). +If the transfer is initiated but the destination never rings or the call drops, follow [Troubleshoot call transfers](/phone-calling/in-call-control/transfer-calls/debug-forwarding-drops). ## API reference @@ -232,7 +232,7 @@ The [Update Tool API reference](/api-reference/tools/update) documents transfer- Let a transfer assistant converse with the destination. - + Diagnose incomplete transfers and provider routing failures.