Contact management
List contacts
query Parameters
searchSearch by name, phone, or email (case-insensitive)
tagFilter by tag (JSONB containment)
pageper_pageList contacts › Responses
Paginated contacts. Note this endpoint wraps its rows in a pagination envelope; it does NOT return a bare array.
totalMatching contacts across all pages
page1-based page number
per_pagePage size actually applied, clamped to 100
total_pagesImport contacts from a CSV file
Multipart upload (field name file). Recognized columns (English or Indonesian header names): name, phone_number, email, channel_source, tags. A headerless file is read in that column order. Phone numbers are normalized to bare digits; rows whose phone or email already exists are skipped. Maximum 5000 data rows per request. Requires contacts.manage permission.
Import contacts from a CSV file › Responses
Import summary
importedNewly created contacts
skippedRows whose phone/email already existed
errorsPer-row failures, "Row N — reason"
Preview a CSV before importing (wizard step 1)
Parses raw CSV content and returns headers, up to 5 sample rows, and the total data-row count. Requires contacts.manage permission.
Preview a CSV before importing (wizard step 1) › Request Body
csv_contentRaw CSV text
Preview a CSV before importing (wizard step 1) › Responses
Parsed preview
headerssample_rowstotal_rowsImport contacts from CSV with explicit column mapping (wizard step 2)
column_mapping maps CSV header names to contact fields (name, phone_number, email, channel, tags). Rows with a phone number upsert on it; email-only rows match existing contacts case-insensitively by email. Enforces the tenant contact quota for newly created contacts. Maximum 5000 data rows per request. Requires contacts.manage permission.
Import contacts from CSV with explicit column mapping (wizard step 2) › Request Body
csv_contentRaw CSV text (first row must be the header)
CSV header name → contact field
file_nameOriginal file name, echoed into import history
Import contacts from CSV with explicit column mapping (wizard step 2) › Responses
Import summary
importedNewly created contacts
updatedExisting contacts refreshed by upsert
skippedRows without phone or email
errorsPer-row failures, "Row N — reason"
Get a contact by ID
path Parameters
idGet a contact by ID › Responses
Contact details
idchannel_sourcewhatsapp, instagram, or email
tagsJSON array of tag strings
created_atupdated_atphone_numbernameemailUpdate a contact
Update name, email, or tags. At least one field required.
path Parameters
idUpdate a contact › Request Body
nameemailtagsUpdate a contact › Responses
Updated contact
idchannel_sourcewhatsapp, instagram, or email
tagsJSON array of tag strings
created_atupdated_atphone_numbernameemailList conversations for a contact
path Parameters
idquery Parameters
pageper_pageList conversations for a contact › Responses
Conversations for this contact
idcontact_idstatusThe complete set of conversation states. Mirrors ConversationStatus in crates/omni-common/src/models/conversation.rs.
pending and expired were removed by migrations/20260326000039_simplify_conversation_statuses.sql (pending → open, expired → resolved) and snoozed was added by migrations/20260519000012_conversation_snooze.sql. Passing a removed value as a status filter is rejected with 400 before the handler runs, because the query struct deserializes straight into this enum.
A snoozed conversation is hidden from the inbox until snoozed_until passes, at which point it reopens automatically.
last_message_atunread_countcreated_atupdated_atcontact_channelwhatsapp, instagram, or email
assigned_agent_idassigned_agent_nameFull name of the assigned agent (null when unassigned)
contact_namecontact_phoneAll co-assignees (human agents and/or AI agent) attached to this conversation via the conversation_assignees junction, ordered by assignment time. assigned_agent_id remains the denormalized primary.