Contacts
A Contact Profile represents one human. Multiple identities (WhatsApp wa_id, Telegram chat_id, email, web visitor token, …) can all point to the same profile — so a customer who switches from WhatsApp to email is recognised as the same person and conversations stay linked to one profile.
Data model
ContactProfile (one row per human)
├── ContactIdentity[] ← channel-specific identifiers
├── attributes (JSONB) ← reserved + custom fields
├── tags[] ← labels
├── accountId ← optional B2B link
└── source ← inbound:whatsapp | csv_import | manual | api | …How contacts get created
- Inbound message — first message on a new channel creates a Profile + Identity
- CSV import — bulk upload from
/dashboard/contacts → Import - Manual entry —
/dashboard/contacts → New contact - Public booking — booking creates a Profile if the phone isn’t already known
- API —
/api/public/...endpoints can find-or-create by phone
Cross-channel auto-merge
When a new identity is added, the platform checks for matching primaryPhone or
primaryEmail on existing profiles. A match merges (the new identity is attached to
the existing profile) instead of creating a duplicate. Phone-like identities
(phone, whatsapp_wa_id) merge with each other; emails merge with each other; the
rest don’t auto-merge (manual merge UI for now).
Reserved vs custom fields
- Reserved fields are platform-defined:
first_name,last_name,phone,email,dob, etc. Always present in suggestions, type-validated. - Custom fields are agency-defined per-scope (CONTACT, ACCOUNT, AGENCY). Used for
things like
lead_score,class_grade,preferred_pickup_time.
Last updated on