Reference
Contacts
Contacts are the core CRM record: leads, customers, and partners owned by the authenticated user. All endpoints scope by user ownership; cross-user access is rejected with 403/404 depending on policy.
List contact properties
Lists every property a contact can carry in this location: the native ones
(sent at the root of
the payload) and this location's custom fields (sent inside
custom_field_values, keyed by key, uuid or numeric id).
GET
/api/contacts/properties
It exists because otherwise the only way to know what you may send is to
read the source: /api/custom-fields lists only the custom ones, and the
native ones were written down nowhere. Each entry says where it goes —
location is root for top-level fields and custom_field_values for
the rest — and which ones cannot be written here, with the alternative
route when there is one.
curl --request GET \
--get "https://klozzo.com/api/contacts/properties" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/properties"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/properties';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/properties'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()
{
"data": [
{
"key": "first_name",
"label": "Nombre",
"type": "text",
"group": "native",
"location": "root",
"writable": true,
"required": true
}
]
}
-
dataobject[]-
keystring -
labelstring -
typestring -
groupstring -
locationstring -
writableboolean -
requiredboolean
-
List lead stages
Lists the lead stages this account defines — the values lead_stage_id
accepts.
GET
/api/lead-stages
Read this before you send lead_stage_id. Lead stages are a catalogue
each account defines for itself — not a fixed list shipped with the
product — so the numbers are different in every account and there is no
way to guess them. Call this endpoint once, keep the mapping, and send the
id:
{ "lead_stage_id": 1 }
null leaves the contact with no stage. An id from another account is
rejected with 422, which is the failure this endpoint exists to prevent.
They come back in the order the settings screen shows them (position),
so you can render your own picker without re-sorting. slug is stable
when somebody renames a stage — match on it if you keep a mapping in your
own system. contacts_count is informational and moves on its own.
meta.usage repeats the one-line instruction, so a caller that only ever
looks at the response body still finds out what to do with the id.
curl --request GET \
--get "https://klozzo.com/api/lead-stages" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/lead-stages"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/lead-stages';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/lead-stages'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()
{
"data": [
{
"id": 1,
"name": "Nuevo",
"slug": "nuevo",
"color": "#22c55e",
"position": 0,
"contacts_count": 42
},
{
"id": 2,
"name": "Contactado",
"slug": "contactado",
"color": "#3b82f6",
"position": 1,
"contacts_count": 17
},
{
"id": 3,
"name": "Calificado",
"slug": "calificado",
"color": null,
"position": 2,
"contacts_count": 4
}
],
"meta": {
"usage": "Manda el `id` en `lead_stage_id` al crear o actualizar un contacto: {\"lead_stage_id\": 1}. `null` lo deja sin estado."
}
}
-
dataobject[]-
idinteger -
namestring -
slugstring -
colorstring -
positioninteger -
contacts_countinteger
-
-
metaobject-
usagestring
-
{
"data": [],
"meta": {
"usage": "Manda el `id` en `lead_stage_id` al crear o actualizar un contacto: {\"lead_stage_id\": 1}. `null` lo deja sin estado."
}
}
-
dataarray -
metaobject-
usagestring
-
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
List contacts
Returns a paginated list of contacts owned by the authenticated user. Supports full-text search across name, email, phone, and company.
GET
/api/contacts
Query parameters
-
searchstringFragment matched against first/last name, email, phone, company.
Example:
ada -
contact_typestringFilter by type. One of
lead,customer,partner.Example:
lead -
sortstringColumn to sort by (e.g.
created_at,last_name). Defaults to newest first.Example:
created_at -
directionstringSort direction,
ascordesc.Example:
desc -
per_pageintegerRows per page. Default 25, maximum 100. See Lists, paging and filters.
Example:
25
curl --request GET \
--get "https://klozzo.com/api/contacts?search=ada&contact_type=lead&sort=created_at&direction=desc&per_page=25" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts"
);
const params = {
"search": "ada",
"contact_type": "lead",
"sort": "created_at",
"direction": "desc",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'search' => 'ada',
'contact_type' => 'lead',
'sort' => 'created_at',
'direction' => 'desc',
'per_page' => '25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts'
params = {
'search': 'ada',
'contact_type': 'lead',
'sort': 'created_at',
'direction': 'desc',
'per_page': '25',
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()
{
"data": [
{
"id": null,
"ulid": null,
"first_name": "Morgan",
"last_name": "Hirthe",
"full_name": "Morgan Hirthe",
"email": "dare.emelie@example.com",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550100",
"phone_e164": null,
"phone_country": "US",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "McLaughlin, Leuschke and Bauch",
"contact_type": "lead",
"timezone": "Asia/Famagusta",
"source": "import",
"source_number": null,
"date_of_birth": "2003-07-15",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": "2026-08-17T14:16:42+00:00",
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
},
{
"id": null,
"ulid": null,
"first_name": "Lucienne",
"last_name": "Haag",
"full_name": "Lucienne Haag",
"email": "lwisoky@example.net",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550101",
"phone_e164": null,
"phone_country": "US",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "Nitzsche-Ankunding",
"contact_type": "customer",
"timezone": "Asia/Ho_Chi_Minh",
"source": "import",
"source_number": null,
"date_of_birth": "2022-07-10",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": "2026-08-03T06:11:58+00:00",
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
}
],
"links": {
"first": "/?page=1",
"last": "/?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"links": [
{
"url": null,
"label": "« Previous",
"page": null,
"active": false
},
{
"url": "/?page=1",
"label": "1",
"page": 1,
"active": true
},
{
"url": null,
"label": "Next »",
"page": null,
"active": false
}
],
"path": "/",
"per_page": 25,
"to": 2,
"total": 2
}
}
-
dataobject[]-
idstring -
ulidstring -
first_namestring -
last_namestring -
full_namestring -
emailstring -
email_verified_atstring -
email_verification_excludedboolean -
email_verification_sent_atstring -
phonestring -
phone_e164string -
phone_countrystring -
phone_typestring -
has_whatsappstring -
whatsapp_checked_atstring -
whatsapp_history_synced_untilstring -
whatsapp_history_exhaustedboolean -
avatar_urlstring -
additional_emailsarray -
additional_phonesarray -
company_namestring -
contact_typestring -
timezonestring -
sourcestring -
source_numberstring -
date_of_birthstring -
dnd_allboolean -
dnd_emailboolean -
dnd_smsboolean -
dnd_callsboolean -
dnd_voicemailboolean -
dnd_gmbstring -
dnd_fb_messengerstring -
dnd_metaobject -
last_activity_atstring -
created_atstring -
updated_atstring -
owner_idstring -
lead_stage_idstring
-
-
linksobject-
firststring -
laststring -
prevstring -
nextstring
-
-
metaobject-
current_pageinteger -
frominteger -
last_pageinteger -
linksobject[]-
urlstring -
labelstring -
pagestring -
activeboolean
-
-
pathstring -
per_pageinteger -
tointeger -
totalinteger
-
Create a contact
POST
/api/contacts
Coming from a form, an ad callback or an automation? Use
POST /api/leadsinstead. This endpoint answers422and drops the whole request when the person already exists — including the campaign they came from and what they asked for this time./api/leadsnever rejects somebody who comes back.
Creates a new contact owned by the authenticated user. Use it when you are deliberately writing a record you know is new: a migration, an import script, an internal tool.
Server-side duplicate detection applies: if the location's
duplicate_mode = block, a 422 is returned when email or phone collides
with an existing contact in the same scope.
Body parameters
-
first_namestring requiredContact first name.
Example:
Ada -
last_namestringContact last name.
Example:
Lovelace -
emailstringPrimary email; unique within scope if duplicate-block is on.
Example:
ada@example.com -
phonestringE.164 with the country code —
+525555555555. A local number is only accepted together withphone_country; without either the request is rejected. Stored normalised to E.164.Example:
+525555555555 -
phone_countrystringISO-3166-1 alpha-2, e.g.
MX. Only needed whenphonehas no+country code; ignored when it does.Example:
MX -
phone_typestringOne of
mobile,home,work,other.Example:
mobile -
company_namestringExample:
Analytical Engines Ltd. -
company_idintegerThe <code>id</code> of an existing record in the companies table.
Example:
16 -
contact_typestringOne of
lead,customer,partner.Example:
lead -
lead_stage_idintegerThe <code>id</code> of an existing record in the lead_stages table.
Example:
16 -
timezonestringIANA timezone.
Example:
America/Mexico_City -
sourcestringFree-form acquisition source.
Example:
web -
channel_integration_idintegerThe <code>id</code> of an existing record in the channel_integrations table.
Example:
16 -
date_of_birthstringISO-8601 date.
Example:
1815-12-10 -
avatarstringImage, max 2MB. Only via multipart/form-data.
-
additional_emailsstring[]Up to 5 extra email addresses.
Example:
["architecto"] -
additional_phonesstring[]Up to 5 extra numbers, same rules as
phone. They share the singlephone_country, so send them in E.164 if they are from different countries.Example:
["architecto"] -
dnd_allbooleanMaster Do-Not-Disturb toggle.
Example:
false -
dnd_emailbooleanBlock email for this person. Independent of
dnd_all, which blocks everything.Example:
false -
dnd_smsbooleanBlock SMS for this person.
Example:
false -
dnd_callsbooleanBlock outbound calls for this person.
Example:
false -
dnd_voicemailbooleanBlock leaving voicemail for this person.
Example:
false -
dnd_gmbbooleanBlock Google Business Messages for this person.
Example:
false -
dnd_fb_messengerbooleanBlock Facebook Messenger for this person.
Example:
false -
auto_engagebooleanEnable automated engagement workflows.
Example:
true -
custom_field_valuesobjectMap of
custom_field_id => value.Example:
[]
curl --request POST \
"https://klozzo.com/api/contacts" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "first_name=Ada"\
--form "last_name=Lovelace"\
--form "email=ada@example.com"\
--form "phone=+525555555555"\
--form "phone_country=MX"\
--form "phone_type=mobile"\
--form "company_name=Analytical Engines Ltd."\
--form "company_id=16"\
--form "contact_type=lead"\
--form "lead_stage_id=16"\
--form "timezone=America/Mexico_City"\
--form "source=web"\
--form "channel_integration_id=16"\
--form "date_of_birth=1815-12-10"\
--form "additional_emails[]=architecto"\
--form "additional_phones[]=architecto"\
--form "dnd_all="\
--form "dnd_email="\
--form "dnd_sms="\
--form "dnd_calls="\
--form "dnd_voicemail="\
--form "dnd_gmb="\
--form "dnd_fb_messenger="\
--form "auto_engage=1"\
--form "avatar=@/private/var/folders/yw/tssyhl110hs29p29scc2m0jc0000gn/T/phpn370553gbibdcAe8NFE"
const url = new URL(
"https://klozzo.com/api/contacts"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('first_name', 'Ada');
body.append('last_name', 'Lovelace');
body.append('email', 'ada@example.com');
body.append('phone', '+525555555555');
body.append('phone_country', 'MX');
body.append('phone_type', 'mobile');
body.append('company_name', 'Analytical Engines Ltd.');
body.append('company_id', '16');
body.append('contact_type', 'lead');
body.append('lead_stage_id', '16');
body.append('timezone', 'America/Mexico_City');
body.append('source', 'web');
body.append('channel_integration_id', '16');
body.append('date_of_birth', '1815-12-10');
body.append('additional_emails[]', 'architecto');
body.append('additional_phones[]', 'architecto');
body.append('dnd_all', '');
body.append('dnd_email', '');
body.append('dnd_sms', '');
body.append('dnd_calls', '');
body.append('dnd_voicemail', '');
body.append('dnd_gmb', '');
body.append('dnd_fb_messenger', '');
body.append('auto_engage', '1');
body.append('avatar', document.querySelector('input[name="avatar"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'first_name',
'contents' => 'Ada'
],
[
'name' => 'last_name',
'contents' => 'Lovelace'
],
[
'name' => 'email',
'contents' => 'ada@example.com'
],
[
'name' => 'phone',
'contents' => '+525555555555'
],
[
'name' => 'phone_country',
'contents' => 'MX'
],
[
'name' => 'phone_type',
'contents' => 'mobile'
],
[
'name' => 'company_name',
'contents' => 'Analytical Engines Ltd.'
],
[
'name' => 'company_id',
'contents' => '16'
],
[
'name' => 'contact_type',
'contents' => 'lead'
],
[
'name' => 'lead_stage_id',
'contents' => '16'
],
[
'name' => 'timezone',
'contents' => 'America/Mexico_City'
],
[
'name' => 'source',
'contents' => 'web'
],
[
'name' => 'channel_integration_id',
'contents' => '16'
],
[
'name' => 'date_of_birth',
'contents' => '1815-12-10'
],
[
'name' => 'additional_emails[]',
'contents' => 'architecto'
],
[
'name' => 'additional_phones[]',
'contents' => 'architecto'
],
[
'name' => 'dnd_all',
'contents' => ''
],
[
'name' => 'dnd_email',
'contents' => ''
],
[
'name' => 'dnd_sms',
'contents' => ''
],
[
'name' => 'dnd_calls',
'contents' => ''
],
[
'name' => 'dnd_voicemail',
'contents' => ''
],
[
'name' => 'dnd_gmb',
'contents' => ''
],
[
'name' => 'dnd_fb_messenger',
'contents' => ''
],
[
'name' => 'auto_engage',
'contents' => '1'
],
[
'name' => 'avatar',
'contents' => fopen('/private/var/folders/yw/tssyhl110hs29p29scc2m0jc0000gn/T/phpn370553gbibdcAe8NFE', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts'
files = {
'first_name': (None, 'Ada'),
'last_name': (None, 'Lovelace'),
'email': (None, 'ada@example.com'),
'phone': (None, '+525555555555'),
'phone_country': (None, 'MX'),
'phone_type': (None, 'mobile'),
'company_name': (None, 'Analytical Engines Ltd.'),
'company_id': (None, '16'),
'contact_type': (None, 'lead'),
'lead_stage_id': (None, '16'),
'timezone': (None, 'America/Mexico_City'),
'source': (None, 'web'),
'channel_integration_id': (None, '16'),
'date_of_birth': (None, '1815-12-10'),
'additional_emails[]': (None, 'architecto'),
'additional_phones[]': (None, 'architecto'),
'dnd_all': (None, ''),
'dnd_email': (None, ''),
'dnd_sms': (None, ''),
'dnd_calls': (None, ''),
'dnd_voicemail': (None, ''),
'dnd_gmb': (None, ''),
'dnd_fb_messenger': (None, ''),
'auto_engage': (None, '1'),
'avatar': open('/private/var/folders/yw/tssyhl110hs29p29scc2m0jc0000gn/T/phpn370553gbibdcAe8NFE', 'rb')}
payload = {
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+525555555555",
"phone_country": "MX",
"phone_type": "mobile",
"company_name": "Analytical Engines Ltd.",
"company_id": 16,
"contact_type": "lead",
"lead_stage_id": 16,
"timezone": "America\/Mexico_City",
"source": "web",
"channel_integration_id": 16,
"date_of_birth": "1815-12-10",
"additional_emails": [
"architecto"
],
"additional_phones": [
"architecto"
],
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": false,
"dnd_fb_messenger": false,
"auto_engage": true,
"custom_field_values": []
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'multipart/form-data',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, files=files)
response.json()
{
"data": {
"id": null,
"ulid": null,
"first_name": "Christelle",
"last_name": "Bailey",
"full_name": "Christelle Bailey",
"email": "rowan.gulgowski@example.com",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550102",
"phone_e164": null,
"phone_country": "CO",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "Dach-Gaylord",
"contact_type": "customer",
"timezone": "Africa/Dakar",
"source": "import",
"source_number": null,
"date_of_birth": null,
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": null,
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
}
}
-
dataobject-
idstring -
ulidstring -
first_namestring -
last_namestring -
full_namestring -
emailstring -
email_verified_atstring -
email_verification_excludedboolean -
email_verification_sent_atstring -
phonestring -
phone_e164string -
phone_countrystring -
phone_typestring -
has_whatsappstring -
whatsapp_checked_atstring -
whatsapp_history_synced_untilstring -
whatsapp_history_exhaustedboolean -
avatar_urlstring -
additional_emailsarray -
additional_phonesarray -
company_namestring -
contact_typestring -
timezonestring -
sourcestring -
source_numberstring -
date_of_birthstring -
dnd_allboolean -
dnd_emailboolean -
dnd_smsboolean -
dnd_callsboolean -
dnd_voicemailboolean -
dnd_gmbstring -
dnd_fb_messengerstring -
dnd_metaobject -
last_activity_atstring -
created_atstring -
updated_atstring -
owner_idstring -
lead_stage_idstring
-
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "Ya existe un contacto con este correo electrónico.",
"errors": {
"email": [
"Ya existe un contacto con este correo electrónico."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
Fetch a contact
Returns one contact by id, with everything stored on it. Returns 404 if the contact does not belong to the authenticated user, 403 if policy denies access.
GET
/api/contacts/{id}
Path parameters
-
idinteger requiredThe contact ID.
Example:
42
curl --request GET \
--get "https://klozzo.com/api/contacts/42" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()
{
"data": {
"id": null,
"ulid": null,
"first_name": "Morgan",
"last_name": "Hirthe",
"full_name": "Morgan Hirthe",
"email": "imclaughlin@example.org",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550103",
"phone_e164": null,
"phone_country": "MX",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "O'Keefe Inc",
"contact_type": "lead",
"timezone": "Pacific/Tongatapu",
"source": "manual",
"source_number": null,
"date_of_birth": null,
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": "2026-08-09T18:01:18+00:00",
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
}
}
-
dataobject-
idstring -
ulidstring -
first_namestring -
last_namestring -
full_namestring -
emailstring -
email_verified_atstring -
email_verification_excludedboolean -
email_verification_sent_atstring -
phonestring -
phone_e164string -
phone_countrystring -
phone_typestring -
has_whatsappstring -
whatsapp_checked_atstring -
whatsapp_history_synced_untilstring -
whatsapp_history_exhaustedboolean -
avatar_urlstring -
additional_emailsarray -
additional_phonesarray -
company_namestring -
contact_typestring -
timezonestring -
sourcestring -
source_numberstring -
date_of_birthstring -
dnd_allboolean -
dnd_emailboolean -
dnd_smsboolean -
dnd_callsboolean -
dnd_voicemailboolean -
dnd_gmbstring -
dnd_fb_messengerstring -
dnd_metaobject -
last_activity_atstring -
created_atstring -
updated_atstring -
owner_idstring -
lead_stage_idstring
-
{
"message": "No query results for model [App\\Models\\Contact]."
}
Every error shares the same shape — message and errors.
See Errors.
Update a contact
Updates one contact by id, changing only the fields you send. Audit log records the diff between old and new values. Avatar is not updatable here (re-upload via store or future endpoint).
PUT
/api/contacts/{id}
Path parameters
-
idinteger requiredThe contact ID.
Example:
42
Body parameters
-
first_namestringExample:
Ada -
last_namestringExample:
Lovelace -
emailstringExample:
ada@example.com -
phonestringE.164 with the country code —
+525555555555. A local number is only accepted together withphone_country; without either the request is rejected. Stored normalised to E.164.Example:
+525555555555 -
phone_countrystringISO-3166-1 alpha-2, e.g.
MX. Only needed whenphonehas no+country code; ignored when it does.Example:
MX -
phone_typestringOne of
mobile,home,work,other.Example:
mobile -
additional_phonesstring[]Up to 5 extra numbers, same rules as
phone. They share the singlephone_country, so send them in E.164 if they are from different countries.Example:
["architecto"] -
company_namestringExample:
Analytical Engines Ltd. -
company_idintegerThe <code>id</code> of an existing record in the companies table.
Example:
16 -
contact_typestringOne of
lead,customer,partner.Example:
customer -
lead_stage_idintegerThe <code>id</code> of an existing record in the lead_stages table.
Example:
16 -
timezonestringIANA timezone.
Example:
America/Mexico_City -
sourcestringExample:
web -
date_of_birthstringISO-8601 date.
Example:
1815-12-10 -
dnd_allbooleanExample:
false -
dnd_emailbooleanBlock email for this person. Independent of
dnd_all, which blocks everything.Example:
false -
dnd_smsbooleanBlock SMS for this person.
Example:
false -
dnd_callsbooleanBlock outbound calls for this person.
Example:
false -
dnd_voicemailbooleanBlock leaving voicemail for this person.
Example:
false -
dnd_gmbbooleanBlock Google Business Messages for this person.
Example:
false -
dnd_fb_messengerbooleanBlock Facebook Messenger for this person.
Example:
false -
custom_field_valuesobjectMap of
custom_field_id => value.Example:
[]
curl --request PUT \
"https://klozzo.com/api/contacts/42" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"first_name\": \"Ada\",
\"last_name\": \"Lovelace\",
\"email\": \"ada@example.com\",
\"phone\": \"+525555555555\",
\"phone_country\": \"MX\",
\"phone_type\": \"mobile\",
\"additional_phones\": [
\"architecto\"
],
\"company_name\": \"Analytical Engines Ltd.\",
\"company_id\": 16,
\"contact_type\": \"customer\",
\"lead_stage_id\": 16,
\"timezone\": \"America\\/Mexico_City\",
\"source\": \"web\",
\"date_of_birth\": \"1815-12-10\",
\"dnd_all\": false,
\"dnd_email\": false,
\"dnd_sms\": false,
\"dnd_calls\": false,
\"dnd_voicemail\": false,
\"dnd_gmb\": false,
\"dnd_fb_messenger\": false,
\"custom_field_values\": []
}"
const url = new URL(
"https://klozzo.com/api/contacts/42"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+525555555555",
"phone_country": "MX",
"phone_type": "mobile",
"additional_phones": [
"architecto"
],
"company_name": "Analytical Engines Ltd.",
"company_id": 16,
"contact_type": "customer",
"lead_stage_id": 16,
"timezone": "America\/Mexico_City",
"source": "web",
"date_of_birth": "1815-12-10",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": false,
"dnd_fb_messenger": false,
"custom_field_values": []
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'first_name' => 'Ada',
'last_name' => 'Lovelace',
'email' => 'ada@example.com',
'phone' => '+525555555555',
'phone_country' => 'MX',
'phone_type' => 'mobile',
'additional_phones' => ['architecto'],
'company_name' => 'Analytical Engines Ltd.',
'company_id' => 16,
'contact_type' => 'customer',
'lead_stage_id' => 16,
'timezone' => 'America/Mexico_City',
'source' => 'web',
'date_of_birth' => '1815-12-10',
'dnd_all' => false,
'dnd_email' => false,
'dnd_sms' => false,
'dnd_calls' => false,
'dnd_voicemail' => false,
'dnd_gmb' => false,
'dnd_fb_messenger' => false,
'custom_field_values' => [],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42'
payload = {
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@example.com",
"phone": "+525555555555",
"phone_country": "MX",
"phone_type": "mobile",
"additional_phones": [
"architecto"
],
"company_name": "Analytical Engines Ltd.",
"company_id": 16,
"contact_type": "customer",
"lead_stage_id": 16,
"timezone": "America\/Mexico_City",
"source": "web",
"date_of_birth": "1815-12-10",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": false,
"dnd_fb_messenger": false,
"custom_field_values": []
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PUT', url, headers=headers, json=payload)
response.json()
{
"data": {
"id": null,
"ulid": null,
"first_name": "Christelle",
"last_name": "Bailey",
"full_name": "Christelle Bailey",
"email": "jdach@example.org",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550104",
"phone_e164": null,
"phone_country": "AR",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "Runte-Considine",
"contact_type": "lead",
"timezone": "America/Indiana/Tell_City",
"source": "web",
"source_number": null,
"date_of_birth": "1985-10-22",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": "2026-07-31T16:34:00+00:00",
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
}
}
-
dataobject-
idstring -
ulidstring -
first_namestring -
last_namestring -
full_namestring -
emailstring -
email_verified_atstring -
email_verification_excludedboolean -
email_verification_sent_atstring -
phonestring -
phone_e164string -
phone_countrystring -
phone_typestring -
has_whatsappstring -
whatsapp_checked_atstring -
whatsapp_history_synced_untilstring -
whatsapp_history_exhaustedboolean -
avatar_urlstring -
additional_emailsarray -
additional_phonesarray -
company_namestring -
contact_typestring -
timezonestring -
sourcestring -
source_numberstring -
date_of_birthstring -
dnd_allboolean -
dnd_emailboolean -
dnd_smsboolean -
dnd_callsboolean -
dnd_voicemailboolean -
dnd_gmbstring -
dnd_fb_messengerstring -
dnd_metaobject -
last_activity_atstring -
created_atstring -
updated_atstring -
owner_idstring -
lead_stage_idstring
-
Delete a contact
Soft-deletes one contact by id: it stops appearing anywhere in the CRM and is recoverable. All associated activity, conversations, and tags remain intact for audit purposes.
DELETE
/api/contacts/{id}
Path parameters
-
idinteger requiredThe contact ID.
Example:
42
curl --request DELETE \
"https://klozzo.com/api/contacts/42" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()
{
"message": "Contact deleted."
}
-
messagestring
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
Bulk action on contacts
Performs an action on multiple contacts in one call. Currently only delete
is supported. Bulk delete is permanent (no soft-delete) and emits a single
audit log entry covering all affected IDs.
POST
/api/contacts/bulk
Body parameters
-
actionstring requiredAction to perform. Currently only
delete.Example:
delete -
idsinteger[] requiredIDs of contacts to act on (min 1).
Example:
[16]
curl --request POST \
"https://klozzo.com/api/contacts/bulk" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"action\": \"delete\",
\"ids\": [
16
]
}"
const url = new URL(
"https://klozzo.com/api/contacts/bulk"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"action": "delete",
"ids": [
16
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/bulk';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'action' => 'delete',
'ids' => [16],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/bulk'
payload = {
"action": "delete",
"ids": [
16
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()
{
"message": "Bulk action completed."
}
-
messagestring
{
"message": "The selected action is invalid.",
"errors": {
"action": [
"The selected action is invalid."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
Check whether a contact's email really exists
Queues a deliverability check against the configured verification
provider and answers 202 straight away — the result is not ready when
this call returns. Read email_verification_status on the contact (or
wait for the contact.updated webhook) to find out how it went.
POST
/api/contacts/{contact_id}/verify-email
Use it before a first send to a list you did not collect yourself. A bounce rate above a few percent is what gets a sending domain blocked, and that is not something you undo in an afternoon.
The check costs money per address on most providers, so it is deliberately a call you make, never something the CRM does on its own.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
Body parameters
-
driverstringForce a specific provider instead of the account default. One of
zerobounce,mailgun,ses,null.Example:
zerobounce
curl --request POST \
"https://klozzo.com/api/contacts/42/verify-email" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"driver\": \"zerobounce\"
}"
const url = new URL(
"https://klozzo.com/api/contacts/42/verify-email"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"driver": "zerobounce"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/verify-email';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'driver' => 'zerobounce',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/verify-email'
payload = {
"driver": "zerobounce"
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()
{
"message": "Email verification queued.",
"contact_id": 42,
"status": "unverified"
}
-
messagestring -
contact_idinteger -
statusstring
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "Contact has no email to verify.",
"errors": {
"email": [
"Contact has no email to verify."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "Contact is excluded from email verification.",
"errors": {
"email": [
"Contact is excluded from email verification."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
Reassign contact owner
Transfers ownership of a contact to a different user. Pass owner_id = null
to unassign. Requires the reassignOwner policy.
PATCH
/api/contacts/{contact_id}/owner
Path parameters
-
contact_idinteger requiredThe contact ID.
Example:
42
Body parameters
-
owner_idintegerUser ID of the new owner. Set to
nullto unassign.Example:
7
curl --request PATCH \
"https://klozzo.com/api/contacts/42/owner" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"owner_id\": 7
}"
const url = new URL(
"https://klozzo.com/api/contacts/42/owner"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"owner_id": 7
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/owner';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'owner_id' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/owner'
payload = {
"owner_id": 7
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PATCH', url, headers=headers, json=payload)
response.json()
{
"data": {
"id": null,
"ulid": null,
"first_name": "Audra",
"last_name": "Crooks",
"full_name": "Audra Crooks",
"email": "rempel.chadrick@example.org",
"email_verified_at": null,
"email_verification_excluded": false,
"email_verification_sent_at": null,
"phone": "+12025550105",
"phone_e164": null,
"phone_country": "CO",
"phone_type": null,
"has_whatsapp": null,
"whatsapp_checked_at": null,
"whatsapp_history_synced_until": null,
"whatsapp_history_exhausted": false,
"avatar_url": null,
"additional_emails": [],
"additional_phones": [],
"company_name": "Gaylord and Sons",
"contact_type": "customer",
"timezone": "America/Eirunepe",
"source": "manual",
"source_number": null,
"date_of_birth": "1991-08-13",
"dnd_all": false,
"dnd_email": false,
"dnd_sms": false,
"dnd_calls": false,
"dnd_voicemail": false,
"dnd_gmb": null,
"dnd_fb_messenger": null,
"dnd_meta": [],
"last_activity_at": null,
"created_at": null,
"updated_at": null,
"owner_id": null,
"lead_stage_id": null
}
}
-
dataobject-
idstring -
ulidstring -
first_namestring -
last_namestring -
full_namestring -
emailstring -
email_verified_atstring -
email_verification_excludedboolean -
email_verification_sent_atstring -
phonestring -
phone_e164string -
phone_countrystring -
phone_typestring -
has_whatsappstring -
whatsapp_checked_atstring -
whatsapp_history_synced_untilstring -
whatsapp_history_exhaustedboolean -
avatar_urlstring -
additional_emailsarray -
additional_phonesarray -
company_namestring -
contact_typestring -
timezonestring -
sourcestring -
source_numberstring -
date_of_birthstring -
dnd_allboolean -
dnd_emailboolean -
dnd_smsboolean -
dnd_callsboolean -
dnd_voicemailboolean -
dnd_gmbstring -
dnd_fb_messengerstring -
dnd_metaobject -
last_activity_atstring -
created_atstring -
updated_atstring -
owner_idstring -
lead_stage_idstring
-
Add a follower to a contact
Followers receive notifications about activity on the contact. Idempotent: adding an existing follower is a no-op.
POST
/api/contacts/{contact_id}/followers
Path parameters
-
contact_idinteger requiredThe contact ID.
Example:
42
Body parameters
-
user_idinteger requiredID of the user to add as follower.
Example:
7
curl --request POST \
"https://klozzo.com/api/contacts/42/followers" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"user_id\": 7
}"
const url = new URL(
"https://klozzo.com/api/contacts/42/followers"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"user_id": 7
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/followers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'user_id' => 7,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/followers'
payload = {
"user_id": 7
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()
{
"message": "Follower added.",
"followers": [
7,
12
]
}
-
messagestring -
followersinteger[]
Remove a follower from a contact
Stops sending this user notifications about the contact. It does not touch ownership: the owner is who the contact belongs to, a follower is only somebody watching, and removing the last follower leaves the owner intact.
DELETE
/api/contacts/{contact_id}/followers/{user}
Idempotent — removing somebody who was not following answers 200 all the
same, so a sync that cannot remember what it already did is safe.
Path parameters
-
contact_idinteger requiredThe contact ID.
Example:
42 -
userinteger requiredID of the user to remove.
Example:
7
curl --request DELETE \
"https://klozzo.com/api/contacts/42/followers/7" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42/followers/7"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/followers/7';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/followers/7'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()
{
"message": "Follower removed.",
"followers": [
12
]
}
-
messagestring -
followersinteger[]
List a contact's do-not-disturb settings
Lists the do-not-disturb settings of one contact: one entry per channel this person has asked not to be reached on. An empty list means nothing is blocked — the absence of a row is the permission.
GET
/api/contacts/{contact_id}/dnd-settings
is_active is the field to read: a setting can exist and be inactive
because it was scheduled until a date that has already passed.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
curl --request GET \
--get "https://klozzo.com/api/contacts/42/dnd-settings" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42/dnd-settings"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/dnd-settings';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/dnd-settings'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()
{
"data": [
{
"id": 9,
"contact_id": 42,
"channel": "sms",
"enabled": true,
"scheduled_until": null,
"set_by": 3,
"set_at": "2026-08-23T17:04:00+00:00",
"is_active": true
}
]
}
-
dataobject[]-
idinteger -
contact_idinteger -
channelstring -
enabledboolean -
scheduled_untilstring -
set_byinteger -
set_atstring -
is_activeboolean
-
{
"message": "No query results for model [App\\Models\\Contact]."
}
Every error shares the same shape — message and errors.
See Errors.
Block a channel for a contact
Records that this person does not want to be reached on one channel. The CRM checks it before every send, so blocking a channel here stops messages queued by anything — a sequence, an automation, an agent typing in the inbox.
POST
/api/contacts/{contact_id}/dnd-settings
Sending the same channel twice updates the existing setting instead of
creating a second one, so a retry is safe.
Pass scheduled_until for a temporary block ("not until after the
holidays"); leave it out for an indefinite one. Every change is written to
the audit log with who made it — consent decisions have to be provable.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
Body parameters
-
channelstring requiredExample:
architecto -
enabledbooleanExample:
true -
scheduled_untilstringMust be a valid date. Must be a date after <code>now</code>.
Example:
2052-09-18
curl --request POST \
"https://klozzo.com/api/contacts/42/dnd-settings" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"channel\": \"architecto\",
\"enabled\": true,
\"scheduled_until\": \"2052-09-18\"
}"
const url = new URL(
"https://klozzo.com/api/contacts/42/dnd-settings"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"channel": "architecto",
"enabled": true,
"scheduled_until": "2052-09-18"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/dnd-settings';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'channel' => 'architecto',
'enabled' => true,
'scheduled_until' => '2052-09-18',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/dnd-settings'
payload = {
"channel": "architecto",
"enabled": true,
"scheduled_until": "2052-09-18"
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()
{
"data": {
"id": 9,
"contact_id": 42,
"channel": "sms",
"enabled": true,
"scheduled_until": null,
"set_by": 3,
"set_at": "2026-08-23T17:04:00+00:00",
"is_active": true
}
}
-
dataobject-
idinteger -
contact_idinteger -
channelstring -
enabledboolean -
scheduled_untilstring -
set_byinteger -
set_atstring -
is_activeboolean
-
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "The selected channel is invalid.",
"errors": {
"channel": [
"The selected channel is invalid."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
Unblock a channel for a contact
Removes the setting, which means the channel is open again. Use it when somebody opts back in — never to "clean up" a list, because a deleted block is a lost consent decision and the audit entry is the only trace left of it.
DELETE
/api/contacts/{contact_id}/dnd-settings/{dndSetting_id}
A setting that belongs to another contact answers 404, even if the id
exists.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42 -
dndSetting_idstring requiredThe setting's id, from the list endpoint.
Example:
9
curl --request DELETE \
"https://klozzo.com/api/contacts/42/dnd-settings/9" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42/dnd-settings/9"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/dnd-settings/9';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/dnd-settings/9'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()
Read a contact's custom field values
Returns the custom field values of one contact as a flat object keyed by
the field's key — the same keys you send when
writing. Fields the contact has never been given a value for are simply
absent; there is no null placeholder for them.
GET
/api/contacts/{contact_id}/custom-field-values
To find out which keys exist in this account, call
GET /api/contacts/properties (everything a contact can carry) or
GET /api/custom-fields (the custom ones with their type and options).
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
curl --request GET \
--get "https://klozzo.com/api/contacts/42/custom-field-values" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42/custom-field-values"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/custom-field-values';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/custom-field-values'
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()
{
"data": {
"presupuesto_mensual": "25000",
"canal_preferido": "whatsapp"
}
}
-
dataobject-
presupuesto_mensualstring -
canal_preferidostring
-
{
"message": "No query results for model [App\\Models\\Contact]."
}
Every error shares the same shape — message and errors.
See Errors.
Write a contact's custom field values
Writes the custom field values of one contact. Send the values you want to
set inside values, keyed by the field's
key, uuid or numeric id. Only the keys you send are touched —
this is a merge, not a replacement, so omitting a field leaves it alone
and there is no way to accidentally blank the rest of the record.
PUT
/api/contacts/{contact_id}/custom-field-values
Unlike POST /api/leads, an unknown key here is an error, not
something kept aside: this endpoint exists to write values on purpose, so
a typo is a bug worth stopping. You get a 422 naming every key that did
not match, and nothing is written.
Values are validated against the field's own type — a date field rejects
"mañana", a dropdown rejects an option that is not on its list.
Custom fields belong to the location, not to whoever created them: a field another agent added is yours to write too.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
Body parameters
-
valuesobject requiredThe values to set, keyed by field.
Example:
{"presupuesto_mensual":"25000"}
curl --request PUT \
"https://klozzo.com/api/contacts/42/custom-field-values" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"values\": {
\"presupuesto_mensual\": \"25000\"
}
}"
const url = new URL(
"https://klozzo.com/api/contacts/42/custom-field-values"
);
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"values": {
"presupuesto_mensual": "25000"
}
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/custom-field-values';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'values' => ['presupuesto_mensual' => '25000'],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/custom-field-values'
payload = {
"values": {
"presupuesto_mensual": "25000"
}
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('PUT', url, headers=headers, json=payload)
response.json()
{
"message": "Custom field values synced."
}
-
messagestring
{
"message": "This action is unauthorized."
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "El campo personalizado «presupesto» no existe o no está disponible en tu cuenta.",
"errors": {
"values.presupesto": [
"El campo personalizado «presupesto» no existe o no está disponible en tu cuenta."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
{
"message": "The values.fecha_de_alta is not a valid date.",
"errors": {
"values.fecha_de_alta": [
"The values.fecha_de_alta is not a valid date."
]
}
}
Every error shares the same shape — message and errors.
See Errors.
List a contact's activity timeline
Lists the activity of one contact, newest first: pages visited, forms submitted, calls, notes, tag changes, messages. It is the same feed the contact screen shows.
GET
/api/contacts/{contact_id}/activity
Paginated by cursor, not by page number. The timeline grows while you
read it, and a page number would either repeat or skip events as new ones
arrive. Follow next_cursor until it comes back null; do not build
?page=2 by hand.
Filter with event_type to sync one kind of thing — pass it more than
once for several. The payload shape depends on the event type, so read
event_type before reaching into payload.
Path parameters
-
contact_idstring requiredThe contact's id.
Example:
42
Query parameters
-
per_pageintegerRows per page. Default 25, maximum 100. See Lists, paging and filters.
Example:
25 -
cursorstringThe
next_cursorfrom the previous response.Example:
eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0 -
event_typestring[]Only these event types.
Example:
["page_visited","form_submitted"]
curl --request GET \
--get "https://klozzo.com/api/contacts/42/activity?per_page=25&cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0&event_type[]=page_visited&event_type[]=form_submitted" \
--header "Authorization: Bearer {YOUR_API_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
const url = new URL(
"https://klozzo.com/api/contacts/42/activity"
);
const params = {
"per_page": "25",
"cursor": "eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"event_type[0]": "page_visited",
"event_type[1]": "form_submitted",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://klozzo.com/api/contacts/42/activity';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {YOUR_API_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'cursor' => 'eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0',
'event_type[0]' => 'page_visited',
'event_type[1]' => 'form_submitted',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json
url = 'https://klozzo.com/api/contacts/42/activity'
params = {
'per_page': '25',
'cursor': 'eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0',
'event_type[0]': 'page_visited',
'event_type[1]': 'form_submitted',
}
headers = {
'Authorization': 'Bearer {YOUR_API_TOKEN}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, params=params)
response.json()
{
"data": [
{
"id": 881,
"event_type": "form_submitted",
"subject_type": "contact",
"subject_id": 42,
"actor_type": null,
"actor_id": null,
"location_id": 3,
"payload": {
"form_id": "newsletter",
"utm_source": "adwords"
},
"occurred_at": "2026-08-23T17:04:00+00:00"
}
],
"next_cursor": "eyJpZCI6ODgxfQ",
"prev_cursor": null
}
-
dataobject[]-
idinteger -
event_typestring -
subject_typestring -
subject_idinteger -
actor_typestring -
actor_idstring -
location_idinteger -
payloadobject-
form_idstring -
utm_sourcestring
-
-
occurred_atstring
-
-
next_cursorstring -
prev_cursorstring
{
"message": "No query results for model [App\\Models\\Contact]."
}
Every error shares the same shape — message and errors.
See Errors.