API: Bulk Import of Contacts via API

Last updated on Sep 22, 2026

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"
    }
  ]
}
  • index indicates the 0-based position of the row in the data array.
  • errors per row may be an array of validation messages OR a single string for key conflicts (duplicate, identifier × email × phone divergence, or AMBIGUOUS_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_create with upsert: true and the default settings, any field not sent will be overwritten with null, and JSONB attributes (additional_attributes, custom_attributes) are fully overwritten on each update.

Example: if a contact has email and phone_number registered, and you send an update with only phone_number, the email field will be deleted.

If 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 combine apply_tags: true + respect_sentinel: true + merge_jsonb: "shallow" + normalize_lookup: true for 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_add and tags_remove are silently ignored unless you send apply_tags: true (or api_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" without respect_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 set respect_sentinel: true and 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: true with 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: true in 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