Sync contacts
Create and update contacts with several identities, link them to your own user ids, find them again, and merge duplicates.
Last updated
Create a contact with identities
A contact is a person; identities are how messages find them: emails, phones and external ids (your CRM or user id). Custom attributes hold anything your team defines under Settings, Custom attributes.
curl -X POST "$KOMMIO_URL/contacts" \
-H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: crm-user-9917" \
-d '{"name": "Ana Souza", "company": "Acme", "emails": ["[email protected]"], "phones": ["+5511999990000"],
"external_ids": ["crm:9917"], "custom_attributes": {"plan": "team", "lifetime_value": 4200}}'const { data: contact } = await api.POST("/contacts", {
body: { name: "Ana Souza", company: "Acme", emails: ["[email protected]"], phones: ["+5511999990000"], external_ids: ["crm:9917"],
custom_attributes: { plan: "team", lifetime_value: 4200 } },
});contact = requests.post(f"{BASE}/contacts", headers={**H, "Idempotency-Key": "crm-user-9917"}, json={
"name": "Ana Souza", "company": "Acme", "emails": ["[email protected]"], "phones": ["+5511999990000"],
"external_ids": ["crm:9917"], "custom_attributes": {"plan": "team", "lifetime_value": 4200},
}).json()Response 201 Created
{ "id": 77, "name": "Ana Souza", "company": "Acme", "emails": ["[email protected]"], "phones": ["+5511999990000"], "external_ids": ["crm:9917"], "custom_attributes": { "plan": "team", "lifetime_value": 4200 }, "blocked": false, "created_at": "2026-09-22T09:10:00Z" }Contacts from your website chat
A website visitor becomes a contact only when there is a conversation. Loading a page with the chat creates nothing, whether the visitor is anonymous or signed in on your site: a signed-in visitor's id, name, email and attributes wait with their chat session, and the contact is created, or matched to the one that already carries their external id, with their first message, with a chat an agent starts from Visitors online, or with an email they leave in the offline form. A contact you create here with the same external id (external_ids) is the one a later sign-in or first message lands on, so a sync that wants every user in Contacts creates them through this API rather than waiting for a sign-in. The Contacts page shows people by default; anonymous website visitors, contacts with no email, phone or external id, have their own chip, and the API's audience parameter (people, visitors, all) makes the same split.
Find a contact
# by anything: name, email, phone, external id
curl "$KOMMIO_URL/contacts?q=crm:9917" -H "Authorization: Bearer $KOMMIO_KEY"
# one contact with its identities and history
curl "$KOMMIO_URL/contacts/77" -H "Authorization: Bearer $KOMMIO_KEY"
curl "$KOMMIO_URL/conversations?contact_id=77" -H "Authorization: Bearer $KOMMIO_KEY"const { data: found } = await api.GET("/contacts", { params: { query: { q: "crm:9917" } } });
const ana = found.items[0];
const { data: history } = await api.GET("/conversations", { params: { query: { contact_id: ana.id } } });Update, add identities, merge
# update name, company, avatar and custom attributes (the body replaces them: send the full set)
curl -X PUT "$KOMMIO_URL/contacts/77" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"name": "Ana Souza", "company": "Acme Ltd", "custom_attributes": {"plan": "business", "lifetime_value": 4200}}'
# add an identity
curl -X POST "$KOMMIO_URL/contacts/77/identities" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"kind": "email", "value": "[email protected]"}'
# merge a duplicate into 77 (conversations and identities move; the duplicate is kept as a pointer)
curl -X POST "$KOMMIO_URL/contacts/77/merge" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"source_id": 91}'await api.PUT("/contacts/{id}", { params: { path: { id: 77 } }, body: { name: "Ana Souza", company: "Acme Ltd", custom_attributes: { plan: "business", lifetime_value: 4200 } } });
await api.POST("/contacts/{id}/identities", { params: { path: { id: 77 } }, body: { kind: "email", value: "[email protected]" } });
await api.POST("/contacts/{id}/merge", { params: { path: { id: 77 } }, body: { source_id: 91 } });A nightly sync, end to end
import os, requests
BASE, KEY = os.environ["KOMMIO_URL"], os.environ["KOMMIO_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
def upsert(user):
ext = f"crm:{user['id']}"
found = requests.get(f"{BASE}/contacts", headers=H, params={"q": ext}).json()["items"]
fields = {"name": user["name"], "company": user["company"], "custom_attributes": {"plan": user["plan"]}}
if found:
# PUT replaces name, company, avatar and attributes; identities are managed separately
return requests.put(f"{BASE}/contacts/{found[0]['id']}", headers=H, json=fields)
body = {**fields, "emails": [user["email"]], "external_ids": [ext]}
return requests.post(f"{BASE}/contacts", headers={**H, "Idempotency-Key": ext}, json=body)
for user in crm.users_changed_since(yesterday):
r = upsert(user)
r.raise_for_status()Contacts you may see follow the same visibility as conversations, and contacts:export gates the CSV export under Contacts. Erasure (DELETE /contacts/{id} with contacts:delete) removes personal data and publishes contact.erased.
Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].