Contacts

A contact is a person you can message across WhatsApp, SMS, and email. Every send targets an existing contact, so contacts are usually the first thing your integration creates. A contact is identified by its phone (E.164) and/or email.

Create or update (upsert)

bt.contacts.upsert creates a contact, or updates the one already matched by the same phone or email — so it is safe to call on every sync without creating duplicates:

upsert.ts
const contact = await bt.contacts.upsert({
  phone: "+27821234567",
  email: "sam@example.com",
  first_name: "Sam",
  last_name: "Mokoena",
  tags: ["vip"],
});

console.log(contact.id); // "con_123"

The contact shape

Channel deliverability flags are nested under channels — they are derived server-side from the identifiers on the record (a contact with a phone gets has_whatsapp/has_sms; an email gets has_email):

Contact
{
  "id": "con_123",
  "business_id": null,
  "first_name": "Sam",
  "last_name": "Mokoena",
  "phone": "+27821234567",
  "email": "sam@example.com",
  "tags": ["vip"],
  "channels": {
    "has_whatsapp": true,
    "has_sms": true,
    "has_email": true
  },
  "suppressed": false,
  "created_at": "2026-05-22T10:30:00.000Z",
  "updated_at": "2026-05-22T10:30:00.000Z"
}

Read a single flag as contact.channels.has_whatsapp — not contact.has_whatsapp. The top-level suppressed flag is the do-not-contact gate; see Suppression and opt-outs.

List contacts

Listing is cursor-paginated. Pass limit and the next_cursor from the previous page:

list.ts
let cursor: string | null | undefined;
do {
  const page = await bt.contacts.list({ limit: 100, cursor: cursor ?? undefined });
  for (const c of page.data) {
    // ... process each contact
  }
  cursor = page.next_cursor;
} while (cursor);

Get one contact

get.ts
const contact = await bt.contacts.get("con_123");

Update a contact

update is a partial PATCH: only the fields you pass change, and the body must contain at least one field. Passing null for phone or email clears it (and the matching channel flag); omitting a field leaves it untouched.

update.ts
await bt.contacts.update("con_123", {
  first_name: "Samuel",
  tags: ["vip", "renewed"],
});

// Clear an email and its has_email flag:
await bt.contacts.update("con_123", { email: null });

Toggling the do-not-contact gate is also a contact update — set suppressed: true to block every channel for the contact, or false to re-enable. Contact changes emit contact.created / contact.updated webhooks.