Skip to content

Conversations & contacts

What a conversation is across channels, how transcripts are structured, and how one person is recognised across WhatsApp, the web, and the phone.

Open the inbox

Every channel converges here. A widget chat at 11 p.m., a WhatsApp thread, a phone call — all three become a conversation with a full transcript, and all three resolve to a contact in the same customer directory. This page explains both, including where the identity story is still incomplete.

What a conversation is #

A conversation is one continuous exchange between a customer and an agent on one channel. It carries:

Field What it is
conversation_id The stable identifier. Use it to fetch the transcript.
channel Where it happened — web, whatsapp, or voice.
agent_id Which agent handled it.
status Where it stands; open conversations report open.
customer What is known about the person — name, email, and the channel-specific identifier.
message_count How many messages it contains.
preview The first 140 characters of the last real message, for list views.
created_at / updated_at When it started and when it last moved.
acquisition The ad that opened the conversation, when a Click-to-WhatsApp ad did; otherwise null. Set once and never changed.

Conversations are per channel by design. A customer who chats on your website in the morning and messages your WhatsApp number in the afternoon has two conversations — because they are two different threads, with different histories and different constraints. They resolve to one contact, which is where the two halves come back together.

A voice call produces a conversation like any other. The transcript is the turns of the call, speaker by speaker; the audio recording, the call outcome, and the response-latency figures live alongside it on the call record. See How voice works.

Visible threads and billable AI sessions #

The transcript boundary and the billing boundary are deliberately different. A website visitor can refresh and keep one continuous visible thread; the platform does not wipe the transcript merely because another usage unit began.

On web chat and WhatsApp, one AI conversation on the usage meter covers up to 20 messages counting both sides, normally 10 customer/agent exchanges, or 24 hours from the start of that billing window—whichever boundary arrives first. The next customer message opens another billable window inside the same visible thread. Customer and AI messages consume that window; replies from your team and system messages do not. Console preview traffic is never billed. Calls are metered in voice minutes instead.

This means Inbox and the customer widget can show one coherent history while Billing correctly reports two AI conversations after, for example, an eleventh exchange. Choosing Start over creates a genuinely new transcript, but it is not required for the meter to roll over.

Message roles #

The public transcript uses three roles.

Role Who
user The customer. What they typed, or what they said on the call.
model The agent. On a WhatsApp number shared with the WhatsApp Business app, a reply the owner sent from their phone also appears as model.
human A person on your team, replying through Live support or from a ticket.

There is no system role in a transcript, no assistant alias, and no tool-call messages. Most conversations contain only user and model; handle human too if you sync transcripts.

The operational trail #

Alongside those messages, the platform records an internal trail of what the agent did. On calls, that is which knowledge base it searched, which live API it called, which MCP tool it used, which action it took, and why it played a filler line while it was waiting. On WhatsApp, it records why the agent stayed quiet, for example while your team was handling the conversation.

That trail is deliberately excluded from the public transcript. GET /conversations/{id}/messages never returns trail rows, so a transcript you sync into your warehouse or show to a customer contains the conversation and nothing else.

In the console Inbox the trail is shown, rendered as thin rows rather than message bubbles. Lookups on calls are labelled by where the agent looked: Knowledge base, Live API, MCP tool, Action. You will also see why the agent spoke while thinking — while looking something up, covering a slow reply, the wait ran long, checking the caller was still there, ending the call after silence, call time limit reached.

Read the trail first when a call went wrong

Lookups are recorded on calls only, so this works for voice. It separates three bugs that look identical from the outside. No lookup at all means the agent answered from its own head — a knowledge gap. A lookup that returned the wrong passages means a retrieval problem. The right passages plus a wrong answer means a persona problem. See Testing and publishing.

The inbox #

Every conversation from every channel lands in one Inbox with its full transcript, tagged with the channel it came from. You do not check three places.

The Inbox is where you read and search transcripts, see the operational trail, and look at the photos and voice notes customers sent. On WhatsApp it is also where you step in: Take over opens a live-support session assigned to you, and when the agent has stepped back from a conversation, a banner says why and Resume AI now hands it back. Live transfers the agent starts itself arrive in Live support; outside live hours, the agent can create a ticket for follow-up. See Inbox.

Reading conversations from your systems #

Two endpoints, both requiring the conversations:read scope:

  • GET /conversations — the list, newest activity first, 25 per page by default and up to 100. Filter by agent_id. Walk pages with next_cursor until it comes back null.
  • GET /conversations/{id}/messages — the transcript, oldest first, 100 per page by default and up to 200.

An unknown or foreign conversation id returns 404 rather than an empty page, so a bad id can never look like a quiet conversation.

Internal fields are not projected — you will not see workspace ids, deployment ids, connection ids, or per-message metadata. What comes back is the conversation and its transcript.

For a scheduled sync into a warehouse, see Sync conversations and Pagination.

Contacts #

A contact is a person in your customer directory. It holds:

  • contact_id, and the workspace it belongs to
  • name, phone, email, and wa_id (their WhatsApp identifier)
  • tags
  • consent — separate permissions for voice, whatsapp, and email
  • last_seen_channel
  • created_at

Phone numbers are normalised on the way in. A number carrying its own country code is taken at its word; one without is read as a local number in your workspace's country, and rejected rather than completed if it is not a real number there. Email addresses are trimmed and lower-cased, so Priya@Example.com and priya@example.com are the same person. WhatsApp identifiers are reduced to digits.

How a person is identified #

There are exactly three match keys: email, phone, and WhatsApp ID. Every lookup is scoped to your workspace before anything else happens.

The resolution logic, in order:

  1. 1

    Look up each identity separately

    Whatever identities are known — an email, a phone number, a WhatsApp ID — each is queried against the directory on its own.

  2. 2

    If exactly one contact matches, enrich it

    The existing contact is updated with only the fields that were empty, plus last_seen_channel. A supplied value never overwrites a value that is already there.

  3. 3

    If a supplied identity conflicts, stop

    If the matched contact already has a different email — or a different phone — the operation fails with The contact already has a different {field}; manual review is required. rather than silently choosing one.

  4. 4

    If the identities match two different contacts, stop

    The supplied identities belong to different contacts; manual review is required. A phone number belonging to Rohit and an email belonging to Priya, arriving in the same request, is a data problem a human should look at — not something to resolve by guessing.

  5. 5

    Otherwise, create — if there is a name

    A new contact is created only when a name is known as well as an identity. It is tagged ai-captured when the agent captured it. Without a name, nothing is created.

There is no merge

Nothing in the platform merges two contacts into one. That is a deliberate refusal, not a missing feature — an automatic merge of two records that a conflict check already flagged as suspicious is how one customer's phone number ends up on another customer's file. Conflicts surface for a human to resolve.

Over the public API, POST /contacts upserts a contact and matches by email first, then phone. It requires at least one of the two — a name-only request is rejected with Provide an email or a phone number. Creating a new contact also needs a name: an email or phone that matches nobody, sent without a name, is rejected with The contact could not be resolved from those details. If the details belong to two different existing contacts you get a 409 with Those details belong to two different contacts.

How a contact accumulates across channels #

This is the payoff, and it is worth being precise about how it happens.

Priya messages your WhatsApp number about a delayed order. The agent resolves her by her WhatsApp identifier, finds nothing, and creates a contact carrying her wa_id and the phone number derived from it. Later in the chat she gives an email so she can be updated — that email lands on the same contact, because it was empty.

Three weeks later she opens your website and fills in the widget's lead form with that same email. The resolver matches the email to the existing contact and enriches it rather than creating a second one. Her name from the web form fills a field the WhatsApp conversation never captured.

She then calls your support number. The agent resolves her by caller ID, which matches the phone number already on the record.

One contact. Three channels. No manual reconciliation — and, importantly, no guessing: each step matched on an identifier the channel itself supplied, not on a name that happened to look similar.

Where contacts come from #

Source Behaviour
Web widget lead form Creates or enriches on submit. If it fails, the visitor never sees an error — the chat continues.
Agent opening a ticket on web chat Asks for a first name, then a callback number. Email is optional.
Agent opening a ticket on WhatsApp Uses the chat's own number, and the WhatsApp profile name when there is one; asks for a name only if it has none. Email is optional.
Agent opening a ticket on a call Uses the caller's number, asking for one only when caller ID is withheld. Email is never asked for on a call — speech recognition is not reliable enough to spell an address.
Agent booking an appointment Creates or enriches the contact for the person booked.
After a WhatsApp conversation When the conversation taught the agent something worth remembering and the customer's WhatsApp profile name is known, the contact is created then, so the memory has somewhere to live. See Contacts and customer memory.
POST /contacts Upsert from your own systems. Matches by email, then phone.
Console, and CSV import Manual entry, or a CSV with name, phone, and email columns.

Two honest gaps

Not every conversation creates a contact. A contact needs a name plus a phone number or email. A web visitor who chats anonymously and never identifies themselves leaves a conversation and no directory entry, and so does a caller who gives no name.

CSV import does not deduplicate. Rows are created directly, without the resolution logic above. Importing a list that overlaps your existing directory produces duplicates, and there is no merge to clean them up afterwards. Import into a fresh directory, or check for overlap first.

Open a contact in the console to see its Activity: the calls, web and WhatsApp conversations, tickets and appointments recorded for that person, newest first, each linking to the record itself. Chats from before 17 August 2026 are not linked to contacts. See Activity across channels.

What converges, and what does not #

A matched contact can also carry a private memory card: durable notes from your team, restrained facts picked up after conversations, and a recent-contact summary. That context follows the person across supported channels without becoming part of the public contact API or campaign audience data. See Contacts and customer memory.

Converges: every conversation from every channel into one inbox; every identified customer into one contact record; every channel's volume into one set of analytics.

Does not converge: conversations themselves. A WhatsApp thread and a web chat with the same person remain two conversations, linked only by the contact. There is no unified per-customer thread view.

Where to go next #

Read transcripts from code

Conversations API — list, fetch, and page through messages.

Upsert a customer

Contacts API — matching rules, conflicts, and what comes back.

Hand off to a human

Human handoff for a live specialist, or escalate to a ticket for asynchronous follow-up.

Get told instead of polling

Webhooks — message.received, conversation.started, ticket.created, and the rest.