Skip to content

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.

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 record
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>,
}
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.

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,
}
pub struct ContactEmail {
pub id: Uuid,
pub email: String,
pub label: Option<String>,
pub is_preferred: bool,
}
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).

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:

  1. Search contact_phone for a matching phone number
  2. If found: verify the contact belongs to the same org → return contact ID
  3. 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.

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:

  1. Load the call — skip if no contact_id or no transcription
  2. Load the contact — skip if not a stub (see below)
  3. Send transcription to AI with structured output to extract caller details (name, email, company, job title, language, timezone, gender, pipeline stage)
  4. Validate output — reject “Unknown” or stub-like names
  5. Update contact fields in a transaction (fill empty only, never overwrite)
  6. 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.

All endpoints require an authenticated session.

MethodRouteDescription
GET/api/contacts/listList 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}/callsList all calls for a contact with full details (transcription, analysis)
GET/api/contacts/{id}/todosList todos linked to a contact’s calls
POST/api/contactsCreate 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>,
}

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.

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.

MethodRouteDescription
POST/api/contacts/{contact_id}/phonesAdd 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
MethodRouteDescription
POST/api/contacts/{contact_id}/emailsAdd 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
MethodRouteDescription
POST/api/contacts/{contact_id}/addressesAdd 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,
}

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

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)CommunicationFeed displaying 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 the InlineSuggestionsPanel for 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 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.

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.

MethodRouteDescription
GET/api/contacts/assignments?queryPaginated 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)

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 contacts
  • Uuid (member_id) — show contacts assigned to a specific member

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.

The contact detail sidebar includes an Assigned Users section showing all members assigned to the contact. Admins can add or remove assignments inline.

  • Call — calls link to contacts via contact_id; enrichment runs post-transcription
  • TodosSendEmailToCaller tool 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