API reference
Customers
Customers and leads: create, look up, update and convert a lead into a customer.
Endpoints
-
GET
/customersList customers -
GET
/customers/{customer}Retrieve a customer -
POST
/customersCreate a customer -
PATCH
/customers/{customer}Update a customer -
POST
/customers/{customer}/convertConvert a lead into a customer -
GET
/customers/{customer}/activitiesCustomer timeline
List customers
/api/v1/customers
A list of customers and leads, newest first. Filter by status, type, email address or modification date, search with q and page with the cursor from meta.
- Scope
-
customers.read— Read customers and leads
Query parameters
| Name | Type | Description |
|---|---|---|
limit |
integer | Number of results per page. from 1 to 100 · default: 50 |
cursor |
string | The next_cursor or prev_cursor from meta of the previous response. |
q |
string | Search in name, email address, company name and phone number. at least 2 characters · at most 100 characters |
sort |
string | Sort by created_at or updated_at; a leading minus sign sorts descending. one of: -created_at, created_at, -updated_at, updated_at · default: -created_at |
filter[status] |
string | Only leads or only customers. one of: lead, customer |
filter[type] |
string | Only individuals or only businesses. one of: individual, business |
filter[email] |
string (email) | Exactly this email address (case-insensitive). |
filter[updated_since] |
string (date-time) | Only what changed since this moment: ISO 8601 with a time zone, for example 2026-09-14T10:15:00Z. Useful for synchronising. |
Example request
curl "https://app.klantly.com/api/v1/customers?limit=50&filter[status]=lead" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('GET', 'customers', [
'query' => [
'limit' => 50,
'filter[status]' => 'lead',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers?limit=50&filter[status]=lead', {
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
},
});
const { data } = await response.json();import os
import requests
response = requests.get(
"https://app.klantly.com/api/v1/customers",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
params={
"limit": 50,
"filter[status]": "lead"
},
)
data = response.json()["data"]Response 200
The response is a list with cursor pagination: data contains the objects, meta the pagination.
{
"data": [
{
"object": "customer",
"id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "business",
"status": "lead",
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"company_name": "De Vries Bouw",
"vat_number": "NL123456789B01",
"coc_number": "12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL",
"email_unsubscribed": false,
"converted_at": null,
"last_activity_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z",
"updated_at": "2026-09-14T10:15:00Z"
}
],
"meta": {
"limit": 50,
"next_cursor": "eyJpZCI6IjlkM2Y2YzFlIn0",
"prev_cursor": null
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid.
Retrieve a customer
/api/v1/customers/{customer}
A single customer by id. The response contains an ETag you can send in If-Match when updating.
- Scope
-
customers.read— Read customers and leads
Path parameters
| Name | Type | Description |
|---|---|---|
customer required |
string (uuid) | The id (UUID) of the customer. |
Example request
curl "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('GET', 'customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
},
});
const { data } = await response.json();import os
import requests
response = requests.get(
"https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "customer",
"id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "business",
"status": "lead",
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"company_name": "De Vries Bouw",
"vat_number": "NL123456789B01",
"coc_number": "12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL",
"email_unsubscribed": false,
"converted_at": null,
"last_activity_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z",
"updated_at": "2026-09-14T10:15:00Z"
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found.
Create a customer
/api/v1/customers
Creates a new lead. The email address is required and unique within your company: an existing address returns a validation error. Automations and the customer timeline work exactly as when creating a customer in Klantly itself.
- Scope
-
customers.write— Create and update customers and leads
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Body (JSON)
| Field | Type | Description |
|---|---|---|
email
required
|
string (email) | Email address; unique within your company. at most 255 characters · unique within your company |
type
optional
|
string | individual or business. one of: individual, business |
name
optional
|
string | Name of the contact person. can be empty (null) · at most 255 characters |
phone
optional
|
string | Phone number. can be empty (null) · at most 255 characters |
address
optional
|
string | Street and house number. can be empty (null) · at most 255 characters |
city
optional
|
string | City. can be empty (null) · at most 255 characters |
postal_code
optional
|
string | Postal code. can be empty (null) · at most 64 characters |
country
optional
|
string | Country. can be empty (null) · at least 2 characters · at most 2 characters |
company_name
optional
|
string | Company name, for a business customer. can be empty (null) · at most 255 characters |
vat_number
optional
|
string | VAT number. can be empty (null) · at most 20 characters |
coc_number
optional
|
string | Chamber of Commerce number. can be empty (null) · at most 30 characters |
Example request
curl -X POST "https://app.klantly.com/api/v1/customers" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-d '{
"email": "jan@example.com",
"name": "Jan de Vries",
"type": "business",
"company_name": "De Vries Bouw",
"city": "Utrecht"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'customers', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'email' => 'jan@example.com',
'name' => 'Jan de Vries',
'type' => 'business',
'company_name' => 'De Vries Bouw',
'city' => 'Utrecht',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
body: JSON.stringify({
"email": "jan@example.com",
"name": "Jan de Vries",
"type": "business",
"company_name": "De Vries Bouw",
"city": "Utrecht"
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/customers",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
json={
"email": "jan@example.com",
"name": "Jan de Vries",
"type": "business",
"company_name": "De Vries Bouw",
"city": "Utrecht"
},
)
data = response.json()["data"]Response 201
{
"data": {
"object": "customer",
"id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "business",
"status": "lead",
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"company_name": "De Vries Bouw",
"vat_number": "NL123456789B01",
"coc_number": "12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL",
"email_unsubscribed": false,
"converted_at": null,
"last_activity_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z",
"updated_at": "2026-09-14T10:15:00Z"
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid. -
422
unknown_field— The input contains an unknown field. -
415
unsupported_media_type— This format is not supported. -
413
payload_too_large— The request body is too large. -
403
limit_reached— The subscription limit has been reached. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Update a customer
/api/v1/customers/{customer}
Changes only the fields you send. The status does not change through this endpoint; use Convert a lead into a customer for that.
- Scope
-
customers.write— Create and update customers and leads
Send the ETag in If-Match and you will never accidentally overwrite a newer version.
Path parameters
| Name | Type | Description |
|---|---|---|
customer required |
string (uuid) | The id (UUID) of the customer. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
email
optional
|
string (email) | Email address; unique within your company. at most 255 characters · unique within your company |
type
optional
|
string | individual or business. one of: individual, business |
name
optional
|
string | Name of the contact person. can be empty (null) · at most 255 characters |
phone
optional
|
string | Phone number. can be empty (null) · at most 255 characters |
address
optional
|
string | Street and house number. can be empty (null) · at most 255 characters |
city
optional
|
string | City. can be empty (null) · at most 255 characters |
postal_code
optional
|
string | Postal code. can be empty (null) · at most 64 characters |
country
optional
|
string | Country. can be empty (null) · at least 2 characters · at most 2 characters |
company_name
optional
|
string | Company name, for a business customer. can be empty (null) · at most 255 characters |
vat_number
optional
|
string | VAT number. can be empty (null) · at most 20 characters |
coc_number
optional
|
string | Chamber of Commerce number. can be empty (null) · at most 30 characters |
Example request
curl -X PATCH "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+31 6 12345678",
"city": "Amersfoort"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('PATCH', 'customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', [
'json' => [
'phone' => '+31 6 12345678',
'city' => 'Amersfoort',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phone": "+31 6 12345678",
"city": "Amersfoort"
}),
});
const { data } = await response.json();import os
import requests
response = requests.patch(
"https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
json={
"phone": "+31 6 12345678",
"city": "Amersfoort"
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "customer",
"id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "business",
"status": "lead",
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"company_name": "De Vries Bouw",
"vat_number": "NL123456789B01",
"coc_number": "12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL",
"email_unsubscribed": false,
"converted_at": null,
"last_activity_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z",
"updated_at": "2026-09-14T10:15:00Z"
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
422
validation_failed— The input is invalid. -
422
unknown_field— The input contains an unknown field. -
415
unsupported_media_type— This format is not supported. -
413
payload_too_large— The request body is too large. -
404
not_found— Not found. -
412
precondition_failed— The record has been changed in the meantime.
Convert a lead into a customer
/api/v1/customers/{customer}/convert
Converts a lead into a customer. If it already is a customer, nothing changes and you simply get the customer back.
- Scope
-
customers.write— Create and update customers and leads
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
customer required |
string (uuid) | The id (UUID) of the customer. |
Example request
curl -X POST "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/convert" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f"$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/convert', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/convert', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/convert",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "customer",
"id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "business",
"status": "lead",
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"company_name": "De Vries Bouw",
"vat_number": "NL123456789B01",
"coc_number": "12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL",
"email_unsubscribed": false,
"converted_at": null,
"last_activity_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z",
"updated_at": "2026-09-14T10:15:00Z"
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found. -
422
idempotency_key_reused— This Idempotency-Key was already used for a different request. -
409
idempotency_in_progress— A request with this Idempotency-Key is still in progress.
Customer timeline
/api/v1/customers/{customer}/activities
The timeline of a customer, newest first: quotes, invoices, appointments, emails and more. References to linked records are under related.
- Scope
-
customers.read— Read customers and leads
Path parameters
| Name | Type | Description |
|---|---|---|
customer required |
string (uuid) | The id (UUID) of the customer. |
Query parameters
| Name | Type | Description |
|---|---|---|
limit |
integer | Number of results per page. from 1 to 100 · default: 50 |
cursor |
string | The next_cursor or prev_cursor from meta of the previous response. |
filter[type] |
string | Only activities of this type, for example quote_sent or invoice_created. at most 40 characters |
Example request
curl "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/activities" \
-H "Authorization: Bearer $KLANTLY_API_KEY"$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('GET', 'customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/activities');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/activities', {
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
},
});
const { data } = await response.json();import os
import requests
response = requests.get(
"https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/activities",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
)
data = response.json()["data"]Response 200
The response is a list with cursor pagination: data contains the objects, meta the pagination.
{
"data": [
{
"object": "customer_activity",
"id": "9d3f7c62-3e7a-4f9d-8c4b-5a6e7f8a9ba3",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"type": "quote_sent",
"title": null,
"description": null,
"related": {
"quote_id": null,
"invoice_id": null,
"appointment_id": null,
"work_order_id": null,
"task_id": null,
"deal_id": null,
"order_id": null,
"conversation_id": null
},
"happened_at": "2026-09-14T10:15:00Z",
"created_at": "2026-09-14T10:15:00Z"
}
],
"meta": {
"limit": 50,
"next_cursor": "eyJpZCI6IjlkM2Y2YzFlIn0",
"prev_cursor": null
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found. -
422
validation_failed— The input is invalid.
The object
All fields are always present; a field without a value is null.
| Field | Type | Description |
|---|---|---|
object |
string | Always "customer". |
id |
string (uuid) | Unique id (UUID). |
type |
string | individual or business. one of: individual, business |
status |
string | lead or customer. one of: lead, customer |
name |
string | Name of the contact person. can be empty (null) |
email |
string (email) | Email address; unique within your company. |
phone |
string | Phone number. can be empty (null) |
company_name |
string | Company name, for a business customer. can be empty (null) |
vat_number |
string | VAT number. can be empty (null) |
coc_number |
string | Chamber of Commerce number. can be empty (null) |
address |
string | Street and house number. can be empty (null) |
postal_code |
string | Postal code. can be empty (null) |
city |
string | City. can be empty (null) |
country |
string | Country. can be empty (null) |
email_unsubscribed |
boolean | Has unsubscribed from email. |
converted_at |
string (date-time) | When the lead became a customer. can be empty (null) |
last_activity_at |
string (date-time) | Last activity in Klantly. can be empty (null) |
created_at |
string (date-time) | Created at (UTC). |
updated_at |
string (date-time) | Last updated at (UTC). |