Contact
The contact module manages caller identities across the platform. When a call comes in, the system automatically resolves the caller’s phone number to a contact — creating a stub if needed. After the call, GPT-5 extracts the caller’s name and email from the transcription to enrich the record.
How It Works
Section titled “How It Works”Inbound call from +15551234567 → resolve_contact("+15551234567", org_id) Match in contact_phone? → Return contact ID No match? → Create stub contact + link phone → Return new ID
Call ends → Transcription saved → enrich_contact(call_id) Stub contact? → GPT-5 extracts name/email from transcription → Update recordData Model
Section titled “Data Model”Contact
Section titled “Contact”pub struct Contact { pub id: Uuid, pub first_name: String, pub last_name: String, // Professional pub company: Option<String>, pub job_title: Option<String>, // Lifecycle pub source: Option<String>, // e.g. "inbound_sms", "manual" pub status: ContactStatus, // active | inactive | do_not_contact | archived pub pipeline_stage: Option<String>, pub external_id: Option<String>, // ID in an external CRM or system // Personal pub timezone: Option<String>, // IANA timezone string pub preferred_language: Option<String>, pub date_of_birth: Option<String>, // ISO 8601 date pub gender: Option<String>, pub avatar_url: Option<String>, // AI pub ai_summary: Option<String>, // GPT-generated summary // Related entities pub phones: Vec<ContactPhone>, pub emails: Vec<ContactEmail>, pub addresses: Vec<ContactAddress>, pub tags: Vec<ContactTag>, pub assigned_users: Vec<AssignedUser>, // Timestamps pub created_at: String, pub updated_at: String, pub last_contacted_at: Option<String>,}ContactStatus
Section titled “ContactStatus”pub enum ContactStatus { Active, // "active" — default Inactive, // "inactive" DoNotContact, // "do_not_contact" Archived, // "archived"}Status is set manually and used for filtering and routing rules. DoNotContact can be used by integrations to suppress outbound activity.
ContactPhone
Section titled “ContactPhone”pub struct ContactPhone { pub id: Uuid, pub phone_number: String, // E.164 format pub label: Option<String>, // e.g. "Work", "Mobile" pub is_preferred: bool,}ContactEmail
Section titled “ContactEmail”pub struct ContactEmail { pub id: Uuid, pub email: String, pub label: Option<String>, pub is_preferred: bool,}ContactAddress
Section titled “ContactAddress”pub struct ContactAddress { pub id: Uuid, pub label: Option<String>, // e.g. "Home", "Work" pub street: Option<String>, pub city: Option<String>, pub state: Option<String>, pub zip: Option<String>, pub country: Option<String>, pub is_primary: bool,}A contact has many phone numbers, email addresses, and physical addresses. The is_preferred / is_primary flag marks the primary entry for each type — used by other modules (e.g. the todo system uses the preferred email for follow-ups).
Contact Resolution
Section titled “Contact Resolution”resolve_contact runs when an inbound call arrives, linking the caller to a contact record.
pub async fn resolve_contact( caller_phone: &str, organization_id: Uuid,) -> Result<Uuid, AppError>Flow:
- Search
contact_phonefor a matching phone number - If found: verify the contact belongs to the same org → return contact ID
- If not found: create a stub contact (
first_name: "",last_name: "") → add the phone number as preferred → return new contact ID
Each phone number exists once in the system — no duplicates across contacts.
Contact Enrichment
Section titled “Contact Enrichment”enrich_contact runs after a call is transcribed, filling in stub contacts with real data.
pub async fn enrich_contact(call_id: Uuid) -> Result<(), AppError>Flow:
- Load the call — skip if no
contact_idor no transcription - Load the contact — skip if not a stub (see below)
- Send transcription to AI with structured output to extract caller details (name, email, company, job title, language, timezone, gender, pipeline stage)
- Validate output — reject “Unknown” or stub-like names
- Update contact fields in a transaction (fill empty only, never overwrite)
- Insert email if extracted and contact has none (deduplicated, validated)
A contact is a stub when both first_name and last_name are empty or "Unknown" (case-insensitive). The has_stub_name() check handles all variations. See Call Enrichment for full details on the extraction schema, validation rules, and transaction safety.
API Endpoints
Section titled “API Endpoints”All endpoints require an authenticated session.
Contacts
Section titled “Contacts”| Method | Route | Description |
|---|---|---|
GET | /api/contacts/list | List contacts with search, sort, tag filter, and pagination |
GET | /api/contacts/{id} | Get contact with phones, emails, addresses, and assigned users |
GET | /api/contacts/{id}/calls | List all calls for a contact with full details (transcription, analysis) |
GET | /api/contacts/{id}/todos | List todos linked to a contact’s calls |
POST | /api/contacts | Create a contact |
PUT | /api/contacts/{id} | Update contact fields |
DELETE | /api/contacts/{id} | Delete a contact |
Create/Update payload (ContactData):
pub struct ContactData { pub first_name: String, pub last_name: String, pub company: Option<String>, pub job_title: Option<String>, pub source: Option<String>, pub status: ContactStatus, // defaults to Active pub timezone: Option<String>, pub preferred_language: Option<String>, pub date_of_birth: Option<String>, pub gender: Option<String>, pub pipeline_stage: Option<String>, pub external_id: Option<String>, pub avatar_url: Option<String>,}GET /api/contacts/list
Section titled “GET /api/contacts/list”Returns paginated contacts with server-side search, sort, and tag filtering.
Query Parameters:
{ search?: string, // Full-text search across name, phone, email sort?: "recently_added" | "name_asc" | "last_contacted_at", // Default: recently_added tag_ids?: Uuid[], // Filter by tag IDs (OR: contact has any tag) page?: number, // 0-indexed, default: 0 page_size?: number, // Default: 25, max: 50}Response:
{ contacts: Contact[], total_count: number, page: number, page_size: number, total_pages: number,}Search uses LIKE queries with subqueries for phone and email matching. Tag filtering returns contacts with at least one of the selected tags.
GET /api/contacts/{id}/calls
Section titled “GET /api/contacts/{id}/calls”Returns all calls for a contact, newest first, with full CallDetails including transcription and analysis groups.
Response: CallDetails[] — each entry includes received_on (formatted phone number name), transcription text, and grouped analysis runs per analyzer.
Phone Numbers
Section titled “Phone Numbers”| Method | Route | Description |
|---|---|---|
POST | /api/contacts/{contact_id}/phones | Add a phone number |
PUT | /api/contacts/{contact_id}/phones/{phone_id} | Update a phone number |
DELETE | /api/contacts/{contact_id}/phones/{phone_id} | Remove a phone number |
Email Addresses
Section titled “Email Addresses”| Method | Route | Description |
|---|---|---|
POST | /api/contacts/{contact_id}/emails | Add an email address |
PUT | /api/contacts/{contact_id}/emails/{email_id} | Update an email address |
DELETE | /api/contacts/{contact_id}/emails/{email_id} | Remove an email address |
Addresses
Section titled “Addresses”| Method | Route | Description |
|---|---|---|
POST | /api/contacts/{contact_id}/addresses | Add a physical address |
PUT | /api/contacts/{contact_id}/addresses/{address_id} | Update an address |
DELETE | /api/contacts/{contact_id}/addresses/{address_id} | Remove an address |
Address payload (ContactAddressData):
pub struct ContactAddressData { pub label: Option<String>, // e.g. "Home", "Work" pub street: Option<String>, pub city: Option<String>, pub state: Option<String>, pub zip: Option<String>, pub country: Option<String>, pub is_primary: bool,}UI Views
Section titled “UI Views”Contact List
Section titled “Contact List”The contact list view (/contacts) features:
- Server-side search — real-time full-text search across names, phone numbers, and email addresses with 300ms debounce
- Sort options — recently added (default) or name A-Z
- Tag filtering — multi-select dropdown showing count badge when active, filters by OR (contact has any selected tag)
- Pagination — numbered page controls with ellipsis for large datasets (25 items per page, max 50)
- Responsive table — sticky header on desktop (≥768px), card grid on mobile
Contact Detail
Section titled “Contact Detail”The contact detail view (/contacts/{id}) uses a full-width edge-to-edge layout with three independently scrolling columns:
- Left sidebar (1/4) — avatar with initials, inline name editing, quick action buttons (call, email, note), tag management with color badges, phone/email CRUD with preferred indicators, and two-step delete confirmation
- Center column (2/4) —
CommunicationFeeddisplaying calls and SMS messages in a single chronological timeline. Includes a sticky filter bar (All / Calls / Messages), auto-scroll to the latest entry, date-grouped dividers, and an image lightbox for MMS attachments. Clicking an inbound message opens theInlineSuggestionsPanelfor AI-suggested replies. When the contact has a linked phone number, a compose bar appears at the bottom for sending messages directly from the contact view. - Right panel (1/4) — three tabs:
- Notes — searchable note feed with area-based pill filters (All / Manual / Call / SMS / AI / Alert), sort options (Newest / Oldest / Recently edited), inline note creation with optimistic updates, and a Contact Memory section for AI-synthesized notes
- Todos — list of todos linked to the contact’s calls
- Activity — unified activity timeline merging calls, notes, and todos sorted by date, with compact pill-style filters (All / Calls / Notes / Todos) and collapsible day groups
The layout stacks vertically on mobile and restores the 3-column grid at lg: breakpoint (≥1024px). Each column has a sticky header (filter bar / tabs strip) with independent scroll.
Contact Assignments
Section titled “Contact Assignments”Contact assignments link team members to contacts, enabling workload distribution and focused views. Admins can assign multiple members to a single contact and filter by assignment status.
Assignment Data Model
Section titled “Assignment Data Model”pub struct ContactAssignment { pub id: Uuid, pub first_name: String, pub last_name: String, pub primary_phone: Option<String>, pub assigned_users: Vec<AssignedUser>, pub tags: Vec<ContactTag>, pub created_at: String, pub last_contacted_at: Option<String>,}
pub struct AssignedUser { pub member_id: Uuid, pub user_id: Uuid, pub name: String,}Assignments are many-to-many: a contact can have multiple assigned users, and a user can be assigned to many contacts. The contact_user table stores the relationships.
Assignment API
Section titled “Assignment API”| Method | Route | Description |
|---|---|---|
GET | /api/contacts/assignments?query | Paginated assignments view with search, sort, tag, and member filtering (admin-only) |
POST | /api/contacts/{contact_id}/members/{member_id} | Assign a member to a contact (admin-only, idempotent) |
DELETE | /api/contacts/{contact_id}/members/{member_id} | Remove a member from a contact (admin-only) |
GET /api/contacts/assignments
Section titled “GET /api/contacts/assignments”Returns a paginated list of contacts with their assigned users, tags, and primary phone. Supports filtering by search term, tag IDs, and assignment status.
Query Parameters:
{ search?: string, // Full-text search across name, phone sort?: "recently_added" | "name_asc" | "last_contacted_at", // Default: name_asc tag_ids?: Uuid[], // Filter by tag IDs (OR: contact has any tag) member_filter?: "all" | "unassigned" | Uuid, // Filter by assignment status or specific member page?: number, // 0-indexed, default: 0 page_size?: number, // Default: 25, max: 50}Response:
{ contacts: ContactAssignment[], total_count: number, assigned_count: number, // Total assigned contacts org-wide page: number, page_size: number, total_pages: number,}The member_filter can be:
"all"— show all contacts"unassigned"— show only unassigned contactsUuid(member_id) — show contacts assigned to a specific member
Assignment UI
Section titled “Assignment UI”The assignment management view (/contacts/assignments) is admin-only and features:
- Server-side search — real-time full-text search with 300ms debounce
- Sort options — name A-Z (default), recently added, or last contacted
- Tag filtering — multi-select dropdown with color badges
- Member filtering — dropdown to filter by all, unassigned, or specific team member
- Alphabetical grouping — when sorted by name, contacts are grouped by first letter with sticky section headers
- Bulk assignment — select multiple contacts and assign them to a team member in one action
- Inline assignment badges — each row shows assigned users with remove buttons
The view also displays stats at the top showing assigned contact count vs total contacts.
Assignment in Contact Detail
Section titled “Assignment in Contact Detail”The contact detail sidebar includes an Assigned Users section showing all members assigned to the contact. Admins can add or remove assignments inline.
Related Modules
Section titled “Related Modules”- Call — calls link to contacts via
contact_id; enrichment runs post-transcription - Todos —
SendEmailToCallertool uses the contact’s preferred email - Contact Notes — free-text notes with author permissions
- Contact Tags — org-level labels for categorising contacts
- Contact Memory — AI-synthesized memory injected into agent sessions