Cloud Chat: Bulk Import of Contacts via API
To import or update multiple contacts at once via API in Cloud Chat, send a POST request to the endpoint below.
Want to import just one contact? Use the Individual Contact Import endpoint. The two endpoints have different body formats — do not mix them.
Endpoint
POST https://{{CLOUDCHAT_DOMAIN}}/api/v1/accounts/{{ACCOUNT_ID}}/contacts/bulk_create
Headers
| Header | Value |
|---|---|
content-type |
application/json |
api_access_token |
Your API token |
Request Body
The body uses an array data containing the contacts to be imported:
{
"upsert": true,
"data": [
{
"name": "Contact 1",
"email": "[email protected]",
"phone_number": "+5511999998888",
"identifier": "EXT-001",
"additional_attributes": {
"company": "Acme",
"city": "São Paulo"
},
"custom_attributes": {
"plan": "premium",
"source": "march-campaign"
}
},
{
"name": "Contact 2",
"phone_number": "+5511888887777"
}
]
}
Fields per contact
| Field | Required | Description |
|---|---|---|
phone_number |
At least 1 of the 3* | Phone number in international format with + |
email |
At least 1 of the 3* | Contact's email |
identifier |
At least 1 of the 3* | Unique external identifier |
name |
No | Contact's name |
additional_attributes |
No | Object with additional attributes (key-value). Fully overwritten on each update by default. |
custom_attributes |
No | Object with custom attributes defined by the account (key-value). Fully overwritten on each update by default. |
tags_add |
No | Array of tag names to add (requires apply_tags: true or api_version: "v2"). |
tags_remove |
No | Array of tag names to remove (requires apply_tags: true or api_version: "v2"). |
*Each contact must have at least one among phone_number, email, or identifier.
upsert Flag
| Value | Behavior |
|---|---|
false or absent |
Only creates new contacts. If any of the key fields (email, phone number, or identifier) already exists in the database, the row fails with a 422 error (it does not silently ignore). |
true |
Creates new contacts and updates existing ones. |
Example request (curl)
curl -X POST \
'https://{{CLOUDCHAT_DOMAIN}}/api/v1/accounts/{{ACCOUNT_ID}}/contacts/bulk_create' \
--header 'content-type: application/json' \
--header 'api_access_token: YOUR_TOKEN' \
--data '{
"upsert": true,
"data": [
{
"name": "Contact 1",
"email": "[email protected]",
"phone_number": "+5511999998888"
},
{
"name": "Contact 2",
"phone_number": "+5511888887777"
}
]
}'
Response
Success (HTTP 201)
{ "success": true }
Validation or conflict error (HTTP 422)
{
"success": false,
"errors": [
{
"index": 0,
"attributes": { "name": "Contact 1", "email": "[email protected]" },
"errors": ["Email is invalid"]
},
{
"index": 1,
"attributes": { "name": "Contact 2" },
"errors": "Duplicate: Contact with this phone number already exists"
}
]
}
indexindicates the 0-based position of the row in thedataarray.errorsper row may be an array of validation messages OR a single string for key conflicts (duplicate, identifier × email × phone divergence, orAMBIGUOUS_MATCH).
Empty body (HTTP 400)
Returned when data is null or an empty array.
Precautions with upsert
Attention — risk of data loss with
upsert: true:When updating an existing contact via
bulk_createwithupsert: trueand the default settings, any field not sent will be overwritten withnull, and JSONB attributes (additional_attributes,custom_attributes) are fully overwritten on each update.Example: if a contact has
phone_numberregistered, and you send an update with onlyphone_number, theIf you need partial updates without deleting other fields, either use the individual update endpoint:
PUT /api/v1/accounts/{{ACCOUNT_ID}}/contacts/{{CONTACT_ID}}or use the advanced policy parameters described below (
coalesce_nulls,merge_jsonb: "shallow",respect_sentinel).
Advanced behavior (optional policy parameters)
The endpoint accepts optional top-level parameters in the body to customize processing. Defaults preserve the original behavior (parity with prior versions).
| Parameter | Type | Default | Effect |
|---|---|---|---|
api_version |
string | "v1" |
"v1": parity with prior behavior. "v2": enables all new features (tags granular, sentinel __DELETE__, JSONB shallow merge, variant lookup). "v2-safe": conservative subset. |
apply_tags |
boolean | false |
Enables row-level processing of tags_add and tags_remove. |
respect_sentinel |
boolean | false |
Enables __DELETE__ semantics for scalar fields and JSONB keys. |
merge_jsonb |
string | "overwrite" |
"overwrite": replaces JSONB columns wholesale. "shallow": merges incoming keys with existing object. |
coalesce_nulls |
boolean | false |
Treats null values in the payload as "preserve existing" instead of overwriting with null. |
coerce_blank_jsonb |
boolean | false |
Treats empty strings inside JSONB attributes as a delete signal. |
normalize_lookup |
boolean | false |
Enables variant lookup when matching existing contacts (especially for Brazilian phone numbers). |
orphan_guard |
boolean | false |
Rejects payloads that would orphan an existing contact (i.e., remove its last strong identifier). |
Quick recipe for the new features: send
api_version: "v2"to enable everything at once, or combineapply_tags: true+respect_sentinel: true+merge_jsonb: "shallow"+normalize_lookup: truefor the same result with explicit flags.
api_version
{
"api_version": "v2",
"upsert": true,
"data": [
{ "email": "[email protected]", "name": "Updated Name" }
]
}
apply_tags with tags_add and tags_remove
{
"apply_tags": true,
"upsert": true,
"data": [
{
"email": "[email protected]",
"tags_add": ["vip", "premium"],
"tags_remove": ["pending"]
}
]
}
⚠️ Heads up:
tags_addandtags_removeare silently ignored unless you sendapply_tags: true(orapi_version: "v2"). The default preserves prior behavior. If the same tag appears in both lists, removal wins (operations are ordered: add first, then remove).
respect_sentinel with __DELETE__
{
"respect_sentinel": true,
"merge_jsonb": "shallow",
"upsert": true,
"data": [
{
"email": "[email protected]",
"name": "__DELETE__",
"custom_attributes": {
"favorite_color": "__DELETE__"
}
}
]
}
The literal value "__DELETE__" clears the field (NULL for scalars, key removal for JSONB). For JSONB, this only applies to top-level keys. If you legitimately use "__DELETE__" as a value, contact support before sending payloads.
merge_jsonb
{
"merge_jsonb": "shallow",
"upsert": true,
"data": [
{
"email": "[email protected]",
"custom_attributes": { "plan": "premium" }
}
]
}
With "shallow", only the keys you send are updated; other existing keys in custom_attributes and additional_attributes are preserved. With "overwrite" (default), the JSONB column is replaced entirely.
⚠️ Warning —
merge_jsonb: "shallow"withoutrespect_sentinel: true: in this combination there is no way to delete a key. If your client previously assumed that "empty value = delete", existing keys may keep their old values. To delete keys explicitly, also setrespect_sentinel: trueand send"__DELETE__"for those keys.
coalesce_nulls
{
"coalesce_nulls": true,
"upsert": true,
"data": [
{
"email": "[email protected]",
"phone_number": null,
"name": "New Name"
}
]
}
With coalesce_nulls: true, the null for phone_number is treated as "preserve existing" — only name is updated. Useful when your serializer always emits all fields.
coerce_blank_jsonb
{
"coerce_blank_jsonb": true,
"merge_jsonb": "shallow",
"upsert": true,
"data": [
{
"email": "[email protected]",
"custom_attributes": { "favorite_color": "" }
}
]
}
With coerce_blank_jsonb: true, empty strings inside JSONB are treated as a delete signal (the key is removed).
⚠️ Warning —
coerce_blank_jsonb: truewith payloads that include intentional empty strings: any empty string value in JSONB will be treated as delete. Do not enable this if any of your contacts legitimately store an empty string in a custom attribute — those keys will be removed instead of preserved.
normalize_lookup
{
"normalize_lookup": true,
"upsert": true,
"data": [
{ "phone_number": "+5511933334444", "name": "Maria" }
]
}
With normalize_lookup: true (or api_version: "v2"), the system matches contacts by phone number variants. If you send +5511933334444 and an existing contact is stored as +551133334444, the match still happens and the contact is updated rather than a duplicate being created. Useful to avoid duplication across formats with and without the leading 9 for Brazilian mobile numbers.
orphan_guard
{
"orphan_guard": true,
"respect_sentinel": true,
"upsert": true,
"data": [
{
"identifier": "EXT-001",
"email": "__DELETE__",
"phone_number": "__DELETE__"
}
]
}
With orphan_guard: true, payloads that would leave a contact without any strong identifier (email, phone number, identifier) are rejected.
⚠️ Warning —
orphan_guard: truein accounts with identifier-only contacts: if your account intentionally has contacts that exist with only one identifier (for example,identifier-only contacts created by an external system), an update that removes that single identifier will be rejected. Only enable this guard if you are sure every contact has at least one strong key after the update.
Difference between this endpoint and the individual import
This endpoint (bulk_create) |
Individual Import external_import |
|
|---|---|---|
| When to use | Import/update multiple contacts at once | Import/update 1 contact per call |
| Body format | {"upsert": true, "data": [{...}, {...}]} |
Simple flat JSON object |
| Upsert | Requires the flag "upsert": true |
Always active (automatic) |
| Null handling | Omitted fields are overwritten with null (use coalesce_nulls to opt out) |
Omitted fields are preserved |
| Variant phone lookup | Available with normalize_lookup: true or api_version: "v2" |
Exact lookup by phone_number only |
Do not mix formats! If you send a flat object (without
data: [...]) to this endpoint, it will not work. For individual contact import, use the individual import endpoint.
Where to find the configuration parameters
-
CLOUDCHAT_DOMAIN: The domain visible in the browser URL when accessing Cloud Chat
-
ACCOUNT_ID: In account settings
-
API_TOKEN: At
https://{{CLOUDCHAT_DOMAIN}}/app/accounts/{{ACCOUNT_ID}}/profile/settings