Skip to content

Transfer a call

The bot chooses a safe route name. The deployment maps it to the protected phone or SIP destination.

assistant.add_transfer_route(
    "human-support",
    support_destination,
)

When the selected preset enables transfer_call, the model sees human-support—never the raw destination.

Resolve sensitive business context in the call's preparation hook and freeze only the minimum value the deployment transfer operation needs:

@assistant.prepare
async def prepare_call(call: Call) -> object:
    account = await salesforce.resolve_verified_account(call.caller)
    return {"salesforce_account_id": account.id}

Deployment-owned handoff

The default operation is the accepted LiveKit handoff's transfer_call, which uses LiveKit SIP REFER. A deployment can supply an Assistant-level transfer_operation when another control plane owns the handoff:

from vaani import Assistant, Call, TransferDisposition


async def complete_transfer(call: Call, destination: str) -> TransferDisposition:
    # Call the deployment's control plane here. Keep credentials and provider
    # payloads inside that deployment-owned integration.
    account_id = call.state["salesforce_account_id"]
    await ozonetel_transfer(destination, uui=account_id)
    return TransferDisposition.COMPLETED


assistant = Assistant(
    name="Support",
    preset="basic-monolingual@1",
    prompt="Help the caller.",
    transfer_operation=complete_transfer,
)

When configured, this operation receives the resolved immutable Call and the registered destination, then suppresses the default LiveKit transfer_call/SIP REFER operation. A consumer may resolve sensitive business context once in @assistant.prepare and return it in Call.state; the exact frozen state is bound to this call's transfer closure. For example, the Birla deployment stores the verified Salesforce AccountId as salesforce_account_id and passes it as Ozonetel UUI. This is request-scoped data, not a preset-global value or a fallback that can claim screen-pop success. Never put the raw AccountId/UUI in logs, exceptions, Judge evidence, browser payloads, or transfer records.

Call.metadata still contains only the fields allowlisted by the selected preset; it is immutable. Return TransferDisposition.COMPLETED only after the deployment has completed the handoff; ACCEPTED records acceptance without claiming completion. Omitting transfer_operation or setting it to None preserves the current LiveKit REFER behavior exactly.

The deployment operation is not a provider client supplied by VAANI. Do not log the destination, CTI values, credentials, request bodies, or exception details; VAANI records only provider-neutral transfer evidence. The Birla birla-prod-v42@14 preset allowlists the CTI correlation fields ucid, mucid, did, and callerno for the deployment's preparation and business logic. Other metadata remains dropped. Salesforce AccountId belongs in consumer-owned state, not in preset-global configuration or arbitrary incoming metadata.

Requirements

  • Register the route before assistant.check().
  • Keep the destination in deployment configuration.
  • Authorize the caller and transfer reason in bot-owned business logic.
  • Regenerate the production lock after routes change.
  • Test the actual SIP destination in the target environment.

Interpret the result

Disposition Meaning
REJECTED_AFTER_END_REQUEST ending was already accepted before transfer admission
FAILED handoff failed
TIMED_OUT completion was not observed before the deadline
ACCEPTED provider accepted the request; completion is not proven
COMPLETED typed completion evidence was received

Do not display ACCEPTED as completed. VAANI 1.0 supports blind transfer, not warm consultation or conference transfer.

Next: production readiness.