# AI Voice Agent · Retell + Twilio provider setup

Sibling of [readMe.md](/docs/modules_handbook/shared/voice-agent/readMe.md) — the
account-level wiring behind the module, kept so it can be rebuilt or audited. Nothing here
is code: it is the state of the Twilio and Retell accounts. Behaviour lives in the main doc;
the campaign-authoring surface lives in
[ai-profiles.md](/docs/modules_handbook/shared/voice-agent/ai-profiles.md).

## What was created, 2026-08-19, via API

The wiring below was created programmatically (Twilio Trunking API + Retell API) and is
live. If it ever has to be rebuilt, these are the pieces and their actual identifiers:

- **Twilio Elastic SIP Trunk** `TKb85d40f7b76e6057dac309efc1784a73` ("Retell AI voice
  agent"), termination `propertylab-retell.pstn.twilio.com`, origination
  `sip:sip.retellai.com` (so a lead calling BACK reaches the agent, not the old TwiML bin).
- **Twilio IP ACL** `ALbd721f2140427296bf0e6bae5508ff54` on the trunk's termination —
  Retell's blocks `18.98.16.120/30`, `3.42.144.0/23`, `153.57.128.0/18`. Without it Retell's
  SIP INVITEs are refused and every outbound call fails.
- **`+60360431529` associated to the trunk** (PN `PNdb675637c04228761b158721f7ca2424`).
  ⚠️ This moved the number's INBOUND routing off its old TwiML bin and onto the trunk.
  Outbound REST calls (the funnel voice automations) are unaffected.
- **Imported into Retell** (`POST /import-phone-number`) with `termination_uri` above and
  **weighted agent lists** — the old `inbound_agent_id`/`outbound_agent_id` fields were
  removed 2026-03-31; use `inbound_agents`/`outbound_agents` `[{agent_id, weight: 1}]`.
- **Agent** `agent_fad809f03946ac992ba2adc1a0` ("Wai Kit AI 助理"), engine
  **retell-llm** `llm_d196ef4a3b7e4e9d963288579ef6` (model `claude-4.5-haiku`, Chinese
  谈天 persona prompt + compliant AI-disclosure begin message), voice `minimax-Kevin`
  (Chinese; swap for the owner's clone later), `language` **array** `[en-US, zh-CN, ms-MY]`
  (Retell auto-routes ASR from the language set — this combination exceeds Deepgram's
  10-language code-switch coverage, so it routes to **Soniox**), `webhook_url` set to this
  app, 5 custom `post_call_analysis_data` fields (interest_level / purpose / budget /
  preferred_project / follow_up_requested — they land in `ai_voice_calls.analysis`).
  Published via `POST /publish-agent-version/{agent_id}` body `{"version": N}` (the old
  publish-agent endpoint was deprecated 2026-07-20).
- **Knowledge Base** `knowledge_base_32b46a601edc4529` ("Cochrane-Velocity 新盘比较") —
  7 structured entries (Peel Lane / Sunway Cochrane / Binastra Cochrane fact sheets,
  comparison table, FAQ, glossary), attached to the LLM via `knowledge_base_ids`. Edit the
  content in the dashboard (Knowledge Base) — the agent uses it live, +$0.005/min.

**Version traps learned by hitting them:** a PUBLISHED agent version is immutable
  ("Cannot update published agent other than version title") — create a draft with
  `POST /create-agent-version/{agent_id}` `{"base_version": N}` and PATCH with `?version=`;
  and an agent's **engine type is locked once any version exists** ("Cannot update response
  engine after agent versions have been created") — switching conversation-flow → retell-llm
  meant creating a NEW agent and repointing the number + CRM credential. The first
  (dashboard-template) agent `agent_171b010cc1d030b7dd55a84ab9` is orphaned and can be
  deleted in the dashboard.

**Still content work, in the dashboard (re-publish after each):**

1. The conversation flow's **opening line** — must be the approved AI-disclosure sentence
   (the template default is generic filler).
2. **Voice clone** — admin uploads recordings (Voice → Add custom voice, provider
   `platform` for Cantonese headroom; record in ALL THREE languages), then set `voice_id`.
3. **Knowledge Base** — upload the QnA, attach to the agent.

## One canonical webhook receiver, and a second account

The agents are SHARED across environments but Retell takes a SINGLE `webhook_url` per agent,
so every box that syncs must agree on one receiver — **production**
(`app.propertylab.com.my`). A non-production box pins `RETELL_WEBHOOK_URL` to production's
URL in `.env`, and `RetellAgentSync::webhookUrl()` sends it on BOTH create and update. Before
that fix `webhook_url` was written at create only, so agents kept pointing at whichever box
first created them — which is how a whole morning of production calls webhooked into the dev
box as "unknown call". A second environment still sees events in real time via
`RETELL_WEBHOOK_FORWARD_URL` on the receiver (see the main doc's relay bullet).

⚠️ The **AI Appointment System** suite runs its OWN Retell accounts, one per agency, and its
own receiver at `webhooks.ae.retell` — it is a separate fork of this wiring, not a second
consumer of the account above. See `ai_call_profiles.group_id` in the main doc.
