Klantly Developers

API reference

Customers

Customers and leads: create, look up, update and convert a lead into a customer.

Endpoints

List customers

GET /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

NameTypeDescription
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
curl "https://app.klantly.com/api/v1/customers?limit=50&filter[status]=lead" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$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'];
JavaScript
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();
Python
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.

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Retrieve a customer

GET /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

NameTypeDescription
customer required string (uuid) The id (UUID) of the customer.

Example request

cURL
curl "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$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'];
JavaScript
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();
Python
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

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Create a customer

POST /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)

FieldTypeDescription
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
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"
}'
PHP
$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'];
JavaScript
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();
Python
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

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Update a customer

PATCH /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

NameTypeDescription
customer required string (uuid) The id (UUID) of the customer.

Body (JSON)

FieldTypeDescription
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
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"
}'
PHP
$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'];
JavaScript
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();
Python
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

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Convert a lead into a customer

POST /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

NameTypeDescription
customer required string (uuid) The id (UUID) of the customer.

Example request

cURL
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"
PHP
$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'];
JavaScript
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();
Python
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

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

Customer timeline

GET /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

NameTypeDescription
customer required string (uuid) The id (UUID) of the customer.

Query parameters

NameTypeDescription
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
curl "https://app.klantly.com/api/v1/customers/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/activities" \
  -H "Authorization: Bearer $KLANTLY_API_KEY"
PHP
$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'];
JavaScript
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();
Python
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.

Example response
{
  "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

In addition, every endpoint can return the general errors, such as an invalid key or a reached limit. See all error codes.

The object

All fields are always present; a field without a value is null.

FieldTypeDescription
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).