A contact is a person your organisation talks to, across every channel. Conversations, notes, files and campaign sends all hang off the contact, so keeping one record per person — by linking identities and merging duplicates — is what gives agents the full history.
Endpoints
GET/contactsList contactsGET/contacts/{contactId}Retrieve a contactPOST/contactsCreate a contactPUT/contacts/{contactId}Update a contactGET/contacts/{contactId}/360Retrieve a contact’s 360° viewGET/contacts/{contactId}/timelineList a contact’s timelinePOST/contacts/{contactId}/identitiesLink an identityPOST/contacts/mergeMerge two contactsPOST/contacts/merges/{mergeId}/unmergeUnmerge contactsThe contact object
Every string field is nullable: a Web Chat visitor can be a contact with nothing but an id.
Attributes
idintegerUnique identifier of the contact in your organisation.externalIdstringnullableYour own identifier for this person, e.g. the customer ID in your CRM.displayNamestringnullableThe name agents see in the Inbox.phonestringnullablePhone number in international format (+9627…).emailstringnullableEmail address.avatarUrlstringnullablePublic URL of the contact’s picture.statusstringnullableFree-form lifecycle status you maintain, e.g.leadorcustomer.firstNamestringnullableDeprecated. Always present,nullwhen unset. UsedisplayName.lastNamestringnullableDeprecated. Always present,nullwhen unset. UsedisplayName.socialHandlesarray<object>The contact’s social handles. Only Retrieve a contact fills it; other endpoints return[].listsarray<object>Contact lists this person belongs to. Filled only by List contacts; every other endpoint returns[].
{
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": [
{
"id": 7,
"name": "VIP customers"
}
]
}
List contacts
Returns a page of contacts, oldest first (by id). Use q to search by name, phone, email, externalId or any linked identity.
contacts:read Offset paginatedQuery parameters
qstringFree-text search over name, phone, email,externalIdand identities.pageintegerZero-based page number. Default0.sizeintegerPage size. Default20; keep it at 100 or below.
Returns
200A page of results.
curl -X GET "https://api.cx.na6iq.com/api/v1/contacts?page=0&size=25" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts?page=0&size=25', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/contacts?page=0&size=25",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
{
"data": [
{
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": [
{
"id": 7,
"name": "VIP customers"
}
]
}
],
"page": 0,
"size": 25,
"totalElements": 1,
"totalPages": 1
}
Retrieve a contact
Returns one contact. lists is always [] here; socialHandles is filled.
contacts:readPath parameters
contactIdintegerrequiredThe contact’sid.
Returns
200The contact object.
Errors
- 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation.
curl -X GET "https://api.cx.na6iq.com/api/v1/contacts/42" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/42', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/contacts/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
{
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": []
}
Create a contact
Creates a contact. Every field is optional, but a contact you can reach needs at least a phone or an email. If a contact already has the same externalId, email or phone, that contact is updated and returned (still 201).
contacts:writeRequest body
externalIdstringYour own identifier for this person.displayNamestringDisplay name.phonestringPhone number in international format.emailstringEmail address.avatarUrlstringPublic picture URL.statusstringLifecycle status.propertiesJsonstringCustom attributes as a serialised JSON object, e.g."{\"plan\":\"gold\"}". Keys should match your custom attribute definitions.
Returns
201The contact object.
Errors
- 400
INVALID_REQUESTThe body is malformed or contains an unknown field. - 409
CONTACT_IDENTITY_CONFLICTThe email or phone belongs to another contact.
curl -X POST "https://api.cx.na6iq.com/api/v1/contacts" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"externalId": "crm-55102"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"externalId": "crm-55102"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"externalId": "crm-55102"
},
)
res.raise_for_status()
data = res.json()
{
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": []
}
Update a contact
Updates the contact with the fields you send. Fields you leave out or send as null keep their stored value; a field can’t be cleared this way.
contacts:writePath parameters
contactIdintegerrequiredThe contact’sid.
Request body
externalIdstringYour own identifier for this person.displayNamestringDisplay name.phonestringPhone number in international format.emailstringEmail address.avatarUrlstringPublic picture URL.statusstringLifecycle status.propertiesJsonstringCustom attributes as a serialised JSON object, e.g."{\"plan\":\"gold\"}". Keys should match your custom attribute definitions.
Returns
200The contact object.
Errors
- 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation. - 409
CONTACT_IDENTITY_CONFLICTThe email or phone belongs to another contact.
curl -X PUT "https://api.cx.na6iq.com/api/v1/contacts/42" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"status": "customer"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/42', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"status": "customer"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.put(
"https://api.cx.na6iq.com/api/v1/contacts/42",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"status": "customer"
},
)
res.raise_for_status()
data = res.json()
{
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": []
}
Retrieve a contact’s 360° view
Returns the contact together with its identities, channel profiles, lists, stats and recent activity — the data behind the contact panel in the Inbox.
contacts:readPath parameters
contactIdintegerrequiredThe contact’sid.
Query parameters
recentLimitintegerHow many recent conversations and campaign deliveries to include. Default5; clamped to1–20.messageLimitintegerHow many recent messages to include. Default10; clamped to1–50.
Returns
200An object with these top-level keys: `contact`, `identities`, `channelProfiles`, `lists`, `stats`, `recentConversations`, `recentCampaignDeliveries`, `recentMessages` and `socialHandles`.
Errors
- 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation.
curl -X GET "https://api.cx.na6iq.com/api/v1/contacts/42/360" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/42/360', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/contacts/42/360",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
{
"contact": {
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": []
},
"identities": [],
"channelProfiles": [],
"lists": [
{
"id": 7,
"name": "VIP customers"
}
],
"stats": {},
"recentConversations": [],
"recentCampaignDeliveries": [],
"recentMessages": [],
"socialHandles": []
}
List a contact’s timeline
Every message with this person across all channels, newest first. Keyset paginated: pass the last item’s id as afterId to get the next page.
contacts:read Keyset paginatedPath parameters
contactIdintegerrequiredThe contact’sid.
Query parameters
afterIdintegerReturn items older than this ID.0or omitted = newest.sizeintegerItems per page. Default50; clamped to1–100.
Returns
200An array of timeline items.
Errors
- 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation.
curl -X GET "https://api.cx.na6iq.com/api/v1/contacts/42/timeline?size=25" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/42/timeline?size=25', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.get(
"https://api.cx.na6iq.com/api/v1/contacts/42/timeline?size=25",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()
Link an identity
Attaches a channel identity (a phone number, email or channel user ID) to a contact. If another contact already owns that identity, nothing is changed: you get 202 and a merge suggestion is raised for a person to review.
contacts:writePath parameters
contactIdintegerrequiredThe contact’sid.
Request body
typestringrequiredIdentity type (case-insensitive).One of:EMAILPHONEEXTERNAL_IDCHANNEL_IDvaluestringrequiredThe identity value. APHONEorEMAILmust be a valid phone number or email.
Returns
200`200` when linked; `202` when a merge suggestion was raised instead. The body is `{ contact, merged, mergedFromContactId, suggestion }`; `merged` is always `false`.
Errors
- 400
INVALID_REQUEST`type` is not one of the four types, or `value` is not a valid phone number or email. - 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation.
curl -X POST "https://api.cx.na6iq.com/api/v1/contacts/42/identities" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "EMAIL",
"value": "layla.h@example.org"
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/42/identities', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"type": "EMAIL",
"value": "layla.h@example.org"
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/contacts/42/identities",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"type": "EMAIL",
"value": "layla.h@example.org"
},
)
res.raise_for_status()
data = res.json()
Merge two contacts
Merges one contact into another. The survivor keeps both records’ conversations and identities. The response’s mergeId lets you split them back with Unmerge, as long as nothing since makes that unsafe.
contacts:writeRequest body
survivorContactIdintegerrequiredThe contact that remains.absorbedContactIdintegerrequiredThe contact folded into the survivor.fieldsobjectWhich value to keep per field. Keys are onlyexternalId,displayName,phone,emailandavatarUrl(statuscan’t be picked). Each value must be one of the two records’ current values. A field you omit keeps the survivor’s value, or the absorbed contact’s when the survivor’s is blank.
Returns
200`mergeId` can be `null`.
Errors
- 400
MERGE_FIELD_NOT_A_CANDIDATEA `fields` value is not one of the two records’ current values. - 404
CONTACT_NOT_FOUNDNo contact with this ID in your organisation. - 409
CONTACT_MERGE_SAMEThe survivor and the absorbed contact are the same contact. - 409
CONTACT_ALREADY_MERGEDOne of the contacts has already been merged into another. - 409
CONTACT_MERGE_PREVIEW_MISMATCHThe contacts changed since the merge was previewed.
curl -X POST "https://api.cx.na6iq.com/api/v1/contacts/merge" \
-H "Authorization: Bearer $NATIQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"survivorContactId": 1842,
"absorbedContactId": 1907
}'
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/merge', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"survivorContactId": 1842,
"absorbedContactId": 1907
}),
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/contacts/merge",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
json={
"survivorContactId": 1842,
"absorbedContactId": 1907
},
)
res.raise_for_status()
data = res.json()
{
"survivor": {
"id": 1842,
"externalId": "crm-55102",
"displayName": "Layla Haddad",
"phone": "+962791234567",
"email": "layla@example.com",
"avatarUrl": null,
"status": "customer",
"firstName": null,
"lastName": null,
"socialHandles": [],
"lists": []
},
"absorbedContactId": 1907,
"merged": true,
"mergeId": 311
}
Unmerge contacts
Splits a merged contact back into the two original records.
contacts:writePath parameters
mergeIdintegerrequiredThemergeIdreturned by Merge two contacts.
Returns
200An object `{ survivor, restored, merge }`.
Errors
- 404
MERGE_NOT_FOUNDNo merge with this ID in your organisation. - 409
MERGE_NOT_REVERSIBLEThe merge was already reversed, has no snapshot, a newer merge involves either contact, or the absorbed contact is no longer `MERGED`.
curl -X POST "https://api.cx.na6iq.com/api/v1/contacts/merges/42/unmerge" \
-H "Authorization: Bearer $NATIQ_TOKEN"
const res = await fetch('https://api.cx.na6iq.com/api/v1/contacts/merges/42/unmerge', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NATIQ_TOKEN}`,
},
})
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error?.message ?? `HTTP ${res.status}`)
const data = await res.json()
import os, requests
res = requests.post(
"https://api.cx.na6iq.com/api/v1/contacts/merges/42/unmerge",
headers={"Authorization": f"Bearer {os.environ['NATIQ_TOKEN']}"},
)
res.raise_for_status()
data = res.json()