API reference
Appointments
Appointments in the calendar: schedule, reschedule, confirm, cancel and complete them, and send the customer a message.
Endpoints
-
GET
/appointmentsList appointments -
GET
/appointments/{appointment}Retrieve an appointment -
POST
/appointmentsSchedule an appointment -
PATCH
/appointments/{appointment}Update an appointment -
POST
/appointments/{appointment}/confirmConfirm an appointment -
POST
/appointments/{appointment}/cancelCancel an appointment -
POST
/appointments/{appointment}/completeComplete an appointment -
POST
/appointments/{appointment}/notifySend the customer a message -
DELETE
/appointments/{appointment}Delete an appointment
List appointments
/api/v1/appointments
A list of appointments, newest first. Filter by status, customer, user, start time or modification date. Sort by starts_at for calendar order; appointments without a date (an invitation) are then left out.
- Scope
-
appointments.read— Read appointments (with the customer's name, email and phone), appointment types and availability - Required feature
appointments
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. |
sort
|
string | Sort by created_at, updated_at or starts_at; a minus sign in front means descending. With starts_at, appointments without a date are left out. one of: -created_at, created_at, -updated_at, updated_at, -starts_at, starts_at · default: -created_at |
filter[status]
|
string | Only appointments with this status: pending (not confirmed yet), confirmed, cancelled or completed. one of: pending, confirmed, cancelled, completed |
filter[customer_id]
|
string (uuid) | Only what belongs to this customer. |
filter[user_id]
|
string | Only appointments of this user (the id from List users). |
filter[starts_from]
|
string (date-time) | Only appointments that start at or after this moment: ISO 8601 with a time zone. |
filter[starts_until]
|
string (date-time) | Only appointments that start before this moment: ISO 8601 with a time zone. |
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/appointments?filter[starts_from]=2026-10-01T00%3A00%3A00Z&sort=starts_at" \
-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', 'appointments', [
'query' => [
'filter[starts_from]' => '2026-10-01T00:00:00Z',
'sort' => 'starts_at',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments?filter[starts_from]=2026-10-01T00%3A00%3A00Z&sort=starts_at', {
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/appointments",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
params={
"filter[starts_from]": "2026-10-01T00:00:00Z",
"sort": "starts_at"
},
)
data = response.json()["data"]Response 200
The response is a list with cursor pagination: data contains the objects, meta the pagination.
{
"data": [
{
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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 an appointment
/api/v1/appointments/{appointment}
One appointment by id. The response includes an ETag you can send in If-Match when updating.
- Scope
-
appointments.read— Read appointments (with the customer's name, email and phone), appointment types and availability - Required feature
appointments
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Example request
curl "https://app.klantly.com/api/v1/appointments/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', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments/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/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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.
Schedule an appointment
/api/v1/appointments
Schedules an appointment with a customer; name, email and phone come from the customer. Without ends_at the appointment lasts as long as its appointment type, otherwise the default duration from the appointment settings. Klantly does not check availability here: your planning is leading. The API itself sends the customer no email; use Send the customer a message for that. If the company has automations on "appointment scheduled", those do run, just like for an appointment in the calendar.
- Scope
-
appointments.write— Create, update, confirm, cancel and complete appointments (the company's automations run along) - Required feature
appointments
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Body (JSON)
| Field | Type | Description |
|---|---|---|
customer_id
required
|
string (uuid) | The customer of the appointment. Required when creating. |
title
optional
|
string | Title of the appointment. can be empty (null) · at most 255 characters · required without appointment_type_id |
description
optional
|
string | Description. can be empty (null) · at most 2000 characters |
location
optional
|
string | Location, for example the address of the customer. can be empty (null) · at most 255 characters |
starts_at
required
|
string (date-time) | Start (UTC). Empty for an invitation where the customer still picks a time. As input: ISO 8601 with a time zone. |
ends_at
optional
|
string (date-time) | End (UTC). Without ends_at when creating: the duration of the appointment type or the default duration. can be empty (null) |
all_day
optional
|
boolean | An all-day appointment. Times in the response are in UTC: convert back to the company's time zone (Europe/Amsterdam) for the date, otherwise an appointment starting at 00:00 falls on the day before. |
appointment_type_id
optional
|
string (uuid) | The appointment type, or null. can be empty (null) |
user_id
optional
|
string | The user who has the appointment, or null. can be empty (null) |
status
optional
|
string | pending (not confirmed yet), confirmed, cancelled or completed. When creating: pending or confirmed (default). one of: pending, confirmed |
notes
optional
|
string | Internal note on the appointment. can be empty (null) · at most 2000 characters |
Example request
curl -X POST "https://app.klantly.com/api/v1/appointments" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-d '{
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"title": "Inmeten veranda",
"starts_at": "2026-10-01T10:00:00+02:00",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'appointments', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'customer_id' => '9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70',
'title' => 'Inmeten veranda',
'starts_at' => '2026-10-01T10:00:00+02:00',
'appointment_type_id' => '9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
body: JSON.stringify({
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"title": "Inmeten veranda",
"starts_at": "2026-10-01T10:00:00+02:00",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5"
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/appointments",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
json={
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"title": "Inmeten veranda",
"starts_at": "2026-10-01T10:00:00+02:00",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5"
},
)
data = response.json()["data"]Response 201
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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. -
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 an appointment
/api/v1/appointments/{appointment}
Changes only the fields you send. A new starts_at is a reschedule: rescheduled_at is set, and without ends_at the duration stays the same. If an appointment without a date gets a starts_at, ends_at is added as when creating; an ends_at without a start time is not possible (422). To change the status, use confirm, cancel or complete.
- Scope
-
appointments.write— Create, update, confirm, cancel and complete appointments (the company's automations run along) - Required feature
appointments
Send the ETag in If-Match and you will never accidentally overwrite a newer version.
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
title
optional
|
string | Title of the appointment. at most 255 characters |
description
optional
|
string | Description. can be empty (null) · at most 2000 characters |
location
optional
|
string | Location, for example the address of the customer. can be empty (null) · at most 255 characters |
starts_at
optional
|
string (date-time) | Start (UTC). Empty for an invitation where the customer still picks a time. As input: ISO 8601 with a time zone. |
ends_at
optional
|
string (date-time) | End (UTC). Without ends_at when creating: the duration of the appointment type or the default duration. |
all_day
optional
|
boolean | An all-day appointment. Times in the response are in UTC: convert back to the company's time zone (Europe/Amsterdam) for the date, otherwise an appointment starting at 00:00 falls on the day before. |
appointment_type_id
optional
|
string (uuid) | The appointment type, or null. can be empty (null) |
user_id
optional
|
string | The user who has the appointment, or null. can be empty (null) |
notes
optional
|
string | Internal note on the appointment. can be empty (null) · at most 2000 characters |
Example request
curl -X PATCH "https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"starts_at": "2026-10-02T09:00:00Z"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('PATCH', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', [
'json' => [
'starts_at' => '2026-10-02T09:00:00Z',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"starts_at": "2026-10-02T09:00:00Z"
}),
});
const { data } = await response.json();import os
import requests
response = requests.patch(
"https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
json={
"starts_at": "2026-10-02T09:00:00Z"
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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.
Confirm an appointment
/api/v1/appointments/{appointment}/confirm
Sets an appointment to confirmed. If it is already confirmed, nothing changes.
- Scope
-
appointments.write— Create, update, confirm, cancel and complete appointments (the company's automations run along) - Required feature
appointments
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Example request
curl -X POST "https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/confirm" \
-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', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/confirm', [
'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/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/confirm', {
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/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/confirm",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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. -
409
invalid_state_transition— This action is not possible in the current status. -
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.
Cancel an appointment
/api/v1/appointments/{appointment}/cancel
Cancels the appointment, optionally with a reason. A completed appointment can no longer be cancelled. Automations on "appointment cancelled" run, just like in the calendar.
- Scope
-
appointments.write— Create, update, confirm, cancel and complete appointments (the company's automations run along) - Required feature
appointments
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
reason
optional
|
string | The reason for cancelling (optional). can be empty (null) · at most 500 characters |
Example request
curl -X POST "https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/cancel" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-d '{
"reason": "Klant is verhinderd"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/cancel', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'reason' => 'Klant is verhinderd',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
body: JSON.stringify({
"reason": "Klant is verhinderd"
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/cancel",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
json={
"reason": "Klant is verhinderd"
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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. -
409
invalid_state_transition— This action is not possible in the current status. -
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.
Complete an appointment
/api/v1/appointments/{appointment}/complete
Completes the appointment. If the company's lead conversion setting is "appointment completed", a lead becomes a customer, just like in the calendar; with the other settings it stays a lead.
- Scope
-
appointments.write— Create, update, confirm, cancel and complete appointments (the company's automations run along) - Required feature
appointments
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Example request
curl -X POST "https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/complete" \
-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', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/complete', [
'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/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/complete', {
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/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/complete",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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. -
409
invalid_state_transition— This action is not possible in the current status. -
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.
Send the customer a message
/api/v1/appointments/{appointment}/notify
Emails the customer a confirmation, reschedule, cancellation or reminder, using the template the company set up in Klantly. The message must match the status of the appointment. If the company has switched that template off, you get 409 and nothing is sent. Requires the appointments.send scope.
- Scope
-
appointments.send— Email appointment messages to customers - Required feature
appointments
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
message
required
|
string | Which message: confirmation, reschedule, cancellation or reminder. one of: confirmation, reschedule, cancellation, reminder |
Example request
curl -X POST "https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/notify" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-d '{
"message": "confirmation"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/notify', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'message' => 'confirmation',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/notify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
body: JSON.stringify({
"message": "confirmation"
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/notify",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
json={
"message": "confirmation"
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "appointment",
"id": "9d3f7d83-4f8b-4a0e-9d5c-6b7f8a9bacb4",
"title": "Inmeten veranda",
"description": null,
"status": "confirmed",
"starts_at": "2026-10-01T08:00:00Z",
"ends_at": "2026-10-01T09:00:00Z",
"all_day": false,
"location": "Dorpsstraat 1, Utrecht",
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"contact": {
"name": "Jan de Vries",
"email": "jan@example.com",
"phone": "+31 6 12345678"
},
"user_id": "usr_0k3j9x21m4zq8p",
"appointment_type_id": "9d3f7ea4-5a9c-4b1f-8e6d-7c8a9bacbdc5",
"deal_id": "9d3f7a20-1c5e-4d7b-8a2f-3e4c5d6f7a81",
"quote_id": null,
"invoice_id": null,
"notes": null,
"cancellation_reason": null,
"confirmed_at": "2026-09-14T10:15:00Z",
"cancelled_at": null,
"rescheduled_at": null,
"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. -
409
invalid_state_transition— This action is not possible in the current status. -
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.
Delete an appointment
/api/v1/appointments/{appointment}
Deletes the appointment permanently, including from the linked Google Calendar. To just call it off, cancel it instead.
- Scope
-
appointments.delete— Delete appointments - Required feature
appointments
Path parameters
| Name | Type | Description |
|---|---|---|
appointment required |
string (uuid) | The id (UUID) of the appointment. |
Example request
curl -X DELETE "https://app.klantly.com/api/v1/appointments/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('DELETE', 'appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
},
});
const { data } = await response.json();import os
import requests
response = requests.delete(
"https://app.klantly.com/api/v1/appointments/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "note",
"id": "9d3f7b41-2d6f-4e8c-9b3a-4f5d6e7a8b92",
"deleted": true
}
}Possible errors
-
403
insufficient_scope— This API key has no access to this action. -
404
not_found— Not found.
The object
All fields are always present; a field without a value is null.
| Field | Type | Description |
|---|---|---|
object |
string | Always "appointment". |
id |
string (uuid) | Unique id (UUID). |
title |
string | Title of the appointment. |
description |
string | Description. can be empty (null) |
status |
string | pending (not confirmed yet), confirmed, cancelled or completed. When creating: pending or confirmed (default). one of: pending, confirmed, cancelled, completed |
starts_at |
string (date-time) | Start (UTC). Empty for an invitation where the customer still picks a time. As input: ISO 8601 with a time zone. can be empty (null) |
ends_at |
string (date-time) | End (UTC). Without ends_at when creating: the duration of the appointment type or the default duration. can be empty (null) |
all_day |
boolean | An all-day appointment. Times in the response are in UTC: convert back to the company's time zone (Europe/Amsterdam) for the date, otherwise an appointment starting at 00:00 falls on the day before. |
location |
string | Location, for example the address of the customer. can be empty (null) |
customer_id |
string (uuid) | The customer of the appointment. Required when creating. can be empty (null) |
contact |
object | The contact details the appointment was made with. |
contact.name |
string | Name. can be empty (null) |
contact.email |
string | Email address; messages to the customer go here. can be empty (null) |
contact.phone |
string | Phone number. can be empty (null) |
user_id |
string | The user who has the appointment, or null. can be empty (null) |
appointment_type_id |
string (uuid) | The appointment type, or null. can be empty (null) |
deal_id |
string (uuid) | The deal on the pipeline board the appointment belongs to (Klantly links this itself). can be empty (null) |
quote_id |
string (uuid) | The linked quote, or null. can be empty (null) |
invoice_id |
string (uuid) | The linked invoice, or null. can be empty (null) |
notes |
string | Internal note on the appointment. can be empty (null) |
cancellation_reason |
string | Why the appointment was cancelled, or null. can be empty (null) |
confirmed_at |
string (date-time) | When the appointment was confirmed. can be empty (null) |
cancelled_at |
string (date-time) | When the appointment was cancelled. can be empty (null) |
rescheduled_at |
string (date-time) | When the appointment was last rescheduled. can be empty (null) |
created_at |
string (date-time) | Created at (UTC). |
updated_at |
string (date-time) | Last updated at (UTC). |