Phone
The phone module manages phone number lifecycle: listing owned numbers, purchasing new ones via Twilio, assigning voice agents and text agents per number, and setting friendly names for easier identification.
Module Structure
Section titled “Module Structure”src/mods/phone/├── api/ # CRUD endpoints for phone numbers and agent assignment├── components/ # UI: phone list, card, details form, buy flow, confirm modal├── types/ # Phone, PhoneData, BuyPhoneResponse, SearchAvailableResponse└── views/ # PhoneListView, PhoneDetailsView, BuyPhoneViewAPI Endpoints
Section titled “API Endpoints”All endpoints require an authenticated Session and enforce ABAC permission checks.
| Method | Route | Permission | Description |
|---|---|---|---|
GET | /api/phones | Phone:Collection:List | List all org phone numbers |
GET | /api/phones/{id} | Phone:Instance:View | Get a single phone by UUID |
PUT | /api/phones/{id} | Phone:Instance:Update | Update friendly name, voice agent, and/or text agent |
PUT | /api/phones/{phone_number_id}/agent/{agent_id} | Phone:Instance:Update | Assign a voice agent (legacy route) |
DELETE | /api/phones/{phone_number_id}/agent | Phone:Instance:Update | Clear voice agent assignment |
The purchase endpoints live in the twilio module but are part of the buy flow:
| Route | Method | Description |
|---|---|---|
POST | /api/twilio/search-available-numbers | Search by area code |
POST | /api/twilio/buy-phone-number | Purchase an E.164 number |
Permissions
Section titled “Permissions”The phone resource defines four ABAC permissions. All phone access is org-scoped — there are no assignment-based variants like the contact module.
| Permission | Grants |
|---|---|
Phone:Collection:List | View the phone number list |
Phone:Collection:Create | Purchase new phone numbers |
Phone:Instance:View | View phone details |
Phone:Instance:Update | Edit friendly name and agent assignments |
Super-admins and org owners bypass all permission checks.
API Enforcement
Section titled “API Enforcement”Collection endpoints check permissions before querying the database — unauthorized requests never reach the DB:
PhoneResource::has_collection_permission(&session, PhoneCollectionPermission::List) .or_forbidden("Not allowed to list phones")?;Instance endpoints check permissions after loading the entity (the org ID is needed for the instance check):
PhoneResource::has_instance_permission(&session, PhoneInstanceAction::Update, &instance) .or_forbidden("Not allowed to update this phone")?;UI Guards
Section titled “UI Guards”All three phone views enforce permissions client-side with early-return AccessDenied guards:
| View | Permission | Behavior |
|---|---|---|
PhoneListView | Phone:Collection:List | Shows AccessDenied if missing |
BuyPhoneView | Phone:Collection:Create | Shows AccessDenied if missing |
PhoneDetailsView | Phone:Instance:View | Shows AccessDenied if missing |
The Buy Number button in the list view only renders when the user holds Phone:Collection:Create. The Save button in the details form disables via a read_only prop when the user lacks Phone:Instance:Update:
// PhoneDetailsView passes read_only based on permissionPhoneDetails { phone, agents, text_agents, on_save: handle_save, read_only: !can_update,}Data Types
Section titled “Data Types”pub struct Phone { pub id: Uuid, pub number: String, // E.164 format, e.g. "+15551234567" pub friendly_name: Option<String>, // User-defined label, e.g. "Front Desk" pub agent_id: Option<Uuid>, // Voice agent — None = unassigned pub text_agent_id: Option<Uuid>, // Text agent — None = unassigned pub text_agent_auto_reply: bool, // Auto-send best suggestion for inbound SMS}PhoneData (Update Payload)
Section titled “PhoneData (Update Payload)”pub struct PhoneData { pub friendly_name: Option<String>, // Trimmed; empty string clears the name pub agent_id: Option<Uuid>, // None = clear voice agent pub text_agent_id: Option<Uuid>, // None = clear text agent pub text_agent_auto_reply: bool,}Purchase Responses
Section titled “Purchase Responses”BuyPhoneResponse maps to HTTP status codes:
| Variant | HTTP Status |
|---|---|
Success(String) | 201 |
InvalidPhoneNumber | 400 |
NoNumbersAvailable | 404 |
TwilioNotConfigured | 500 |
TwilioError(String) | 502 |
InternalError(String) | 500 |
SearchAvailableResponse follows the same pattern, returning Vec<AvailablePhone> on success (200).
Friendly Names
Section titled “Friendly Names”Each phone number can have an optional friendly_name label (e.g. "Front Desk", "Sales Line").
- Set or update:
PUT /api/phones/{id}with{ "friendly_name": "Front Desk", "agent_id": null } - Clear: Send an empty string or omit the field — the server trims and normalises to
None - Fallback: Wherever a friendly name is shown (phone cards, analyzer selectors, call history), the raw E.164 number is always used as a fallback when no name is set
Phone Purchase Flow
Section titled “Phone Purchase Flow”User enters 3-digit area code → POST /api/twilio/search-available-numbers → Results grid: friendly name, location, capability badges (Voice/SMS/Fax) → User selects a number → ConfirmPurchaseModal → POST /api/twilio/buy-phone-number (E.164 number) Server side: 1. Validate E.164 format 2. Fetch org Twilio credentials 3. Call Twilio API to purchase 4. Insert phone_number record (id, org_id, number, twilio_sid, agent_id=None, friendly_name=None) 5. Auto-assign default analyzers to the new number 6. Set voice webhook URL on Twilio to {APP_HOST}/twilio/voice → Navigate to phone listAgent Assignment
Section titled “Agent Assignment”Each phone number supports two independent agent assignments, both managed from the phone details form.
Voice Agent
Section titled “Voice Agent”Handles inbound calls on this number. Only one voice agent can be assigned per phone.
- Assign/update:
PUT /api/phones/{id}withagent_idset - Clear via dedicated route:
DELETE /api/phones/{phone_number_id}/agent - Clear via update:
PUT /api/phones/{id}withagent_id: null
Text Agent (PR #295)
Section titled “Text Agent (PR #295)”Handles inbound SMS on this number. Acts as the org-wide default for that number — used when no contact-level text agent assignment exists for the inbound sender.
- Assign/update:
PUT /api/phones/{id}withtext_agent_idset - Clear:
PUT /api/phones/{id}withtext_agent_id: null - Auto-reply toggle: when
text_agent_auto_reply: true, the agent automatically sends the highest-confidence reply suggestion to every inbound SMS on this number (no human review)
Priority order for inbound SMS routing:
Inbound SMS arrives on phone P → Contact-level text agent link? → Use that agent + its auto_reply setting → No contact link → Phone-level text_agent_id? → Use that + phone's text_agent_auto_reply → No phone agent → No suggestions generated; message stored onlyThe auto-reply toggle only appears in the UI when a text agent is selected. Deselecting the agent hides the toggle and clears the auto_reply state.
After saving, the phone data refetches to reflect the change.
Database Migration
Section titled “Database Migration”PR #145 added friendly_name via a dedicated migration:
migration/src/m20260223_000000_phone_number_add_friendly_name.rsAfter running just migrate, regenerate SeaORM entities with just generate.
PhoneListView — Grid of phone cards (1/2/3 columns responsive). Each card shows the friendly name prominently with the raw number beneath. Links to the details view. Empty state prompts to buy a number. Header includes a “Buy Number” button.
PhoneDetailsView — Loads phone + all agents in parallel. Renders a form with a friendly name text field, agent selector, and save button. Calls PUT /api/phones/{id}.
BuyPhoneView — Hosts the purchase flow. Navigates to the phone list on successful purchase.
Related Modules
Section titled “Related Modules”- Agent — voice agents are assigned to phone numbers to handle inbound calls
- Text Agents — text agents handle inbound SMS; phones act as the org-wide default assignment
- Twilio — provides the purchase and search APIs, receives inbound calls and SMS
- Call — uses
Phone.friendly_namein the “Received On” call history column