Contacts
Manage contacts and contact information
Overview
Section titled “Overview”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.
List Contacts
Section titled “List Contacts”Retrieve a paginated list of contacts.
/api/contactsQuery Parameters
Section titled “Query Parameters”| 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.
Response
Section titled “Response”{ "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 }}Response Fields
Section titled “Response Fields”| 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. |
Get Contact
Section titled “Get Contact”Retrieve a single contact by ID.
/api/contacts/{id}Response
Section titled “Response”{ "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 Contact
Section titled “Create Contact”Create a new contact (or restore a soft-deleted one with the same phone number). Requires contacts:write.
/api/contactsRequest Body
Section titled “Request Body”{ "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 |
Response
Section titled “Response”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 Contact
Section titled “Update Contact”Update an existing contact.
/api/contacts/{id}Request Body
Section titled “Request Body”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) |
Response
Section titled “Response”Returns the full updated contact (same shape as Get Contact).
Contact Figures
Section titled “Contact Figures”Counts for the contacts you can see, as shown above the contacts list.
/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.
Workspace Limits
Section titled “Workspace Limits”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.
Import Contacts
Section titled “Import Contacts”Add or update up to 5,000 contacts in one request; send longer lists in several. Requires the contacts:import permission.
/api/contacts/importRequest Body
Section titled “Request Body”{ "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.
Response
Section titled “Response”{ "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 Contact
Section titled “Delete Contact”Delete a contact and all associated data.
/api/contacts/{id}Response
Section titled “Response”{ "status": "success", "data": null}Assign Contact
Section titled “Assign Contact”Assign a contact to a team member.
/api/contacts/{id}/assignRequest Body
Section titled “Request Body”{ "user_id": "uuid"}To unassign a contact, set user_id to null:
{ "user_id": null}Response
Section titled “Response”{ "status": "success", "data": { "message": "Contact assigned successfully", "assigned_user_id": "uuid" }}Update Contact Tags
Section titled “Update Contact Tags”Replace the full set of tags on a contact. Requires contacts:write.
/api/contacts/{id}/tagsRequest Body
Section titled “Request Body”{ "tags": ["vip", "billing"]}Response
Section titled “Response”{ "status": "success", "data": { "message": "Contact tags updated", "tags": ["vip", "billing"] }}Mark Contact Read
Section titled “Mark Contact Read”Mark all incoming messages from a contact as read (clears the unread badge). Used by the chat view when the conversation is open.
/api/contacts/{id}/mark-readResponse
Section titled “Response”{ "status": "success", "data": { "status": "ok" }}Contact Metadata
Section titled “Contact Metadata”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.
Supported Data Types
Section titled “Supported Data Types”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 |
Example
Section titled “Example”{ "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, andactiveas 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,Statuscolumns - Interests section — inline badges:
fitness,tech,travel
Nesting Depth
Section titled “Nesting Depth”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}.
Get Session Data
Section titled “Get Session Data”Retrieve chatbot session data for a contact, including collected variables and panel configuration.
/api/contacts/{id}/session-dataResponse
Section titled “Response”{ "status": "success", "data": { "session_id": "uuid", "flow_id": "uuid", "flow_name": "Customer Support Flow", "session_data": { "customer_name": "John Doe", "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} ] } ] } }}Response Fields
Section titled “Response Fields”| 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 |
Messaging & Notes
Section titled “Messaging & Notes”Contacts are the scope for conversation messages and internal notes.
Messages
Section titled “Messages”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 |
Conversation Notes
Section titled “Conversation Notes”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 |
Create Note Request Body
Section titled “Create Note Request Body”{ "content": "Customer prefers email follow-up."}Note Response
Section titled “Note Response”{ "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> }.
AI Reply Suggestions
Section titled “AI Reply Suggestions”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 |
Feedback
Section titled “Feedback”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.