Built-in Tools
These work out of the box with no server needed:Opt-outs (do_not_call)
Opt-in: an agent has it only if its tools list names it. When a caller asks
not to be called again, the tool suppresses their number for the whole project
— the dialling number on an inbound call, the dialled one outbound — so every
later outbound dial to it is refused 409 number_suppressed. It then records
call.opted_out ({number}, also sent to webhook subscribers) and ends the
call. A call with no phone number (WebRTC) is refused and keeps running.
Ending a call politely
end_call and do_not_call don’t hang up when they run. The model answers
the tool’s result as it would any other — that answer is its goodbye — and the
call ends once that reply has played in full. A model that already said goodbye
alongside the tool call is told it may reply with nothing.
The goodbye is final. From the moment it is written the call stops listening:
the caller cannot interrupt it, and anything they say while it plays is not
transcribed or recorded.
So the tool belongs at the moment the conversation is over, and the goodbye
after it. Many models will not say goodbye and call a tool in one reply; if
the instructions ask for the goodbye first, they say it, wait for the caller,
and only call the tool on the caller’s next turn. A description that works:
“Call this when the caller is done or says goodbye. You will then say a short
goodbye, and the call ends after it.”
POST /v1/calls/{call_id}/end is unchanged: an operator ending a call from
outside hangs up at once.
Call transfer (cold / warm)
transfer_call moves a live Twilio call to a human:
transfer_mode:cold(blind — bridge immediately) orwarm(brief the operator first).transfer_message: spoken to the caller before the dial (“Connecting you…”).briefing(warm only): spoken to the operator before bridging — a string, or{"from_summary": true}to summarize the transcript on the fly.fallback_message: spoken to the caller if the operator doesn’t answer, then the call ends.
POST /v1/calls/{call_id}/transfer. Warm
transfer and fallback_message require PUBLIC_BASE_URL to be set (Twilio calls back to
TurnCall for the operator briefing and the no-answer fallback). If the operator’s line goes
to voicemail, the caller is still connected (and can leave a message); a transfer.answered
event reports answered_by (human/machine). See the
the examples/call-transfer example.
Transfers redirect the call’s PSTN leg, so they work on Twilio calls — a
WebRTC or WhatsApp session has no phone leg to bridge.
Choosing the transfer target
target_number is an argument the LLM supplies, so where the number comes from is a design choice. Rule of thumb: prompt = policy (when to transfer, what to say), code = facts that change (who’s on call, at what number).
For the lookup-tool pattern, three rules keep transfers reliable: always return a number (bake in a fallback line — an error mid-transfer strands a frustrated caller), answer fast (the caller waits in silence; the default tool timeout is 10s), and return E.164. Avoid putting schedules or rota tables in the prompt itself — the LLM has no reliable clock and can mistranscribe digits; that logic belongs in the tool.
Custom Webhook Tools
Define tools with awebhook_url — TurnCall POSTs to your server when the LLM invokes the tool:
Sync and async tools
execution_mode decides what happens when the caller interrupts while a tool
is still running.
Use
async for work that is slower than the conversation — a CRM lookup, an
availability check against a slow system — so a caller saying “actually, make
that Tuesday” doesn’t throw away a request already in flight.
Voice only. A text turn is request/response: there is no ongoing conversation
to deliver a late result into, so
execution_mode is ignored on SMS, the
Chat API and WhatsApp text.Webhook Payload
Your server receives:call_id and session_id is set. A voice call sends
call_id; an SMS, Chat API or WhatsApp text conversation sends session_id
and leaves call_id null. Branch on whichever you need — or ignore both if
your handler is stateless.
Tools on text channels
The same webhook tools work in SMS, the Chat API and WhatsApp text. Two differences worth knowing:- Built-in tools are voice-only.
end_call,transfer_call,send_dtmf,do_not_callandhandoff_to_agentall act on a live call, so they aren’t offered in a text session. Configure them if you like — they’re simply not advertised to the model there. - Tool exchanges aren’t part of the stored history. Chat history is rebuilt from the stored customer and assistant messages each turn, so the model sees its own tool calls while it resolves one message and not afterwards.
Signed Tool Webhooks
Setwebhook_secret (min 16 chars) on a tool and TurnCall HMAC-signs every POST so your endpoint can verify it really came from TurnCall:
X-TurnCall-Signature: v1=<hex> and X-TurnCall-Timestamp — HMAC-SHA256 over "{timestamp}.{raw_body}", the same scheme as event webhooks. Verify with:
Tool Invocation Recording
All tool calls (webhook + MCP + built-in) are recorded in thetool_invocations table with:
- Input arguments
- Output result
- Status (success/error)
- Latency (ms)
call_id and session_id, exactly one of them set.
Recording is best-effort and runs after the answer goes out, so a database
hiccup costs you the audit row rather than the caller’s reply. Each recorded
call also dispatches a
tool.result webhook event, on both voice and text.