---
title: "Transfer Destination Request"
method: POST
path: "/vapi/transfer-destination"
tags: ["Vapi"]
---

# Transfer Destination Request

`POST /vapi/transfer-destination`

Resolve and execute a Vapi dynamic transfer (the routeTransfer tool).

routeTransfer is a custom `function` tool carrying arguments, so it is
dispatched as a normal tool-calls message — NOT a transfer-destination-request
webhook. (Vapi only fires that webhook for a transferCall tool that supplies
*no* destination; a tool call carrying arguments is treated as a supplied
destination and Vapi would try to dial the argument as a phone number,
dropping the call. That is the bug this endpoint replaces.)

Flow (Vapi Live Call Control, see
https://docs.vapi.ai/calls/call-dynamic-transfers): resolve the
TransferDestination from the arguments, then POST the transfer to the call's
monitor.controlUrl. We respond with the standard {"results": [...]} custom
tool shape.

Tool arguments:
  - reason (required): a department name ("Lien Dept", "Intake Dept",
    "Settlement Dept", "Opening Dept", "Prelit Dept", "Calendar Dept",
    "Client Services", "Office") maps to the customer's
    TransferDestination rows by name;
    "case" routes by the case's SmartAdvocate status rules.
  - case_number (optional): the synced case to route by when
    reason="case", copied verbatim from lookupClient.
  - announcement (required in the tool schema, tolerated absent here):
    the line the model SPEAKS itself, mirrored into the argument for
    logging. The system never speaks it: dev A/B on 2026-08-31 showed
    every system-spoken slot doubles — the model narrates alongside the
    tool call on about half of transfer turns (three prompt phrasings
    could not stop it), and Vapi occasionally double-played the LCC
    `content` field even on bare tool calls. Instead the prompt has the
    model speak the announcement (ending with <flush />) and then call
    this tool in the same response, and this handler holds the transfer
    POST until the announcement's TTS actually finishes. The hold is
    closed-loop, not a blind timer: it arms a TransferAnnouncementGate row
    and waits for the /webhooks/vapi speech-update events to show the
    assistant has gone quiet (see _await_announcement_quiet). The tool is
    sync (async: false), so Vapi blocks the model's next turn during the
    hold, and the in-flight announcement keeps streaming meanwhile.

Bad or missing arguments degrade to the office fallback, never to a failed
transfer. Only when no destination exists at all, or the control URL is
missing, does this return a tool error (the model then apologizes and keeps
the call going).

## Request body

- VapiCustomToolRequest — Wrapper for Vapi custom tool requests.
  - `message` VapiMessage, required
    - `type` string, required
    - `toolCallList` VapiToolCall[], required
      - `id` string, required
      - `type` string
      - `function` VapiToolCallFunction, required
        - `name` string, required
        - `arguments` object, required
    - `call` VapiCallObject
      - `id` string, required

## Response `200`

Successful Response

- VapiCustomToolResponse — Standard Vapi custom tool response format.
  - `results` VapiToolResult[], required
    - `toolCallId` string, required
    - `result` string, required

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/openintake/apis/fastapi.md) · [All operations](https://skmtc.dev/openintake/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/openintake/fastapi/revisions/b642b91e0a5c/schema)
