curl --request POST \
--url https://api.cloudtalk.io/v1/contacts/bulk \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"matchBy": "phone",
"contacts": [
{
"name": "Ana Lopez",
"phones": [
"+34600000001"
],
"countryCode": "ES"
},
{
"name": "Ben Ortiz",
"phones": [
"+34600000002"
],
"emails": [
"ben@"
]
}
]
}
'import requests
url = "https://api.cloudtalk.io/v1/contacts/bulk"
payload = {
"matchBy": "phone",
"contacts": [
{
"name": "Ana Lopez",
"phones": ["+34600000001"],
"countryCode": "ES"
},
{
"name": "Ben Ortiz",
"phones": ["+34600000002"],
"emails": ["ben@"]
}
]
}
headers = {
"Authorization": "Basic <encoded-value>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Basic <encoded-value>', 'Content-Type': 'application/json'},
body: JSON.stringify({
matchBy: 'phone',
contacts: [
{name: 'Ana Lopez', phones: ['+34600000001'], countryCode: 'ES'},
{name: 'Ben Ortiz', phones: ['+34600000002'], emails: ['ben@']}
]
})
};
fetch('https://api.cloudtalk.io/v1/contacts/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cloudtalk.io/v1/contacts/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'matchBy' => 'phone',
'contacts' => [
[
'name' => 'Ana Lopez',
'phones' => [
'+34600000001'
],
'countryCode' => 'ES'
],
[
'name' => 'Ben Ortiz',
'phones' => [
'+34600000002'
],
'emails' => [
'ben@'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Basic <encoded-value>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cloudtalk.io/v1/contacts/bulk"
payload := strings.NewReader("{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Basic <encoded-value>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cloudtalk.io/v1/contacts/bulk")
.header("Authorization", "Basic <encoded-value>")
.header("Content-Type", "application/json")
.body("{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cloudtalk.io/v1/contacts/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Basic <encoded-value>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"index": 0,
"status": "ok",
"operation": "created",
"contact": {
"id": "44212",
"type": "contact",
"name": "Ana Lopez",
"title": null,
"company": null,
"industry": null,
"website": null,
"address": null,
"city": null,
"state": null,
"zip": null,
"country": {
"code": "ES",
"name": "Spain"
},
"phones": [
"+34600000001"
],
"emails": [],
"tags": [],
"customFields": {},
"favoriteUserId": null,
"source": "api",
"createdAt": "2026-11-02T10:05:00Z",
"updatedAt": "2026-11-02T10:05:00Z",
"dashboardUrl": "https://dashboard.cloudtalk.io/menu/contacts/details/44212"
}
},
{
"index": 1,
"status": "error",
"error": {
"code": "VALIDATION.INVALID_FIELD",
"message": "contacts[1].emails[0] is not a valid email address.",
"target": "contacts[1].emails[0]"
}
}
],
"requestId": "c9856b94-db55-402b-a6ba-adb130351158"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"code": "UNAUTHORIZED",
"message": "You are not authorized to access this resource."
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}<string>{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}Bulk upsert contacts
Creates or updates 1 to 100 contacts in one call and returns one result per row, in request order. Each row is matched on matchBy: a row that matches an existing contact updates it, and any other row creates a contact. When a row matches several contacts, the most recently created one is updated and the others stay as they are, as on Create a contact. Rows in the same call that match each other end up as one contact, and each of those rows reports its id. When they create it, every one of those rows reports operation: created. Every row must carry a value it can be matched on, so retrying the same call updates the contacts it created instead of duplicating them. That is why this endpoint takes no Idempotency-Key.
Check each row. The call answers 200 even when some rows fail. A row with status: error carries an error with a code, a message and a target that names the row, for example contacts[1].emails[0]. Row error codes: VALIDATION.INVALID_FIELD (including a row with nothing to match on, a row whose customFields has more than 100 keys, a tagIds value that is not a tag id, and a favoriteUserId that is not a user id), CONTACTS.UNKNOWN_TAG (a tag id that names no tag in your account), CONTACTS.TAG_READ_ONLY, CONTACTS.UNKNOWN_CUSTOM_FIELD and CONTACTS.UNKNOWN_USER (a user id that names no user in your account). A failed row is not written. Text fields are cleaned up before their length is checked and they are stored, as on Create a contact: emojis and other characters that take 4 bytes in UTF-8, and control characters such as line breaks and tabs, become a space; repeated whitespace becomes one space; and spaces at the start and end are removed.
After a server error. Created rows are committed before updated rows, so a 5xx answer can leave some rows applied. Retry the same call with the same matchBy: rows already applied are matched and updated, not created twice.
Each ok row sends contact.created, or contact.updated when an existing contact changed. An updated row that changed nothing sends no webhook. CloudTalk also exports the contacts to your active CRM integrations.
curl --request POST \
--url https://api.cloudtalk.io/v1/contacts/bulk \
--header 'Authorization: Basic <encoded-value>' \
--header 'Content-Type: application/json' \
--data '
{
"matchBy": "phone",
"contacts": [
{
"name": "Ana Lopez",
"phones": [
"+34600000001"
],
"countryCode": "ES"
},
{
"name": "Ben Ortiz",
"phones": [
"+34600000002"
],
"emails": [
"ben@"
]
}
]
}
'import requests
url = "https://api.cloudtalk.io/v1/contacts/bulk"
payload = {
"matchBy": "phone",
"contacts": [
{
"name": "Ana Lopez",
"phones": ["+34600000001"],
"countryCode": "ES"
},
{
"name": "Ben Ortiz",
"phones": ["+34600000002"],
"emails": ["ben@"]
}
]
}
headers = {
"Authorization": "Basic <encoded-value>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Basic <encoded-value>', 'Content-Type': 'application/json'},
body: JSON.stringify({
matchBy: 'phone',
contacts: [
{name: 'Ana Lopez', phones: ['+34600000001'], countryCode: 'ES'},
{name: 'Ben Ortiz', phones: ['+34600000002'], emails: ['ben@']}
]
})
};
fetch('https://api.cloudtalk.io/v1/contacts/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cloudtalk.io/v1/contacts/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'matchBy' => 'phone',
'contacts' => [
[
'name' => 'Ana Lopez',
'phones' => [
'+34600000001'
],
'countryCode' => 'ES'
],
[
'name' => 'Ben Ortiz',
'phones' => [
'+34600000002'
],
'emails' => [
'ben@'
]
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Basic <encoded-value>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cloudtalk.io/v1/contacts/bulk"
payload := strings.NewReader("{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Basic <encoded-value>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cloudtalk.io/v1/contacts/bulk")
.header("Authorization", "Basic <encoded-value>")
.header("Content-Type", "application/json")
.body("{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cloudtalk.io/v1/contacts/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Basic <encoded-value>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"matchBy\": \"phone\",\n \"contacts\": [\n {\n \"name\": \"Ana Lopez\",\n \"phones\": [\n \"+34600000001\"\n ],\n \"countryCode\": \"ES\"\n },\n {\n \"name\": \"Ben Ortiz\",\n \"phones\": [\n \"+34600000002\"\n ],\n \"emails\": [\n \"ben@\"\n ]\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"index": 0,
"status": "ok",
"operation": "created",
"contact": {
"id": "44212",
"type": "contact",
"name": "Ana Lopez",
"title": null,
"company": null,
"industry": null,
"website": null,
"address": null,
"city": null,
"state": null,
"zip": null,
"country": {
"code": "ES",
"name": "Spain"
},
"phones": [
"+34600000001"
],
"emails": [],
"tags": [],
"customFields": {},
"favoriteUserId": null,
"source": "api",
"createdAt": "2026-11-02T10:05:00Z",
"updatedAt": "2026-11-02T10:05:00Z",
"dashboardUrl": "https://dashboard.cloudtalk.io/menu/contacts/details/44212"
}
},
{
"index": 1,
"status": "error",
"error": {
"code": "VALIDATION.INVALID_FIELD",
"message": "contacts[1].emails[0] is not a valid email address.",
"target": "contacts[1].emails[0]"
}
}
],
"requestId": "c9856b94-db55-402b-a6ba-adb130351158"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"code": "UNAUTHORIZED",
"message": "You are not authorized to access this resource."
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}<string>{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": [
{
"code": "<string>",
"message": "<string>",
"target": "<string>"
}
],
"target": "<string>"
},
"requestId": "<string>"
}Authorizations
HTTP Basic Authentication. Use your API Access Key ID as the username and your API Access Key Secret as the password. Generate keys in the CloudTalk Dashboard under Account -> Settings -> API Keys (https://dashboard.cloudtalk.io/menu/account/settings/API-keys).