Domain Model¶
Core entities¶
Contact¶
A WhatsApp user identified by wa_id (phone number in international format). Contacts are created automatically on first inbound message.
Contact
├── wa_id: str (e.g. "5219671234567")
├── profile_name: str (from WhatsApp profile)
├── tags: list[Tag]
├── lead_state: new | warm | hot | cold | stalled | enrolled
├── contact_profile: ContactProfile (structured data)
└── contact_events: list[ContactEvent] (append-only log)
Campaign¶
A campaign definition with a pipeline of phases. Contacts are enrolled in campaigns and progress through phases based on conversation signals.
Campaign
├── id: str (UUID)
├── name: str
├── status: draft | approved | running | completed | archived
├── pipeline: list[Phase]
│ └── Phase
│ ├── id: str (e.g. "calificacion", "clase_muestra")
│ ├── skill: PhaseSkill (agent instructions)
│ └── transitions: list[str] (allowed next phases)
├── resources: list[CampaignResource] (documents, media)
├── tenant_context: TenantContext (agent knowledge)
└── campaign_sends: list[CampaignSend] (execution records)
Message¶
An inbound or outbound WhatsApp message.
MessageRecord
├── wamid: str (WhatsApp message ID, globally unique)
├── direction: "inbound" | "outbound"
├── type: "text" | "image" | "video" | "audio" | "document" | "template" | ...
├── body: str
├── contact_wa_id: str
├── phone_number_id: str (business phone)
├── wa_timestamp: datetime
├── biz_opaque_callback_data: str | None (request_id for idempotency)
└── authored_by: "agent" | "system" | "operator" | None
StatusRecord¶
Delivery status from Meta webhook callbacks.
StatusRecord
├── wamid: str (links to MessageRecord)
├── status: "sent" | "delivered" | "read" | "failed"
├── wa_timestamp: datetime
├── error_code: str | None
├── error_title: str | None
└── biz_opaque_callback_data: str | None
Template¶
A WhatsApp message template registered with Meta.
Template
├── name: str (unique identifier)
├── language: str (e.g. "es_MX")
├── status: "APPROVED" | "PENDING" | "REJECTED"
├── category: "MARKETING" | "UTILITY" | "AUTHENTICATION"
├── header: Header | None (TEXT, IMAGE, VIDEO, LOCATION)
├── body: str (with {{1}}, {{2}} parameters)
├── footer: str | None
└── buttons: list[Button] | None
Relationships¶
Campaign ──1:N──► CampaignContactPhase ◄──N:1── Contact
│ │
│ └── phase_id, context (JSON)
│
├──1:N──► CampaignSend (execution record)
├──1:N──► CampaignResource (documents, media)
└──1:1──► TenantContext (agent knowledge)
Contact ──1:N──► Message ──1:N──► StatusRecord
│ (via wamid)
├──1:N──► ContactEvent (append-only)
├──1:1──► ContactProfile
├──1:N──► Tag
└──1:1──► OwnershipEvent (current: agent | human)
Ownership model¶
Every contact has an ownership state: agent (AI responds) or human (operator responds). Transitions happen via:
- Accept handoff — operator takes over (
agent → human) - Release — operator returns to AI (
human → agent) - Auto-release — 30 min idle timeout with pending inbound (
human → agent) - Pipeline trigger — certain phase transitions auto-trigger handoff (e.g. enrollment)