BluetickedDeveloper docs

Contacts

Create, list, fetch and update contacts by API and SDK, including each channel's deliverability flags.

Create or update (upsert)

A contact is a person you can message across WhatsApp, SMS, and email. A send looks its recipient up in your contacts (or creates them in the same call with to.create), so syncing contacts is usually the first thing an integration does. A contact is identified by its phone (E.164) and/or email, within one business.

bt.contacts.upsert creates a contact, or updates the one in that business with the same phone (or, when you send no phone, the same email), so it is safe to call on every sync without creating duplicates. Say what each person agreed to: a channel flag is set only with its consent.

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

console.log(contact.id); // "6f1c2a7e-3b4d-4e8f-9a0b-1c2d3e4f5a6b"

The contact shape

Channel deliverability flags are nested under channels, each with the time its consent was given. A flag needs both the identifier and its consent: has_whatsapp and has_sms a phone and that channel's consent, has_email an email and email consent. Without consent the flag is false:

Contact
{
  "id": "6f1c2a7e-3b4d-4e8f-9a0b-1c2d3e4f5a6b",
  "business_id": "22222222-2222-4222-9222-222222222222",
  "first_name": "Sam",
  "last_name": "Mokoena",
  "phone": "+27821234567",
  "email": "sam@example.com",
  "tags": ["vip"],
  "custom_fields": {},
  "channels": {
    "has_whatsapp": true,
    "whatsapp_opted_in_at": "2026-05-22T10:30:00.000Z",
    "has_sms": true,
    "sms_opted_in_at": "2026-05-22T10:30:00.000Z",
    "has_email": true,
    "email_opted_in_at": "2026-05-22T10:30:00.000Z",
    "email_optin_status": "confirmed"
  },
  "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("6f1c2a7e-3b4d-4e8f-9a0b-1c2d3e4f5a6b");

Update a contact

update is a partial PATCH: only the fields you pass change, and the body must contain at least one field. Changing phone or email (or clearing it with null) resets that identifier's channel flags and consent: a new phone clears has_whatsapp and has_sms until consent is given again. Omitting a field leaves it untouched.

update.ts
await bt.contacts.update("6f1c2a7e-3b4d-4e8f-9a0b-1c2d3e4f5a6b", {
  first_name: "Samuel",
  tags: ["vip", "renewed"],
});

// Clear an email and its has_email flag:
await bt.contacts.update("6f1c2a7e-3b4d-4e8f-9a0b-1c2d3e4f5a6b", { 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. Creating a contact emits the contact.created webhook and update emits contact.updated; an upsert that only updates an existing contact emits neither.