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,
pub phones: Vec<ContactPhone>,
pub emails: Vec<ContactEmail>,
pub created_at: String,
}
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,
}

A contact has many phone numbers and many email addresses. The is_preferred flag marks the primary entry for each type — used by other modules (e.g. the todo system uses the preferred email when sending follow-up emails).

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 already enriched (either name field is non-empty)
  3. Send transcription to GPT-5 with structured output to extract:
    • first_name (empty string if not mentioned)
    • last_name (empty string if not mentioned)
    • email (null if not mentioned)
  4. Update contact name if either field was extracted
  5. Create a contact_email record if an email was found (lowercased, marked as preferred)

All endpoints require an authenticated session.

RouteMethodDescription
/api/contactsGETList all org contacts (newest first)
/api/contacts/{id}GETGet contact with phones and emails
/api/contactsPOSTCreate a contact (first_name, last_name)
/api/contacts/{id}PUTUpdate name fields
/api/contacts/{id}DELETEDelete a contact
RouteMethodDescription
/api/contacts/{contact_id}/phonesPOSTAdd a phone number
/api/contacts/{contact_id}/phones/{phone_id}PUTUpdate a phone number
/api/contacts/{contact_id}/phones/{phone_id}DELETERemove a phone number
RouteMethodDescription
/api/contacts/{contact_id}/emailsPOSTAdd an email address
/api/contacts/{contact_id}/emails/{email_id}PUTUpdate an email address
/api/contacts/{contact_id}/emails/{email_id}DELETERemove an email address
src/mods/contact/
├── api/ # CRUD for contacts, phones, emails
├── components/ # UI: contact list, card, details, phone/email cards, create form
├── services/ # resolve_contact, enrich_contact
├── types/ # Contact, ContactPhone, ContactEmail + data DTOs
└── views/ # ContactListView, ContactDetailsView, CreateContactView
  • Call — calls link to contacts via contact_id; enrichment runs post-transcription
  • TodosSendEmailToCaller tool uses the contact’s preferred email
  • Supporting Modules — dashboard shows active/total contact counts