Skip to content

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)