Skip to content
ReferenceContacts

Contacts

Manage contacts and contact information

DevelopersUpdated

Contacts represent WhatsApp users you communicate with. Each contact stores their phone number, profile information, and conversation history.

Contacts belong to the workspace, not to a WhatsApp number: there is one contact per phone number, shared by every number the workspace connects. whatsapp_account only records the number the contact last talked to, which replies go from by default.

Retrieve a paginated list of contacts.

GET/api/contacts
Parameter Type Description
page integer Page number (default: 1)
limit integer Page size, 1–100. Default 50.
search string Search by profile name (case-insensitive) or phone number. Truncated at 1000 characters.
tags string Comma-separated tags; matches contacts having ANY of them

Contacts are ordered by last_message_at DESC (nulls last), then created_at DESC.

{
"status": "success",
"data": {
"contacts": [
{
"id": "uuid",
"phone_number": "1234567890",
"name": "John",
"profile_name": "John",
"avatar_url": "https://...",
"status": "active",
"tags": ["vip", "billing"],
"metadata": { "custom_field": "value" },
"last_message_at": "2024-01-01T12:00:00Z",
"last_message_preview": "See you tomorrow!",
"unread_count": 2,
"assigned_user_id": "uuid",
"whatsapp_account": "Main Business",
"last_inbound_at": "2024-01-01T11:00:00Z",
"service_window_open": true,
"marketing_opt_out": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
],
"total": 100,
"page": 1,
"limit": 50
}
}
Field Type Description
whatsapp_account string The name (not a UUID) of the WhatsApp number the contact last talked to; empty if they haven’t yet. Messages go from it unless you pick another.
assigned_user_id uuid The agent the contact is assigned to, if any
unread_count integer Number of unread incoming messages
last_message_preview string Preview text of the most recent message
service_window_open boolean True if the customer messaged within the last 24 hours (last_inbound_at)
marketing_opt_out boolean Whether the contact has opted out of marketing messages
country string ISO country of the number (IN), for showing a flag. Left out when phone numbers are masked
status string Always the literal "active" — contacts have no status column.
name string Mirrors profile_name; both hold the same value.

Retrieve a single contact by ID.

GET/api/contacts/{id}
{
"status": "success",
"data": {
"id": "uuid",
"phone_number": "1234567890",
"name": "John",
"profile_name": "John",
"avatar_url": "https://...",
"status": "active",
"tags": ["vip"],
"assigned_user_id": "uuid",
"whatsapp_account": "Main Business",
"metadata": {
"custom_field": "value"
},
"unread_count": 0,
"last_message_at": "2024-01-01T12:00:00Z",
"last_message_preview": "Thanks!",
"service_window_open": true,
"marketing_opt_out": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}

Create a new contact (or restore a soft-deleted one with the same phone number). Requires contacts:write.

POST/api/contacts
{
"phone_number": "+1234567890",
"profile_name": "John Doe",
"whatsapp_account": "Main Business",
"tags": ["vip"],
"metadata": {
"custom_field": "value"
}
}
Field Type Required Description
phone_number string Yes Phone number with the country code. +91 98765-43210 and 0091 9876543210 are both stored as 919876543210; an existing contact with the same number (however it was written) is returned as a conflict
profile_name string No Display name for the contact
whatsapp_account string No Name of the WhatsApp number to send from by default. Usually left out: it’s set when the contact writes in
tags string[] No Tags to attach to the contact
metadata object No Freeform custom data

Returns the created contact in the same shape as Get Contact.

When the workspace has reached its contact limit, adding a contact (or bringing back a deleted one) answers 403 with the limit in data.contact_limit.

Update an existing contact.

PUT/api/contacts/{id}

Only the fields you send are updated.

{
"profile_name": "John Smith",
"whatsapp_account": "Main Business",
"tags": ["vip", "billing"],
"metadata": {
"custom_field": "updated_value"
},
"assigned_user_id": "uuid",
"clear_assigned_agent": false
}
Field Type Description
profile_name string Display name
whatsapp_account string Name of the WhatsApp number to send from by default
tags string[] Replaces the contact’s tags
metadata object Replaces the metadata object
assigned_user_id uuid Assign the contact to an agent
clear_assigned_agent boolean Set true to unassign the contact (takes precedence over assigned_user_id)

Returns the full updated contact (same shape as Get Contact).

Counts for the contacts you can see, as shown above the contacts list.

GET/api/contacts/stats
{
"status": "success",
"data": {
"total": 1240,
"active_today": 37,
"new_this_week": 112,
"opted_out": 9,
"contact_limit": 5000,
"workspace_contacts": 1240,
"import_limit": 50000
}
}

active_today counts contacts who wrote in the last 24 hours (their reply window is open); opted_out counts those who opted out of marketing messages. contact_limit and import_limit are the workspace’s limits (null when unlimited); with a contact limit, workspace_contacts is how many contacts the whole workspace has against it.

A super administrator sets these per workspace, on the workspace’s Limits tab in platform administration. Each limit uses the platform default until set, and can be set to a number or to unlimited. Lowering a limit keeps what the workspace already has and only stops it adding more.

Limit Default What it stops
contacts Unlimited Adding contacts by hand or by import once the workspace has this many. Messaging never stops at it: customers who write in, the send-message API, campaigns and store automations still create the contact they message
broadcast_messages_per_month Unlimited Sending campaign messages once the workspace has sent this many in the calendar month (UTC). Every message sent counts, retries too; running campaigns pause with the reason. See Campaigns
contacts_per_import 50,000 Importing a list with more contacts than this in one go. The app refuses such a file and asks for it to be split; a single request larger than the limit answers 400

Super administrators read and change them with GET /api/admin/organizations/{id}/limits and PUT /api/admin/organizations/{id}/limits (body {"key": "contacts", "mode": "custom", "limit": 5000}; mode is default, unlimited or custom). Every change is audit-logged.

Add or update up to 5,000 contacts in one request; send longer lists in several. Requires the contacts:import permission.

POST/api/contacts/import
{
"contacts": [
{ "phone_number": "+91 98765 43210", "name": "Asha Rao", "tags": ["VIP"] },
{ "phone_number": "+44 7700 900123" }
],
"tags": ["Imported Oct 8"],
"update_existing": true,
"default_country_code": "91"
}
Field Type Description
contacts object[] phone_number (required), name, tags and marketing_opt_out for each contact. An opt-out is always kept: an import adds one but never clears one
tags string[] Added to every contact in the list
update_existing boolean Default true: contacts you already have get the row’s name and gain its tags. Tags are only ever added
default_country_code string Added to numbers written without + or 00 that have 10 digits or fewer. Without it, such numbers are reported as invalid
dry_run boolean Work out what the import would do (the same counts as below) with reads only: nothing is saved or locked. The app uses it to preview an import

Numbers are matched to existing contacts however they were stored, so importing never duplicates a contact. A deleted contact is brought back. Repeats within the list are merged into the first. New tags are added to the workspace’s tag list; a tag that exists with different capitals takes the existing spelling.

{
"status": "success",
"data": {
"created": 1,
"updated": 1,
"unchanged": 0,
"duplicates": 0,
"invalid": 0
}
}

invalid_samples and errors (such as Row 3: "call me" isn't a phone number) are included when some rows were skipped, and new_tags lists tags the import adds to the workspace’s tag list.

When the workspace reaches its contact limit, new contacts stop there: over_limit counts those left out and contact_limit gives the limit. Contacts you already have are still updated. Importing the same list again after the limit is raised adds the rest without duplicating anything.

Delete a contact and all associated data.

DELETE/api/contacts/{id}
{
"status": "success",
"data": null
}

Assign a contact to a team member.

PUT/api/contacts/{id}/assign
{
"user_id": "uuid"
}

To unassign a contact, set user_id to null:

{
"user_id": null
}
{
"status": "success",
"data": {
"message": "Contact assigned successfully",
"assigned_user_id": "uuid"
}
}

Replace the full set of tags on a contact. Requires contacts:write.

PUT/api/contacts/{id}/tags
{
"tags": ["vip", "billing"]
}
{
"status": "success",
"data": {
"message": "Contact tags updated",
"tags": ["vip", "billing"]
}
}

Mark all incoming messages from a contact as read (clears the unread badge). Used by the chat view when the conversation is open.

POST/api/contacts/{id}/mark-read
{
"status": "success",
"data": { "status": "ok" }
}

The metadata field is a freeform JSON object that can hold any structured data. It is displayed in the Contact Info panel alongside tags and session data.

Different value types are rendered differently in the panel:

Type Display
String, number Key-value row
Boolean Yes / No badge
Object Collapsible section with key-value rows
Array of objects Collapsible table with column headers
Array of primitives Inline badges
{
"metadata": {
"plan": "premium",
"age": 30,
"active": true,
"address": {
"city": "Mumbai",
"state": "Maharashtra",
"zip": "400001"
},
"orders": [
{ "id": "ORD-001", "amount": 1500, "status": "delivered" },
{ "id": "ORD-002", "amount": 2300, "status": "pending" }
],
"interests": ["fitness", "tech", "travel"]
}
}

This renders as:

  • General section — plan, age, and active as key-value rows (boolean shown as a badge)
  • Address section — collapsible key-value pairs for city, state, zip
  • Orders section — collapsible table with Id, Amount, Status columns
  • Interests section — inline badges: fitness, tech, travel

The panel supports one level of nesting. Top-level keys are organized as follows:

Top-level value Rendered as
Primitive (string, number, boolean) Row in the General section
Object Its own collapsible section with key-value rows
Array of objects Its own collapsible section with a table
Array of primitives Its own collapsible section with badges

Values inside a nested object or array are always displayed as flat text. If a nested object contains another object, the inner value is shown as a raw JSON string. For example:

{
"metadata": {
"address": {
"city": "Mumbai",
"location": { "lat": 19.07, "lng": 72.87 }
}
}
}

Here city displays as Mumbai, but location displays as {"lat":19.07,"lng":72.87}.

Retrieve chatbot session data for a contact, including collected variables and panel configuration.

GET/api/contacts/{id}/session-data
{
"status": "success",
"data": {
"session_id": "uuid",
"flow_id": "uuid",
"flow_name": "Customer Support Flow",
"session_data": {
"customer_name": "John Doe",
"customer_email": "[email protected]",
"order_id": "ORD-12345",
"order_status": "shipped"
},
"panel_config": {
"sections": [
{
"id": "customer",
"label": "Customer Info",
"columns": 1,
"collapsible": true,
"default_collapsed": false,
"order": 1,
"fields": [
{"key": "customer_name", "label": "Name", "order": 1},
{"key": "customer_email", "label": "Email", "order": 2}
]
},
{
"id": "order",
"label": "Order Details",
"columns": 2,
"collapsible": true,
"default_collapsed": true,
"order": 2,
"fields": [
{"key": "order_id", "label": "Order ID", "order": 1},
{"key": "order_status", "label": "Status", "order": 2}
]
}
]
}
}
}
Field Type Description
session_id uuid The chatbot session ID
flow_id uuid The flow that collected the data
flow_name string Name of the flow
session_data object Key-value pairs of collected variables
panel_config object Panel display configuration from the flow

Contacts are the scope for conversation messages and internal notes.

Messages are sent and retrieved under the contact resource. See the Messages API for full request/response details.

Method Path Description
GET /api/contacts/{id}/messages List messages for a contact (paginated)
POST /api/contacts/{id}/messages Send a message to the contact
POST /api/contacts/{id}/messages/{message_id}/reaction Add or remove an emoji reaction on a message

Internal, contact-scoped notes visible to agents (never sent to the customer). Requires chat:read to list and chat:write to modify.

Method Path Description
GET /api/contacts/{id}/notes List notes for a contact (cursor-paginated, oldest first)
POST /api/contacts/{id}/notes Create a note
PUT /api/contacts/{id}/notes/{note_id} Update a note
DELETE /api/contacts/{id}/notes/{note_id} Delete a note
{
"content": "Customer prefers email follow-up."
}
{
"status": "success",
"data": {
"id": "uuid",
"contact_id": "uuid",
"created_by_id": "uuid",
"created_by_name": "Admin User",
"content": "Customer prefers email follow-up.",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-01T12:00:00Z"
}
}

The list endpoint returns { "notes": [...], "total": <n>, "has_more": <bool> }.

Drafts of the agent’s next reply, written by AI from the conversation, the workspace’s AI instructions and knowledge, and the customer’s memory. Nothing is sent: the agent picks one, edits it if needed and sends it as usual. Requires chat:write and access to the contact (agents only their own chats). The workspace needs the AI reply suggestions component and AI replies and knowledge allocated; each call is metered as AI usage (suggest), at most 120 per agent per hour by default. An agent limited to the current conversation gets drafts from it only (none before a conversation on the contact’s number has started), and not from the customer’s memory.

Method Path Description
POST /api/contacts/{id}/ai/suggestions Write 1–3 drafts of the next reply
POST /api/ai/events/{event_id}/feedback Say whether one of them was sent

The AI reads the last 20 messages. The request body is empty.

{
"status": "success",
"data": {
"suggestions": ["It ships tomorrow; you'll get the tracking link by WhatsApp.", "Let me check with the warehouse and get back to you today."],
"event_id": "uuid"
}
}
Status When
400 The customer hasn’t written yet: nothing to reply to
402 The wallet has no credit left for AI, or the monthly AI limit is reached
403 AI reply suggestions (or AI) are off for the workspace
404 Contact not found, or not one the agent may open
429 Too many suggestions asked for; try again later
502 The AI’s answer couldn’t be read, was cut off or empty (not charged); try again
503 AI isn’t available right now

Send {"used": true, "edit_distance": 12} when the agent sends one of the drafts (edit_distance, optional: how many characters they changed first, 0 to 4,096), {"used": false} when they dismiss them. Only the agent who asked can answer, once; true replaces an earlier false. Returns 404 for an unknown event or another agent’s.

GET /api/ai/status says "suggestions": true when the member can ask for them: the workspace has them and AI on, and the member has chat:write.