Docs/

Contacts

Core data

Contacts

One row per person you can message. Contacts are tenant-scoped and identified by phone number, so they accumulate automatically whether they arrive via form, import, or inbound chat.

The contact model

  • phoneNumberrequiredE.164 string. Unique per workspace — used as the natural key for find-or-create.
  • firstName / lastNameoptionalStrings, used by template variable substitution and {{fullName}}.
  • emailoptionalUsed for the {{email}} variable and the contact detail panel.
  • optedInboolDefault true. Required by Meta — a contact can only be messaged after opt-in is recorded.
  • optedInAtdatetimeSet when optedIn flips to true. Auditable proof of consent.
  • optOutboolDefault false. When true, the contact is suppressed from campaigns and broadcasts.
  • dobdateOptional date-of-birth, used by the CONTACT_BIRTHDAY automation trigger.
  • customFieldsjsonFree-form JSON for attributes that aren’t on the model (loyalty tier, last visit, etc.).

Phone is the natural key

Each phone number is unique within your workspace. Every write path — manual, import, form, inbound, API — runs find-or-create against (tenantId, phoneNumber). Repeat submissions update the existing row instead of duplicating it.

Creating contacts

Manual entry

Add one contact from the Contacts page. Validates phone format and dedupes by phone.

CSV import

Bulk upload with a per-tenant advisory lock and ImportJob tracking. New rows fire CONTACT_CREATED; duplicates are skipped.

Lead form

When a hosted form is submitted, the form payload is upserted into a contact by phone.

Inbound message

If a contact messages your WhatsApp number and no row exists, one is created automatically.

REST API

POST /api/v1/contacts find-or-creates by phoneNumber. 200 on match, 201 on create.

Tags

Tags are flat labels — a many-to-many between contacts and a per-tenant tag list. They are the only primitive automations and segments can match on, so naming matters: keep the names stable.

Working with tags

  • Apply via the Contacts UI, the contact PATCH /api/v1/contacts/:id with tagIds, or an ADD_TAG automation step.
  • Tag names are unique per tenant. The CONTACT_TAG_ADDED automation trigger fires only when tags are genuinely added (id diff), so re-applying the same tag is a no-op.

Segments

A segment is a saved filter — JSON evaluated against your contact list. The most common filter is "contacts in tags X, Y". Note that segments match tag names, not tag IDs, so renaming a tag silently breaks any segment that depended on it.

No native Meta groups

WhatsApp Cloud API has no group primitive — segments are a WhautX layer that compiles to a contact list at send time. Audience size on Meta’s side is irrelevant; trust the WhautX segment count.

Custom fields

Anything that isn’t a model column lives in customFields as JSON. Use them for loyalty tiers, last-visit dates, internal IDs you need to look up by, etc. An UPDATE_CONTACT automation step writes non-whitelisted fields there automatically.