API reference
Work orders
Work orders with lines and a checklist: create them from an ERP, for example, update them, change their status and fetch completed ones.
Endpoints
-
GET
/work-ordersList work orders -
GET
/work-orders/{work_order}Retrieve a work order -
POST
/work-ordersCreate a work order -
PATCH
/work-orders/{work_order}Update a work order -
POST
/work-orders/{work_order}/statusChange the status of a work order -
DELETE
/work-orders/{work_order}Delete a work order
List work orders
/api/v1/work-orders
A list of work orders, newest first, with lines, checklist and photos. Filter by status, customer, user, appointment or modification date. Use filter[status]=completed with filter[updated_since] to fetch completed work.
- Scope
-
work_orders.read— Read work orders, with the customer's name, address and contact details - Required feature
work_orders
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 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 work orders with this status. one of: draft, planned, in_progress, completed, invoiced, cancelled |
filter[customer_id]
|
string (uuid) | Only what belongs to this customer. |
filter[assigned_user_id]
|
string | Only what is assigned to this user (the id from List users). |
filter[appointment_id]
|
string (uuid) | Only work orders for this appointment. |
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/work-orders?filter[status]=completed&sort=-updated_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', 'work-orders', [
'query' => [
'filter[status]' => 'completed',
'sort' => '-updated_at',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders?filter[status]=completed&sort=-updated_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/work-orders",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
params={
"filter[status]": "completed",
"sort": "-updated_at"
},
)
data = response.json()["data"]Response 200
The response is a list with cursor pagination: data contains the objects, meta the pagination.
{
"data": [
{
"object": "work_order",
"id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
"number": "WB-2026-00042",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"location": null,
"description": null,
"work_performed": null,
"customer_notes": null,
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"customer": {
"type": "business",
"name": "De Vries Bouw",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL"
},
"deal_id": null,
"appointment_id": null,
"quote_id": null,
"invoice_id": null,
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"language": "nl",
"currency": "EUR",
"subtotal": "90.00",
"tax_amount": "18.90",
"total": "108.90",
"scheduled_at": "2026-10-01T08:00:00Z",
"started_at": null,
"completed_at": null,
"sent_at": null,
"signature": {
"is_signed": false,
"signed_by_name": null,
"signed_at": null
},
"items": [
{
"object": "work_order_item",
"id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cb",
"type": "labor",
"name": "Arbeid",
"description": null,
"sku": null,
"quantity": "2.00",
"unit": "uur",
"unit_price": "45.00",
"discount_percentage": "0.00",
"discount_amount": "0.00",
"line_total": "90.00",
"is_taxable": true,
"tax_rate": "21.00",
"minutes": 120
}
],
"checklist": [
{
"object": "work_order_checklist_item",
"id": "9d3f868b-c1a3-4c8a-9fde-e3f1a2b3c4dc",
"label": "Druk gecontroleerd",
"checked": false,
"required": true,
"note": null
}
],
"photos": [],
"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 work order
/api/v1/work-orders/{work_order}
One work order by id, with lines, checklist, photos (data only) and the state of the signature.
- Scope
-
work_orders.read— Read work orders, with the customer's name, address and contact details - Required feature
work_orders
Path parameters
| Name | Type | Description |
|---|---|---|
work_order required |
string (uuid) | The id (UUID) of the work order. |
Example request
curl "https://app.klantly.com/api/v1/work-orders/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', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders/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/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "work_order",
"id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
"number": "WB-2026-00042",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"location": null,
"description": null,
"work_performed": null,
"customer_notes": null,
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"customer": {
"type": "business",
"name": "De Vries Bouw",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL"
},
"deal_id": null,
"appointment_id": null,
"quote_id": null,
"invoice_id": null,
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"language": "nl",
"currency": "EUR",
"subtotal": "90.00",
"tax_amount": "18.90",
"total": "108.90",
"scheduled_at": "2026-10-01T08:00:00Z",
"started_at": null,
"completed_at": null,
"sent_at": null,
"signature": {
"is_signed": false,
"signed_by_name": null,
"signed_at": null
},
"items": [
{
"object": "work_order_item",
"id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cb",
"type": "labor",
"name": "Arbeid",
"description": null,
"sku": null,
"quantity": "2.00",
"unit": "uur",
"unit_price": "45.00",
"discount_percentage": "0.00",
"discount_amount": "0.00",
"line_total": "90.00",
"is_taxable": true,
"tax_rate": "21.00",
"minutes": 120
}
],
"checklist": [
{
"object": "work_order_checklist_item",
"id": "9d3f868b-c1a3-4c8a-9fde-e3f1a2b3c4dc",
"label": "Druk gecontroleerd",
"checked": false,
"required": true,
"note": null
}
],
"photos": [],
"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 work order
/api/v1/work-orders
Creates a work order for a customer, as a draft or planned. Klantly gives it a number and calculates the totals; name, address and contact details come from the customer.
- Scope
-
work_orders.write— Create and update work orders, and change their status (completing can send a review request) - Required feature
work_orders
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. Required when creating; name, address and contact details come from the customer. |
status
optional
|
string | draft, planned, in_progress, completed, invoiced or cancelled. When creating: draft or planned. one of: draft, planned |
title
optional
|
string | Title. can be empty (null) · at most 255 characters |
type
optional
|
string | Kind of work, for example installation, repair or maintenance. can be empty (null) · at most 64 characters |
location
optional
|
string | Where the work takes place, if not at the address of the customer. can be empty (null) · at most 255 characters |
description
optional
|
string | Description of the work or the complaint. can be empty (null) · at most 20000 characters |
work_performed
optional
|
string | What was done. can be empty (null) · at most 20000 characters |
customer_notes
optional
|
string | Notes for the customer; shown on the work order. can be empty (null) · at most 20000 characters |
scheduled_at
optional
|
string (date-time) | When the work is planned (UTC). As input: ISO 8601 with a time zone. can be empty (null) |
assigned_user_id
optional
|
string | The technician or user doing the work, or null. can be empty (null) |
appointment_id
optional
|
string (uuid) | The appointment the work order belongs to, or null. can be empty (null) |
quote_id
optional
|
string (uuid) | The quote the work order comes from, or null. can be empty (null) |
language
optional
|
string | Language of the work order: nl, en, de or fr. one of: nl, en, de, fr |
items
optional
|
array | The lines (up to 200). As input: a list with per line name (required) and optionally type, description, sku, quantity, unit, unit_price, discount_percentage, discount_amount, tax_rate, is_taxable and minutes; the list replaces all lines. |
checklist
optional
|
array | The checklist (up to 200 points). As input: per point label (required) and optionally checked, required and note; the list replaces the whole checklist. |
Example request
curl -X POST "https://app.klantly.com/api/v1/work-orders" \
-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": "Onderhoud cv-ketel",
"status": "planned",
"scheduled_at": "2026-10-01T08:00:00Z",
"items": [
{
"type": "labor",
"name": "Arbeid",
"quantity": 2,
"unit_price": "45.00"
}
]
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'work-orders', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'customer_id' => '9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70',
'title' => 'Onderhoud cv-ketel',
'status' => 'planned',
'scheduled_at' => '2026-10-01T08:00:00Z',
'items' => [
0 => [
'type' => 'labor',
'name' => 'Arbeid',
'quantity' => 2,
'unit_price' => '45.00',
],
],
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders', {
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": "Onderhoud cv-ketel",
"status": "planned",
"scheduled_at": "2026-10-01T08:00:00Z",
"items": [
{
"type": "labor",
"name": "Arbeid",
"quantity": 2,
"unit_price": "45.00"
}
]
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/work-orders",
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": "Onderhoud cv-ketel",
"status": "planned",
"scheduled_at": "2026-10-01T08:00:00Z",
"items": [
{
"type": "labor",
"name": "Arbeid",
"quantity": 2,
"unit_price": "45.00"
}
]
},
)
data = response.json()["data"]Response 201
{
"data": {
"object": "work_order",
"id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
"number": "WB-2026-00042",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"location": null,
"description": null,
"work_performed": null,
"customer_notes": null,
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"customer": {
"type": "business",
"name": "De Vries Bouw",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL"
},
"deal_id": null,
"appointment_id": null,
"quote_id": null,
"invoice_id": null,
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"language": "nl",
"currency": "EUR",
"subtotal": "90.00",
"tax_amount": "18.90",
"total": "108.90",
"scheduled_at": "2026-10-01T08:00:00Z",
"started_at": null,
"completed_at": null,
"sent_at": null,
"signature": {
"is_signed": false,
"signed_by_name": null,
"signed_at": null
},
"items": [
{
"object": "work_order_item",
"id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cb",
"type": "labor",
"name": "Arbeid",
"description": null,
"sku": null,
"quantity": "2.00",
"unit": "uur",
"unit_price": "45.00",
"discount_percentage": "0.00",
"discount_amount": "0.00",
"line_total": "90.00",
"is_taxable": true,
"tax_rate": "21.00",
"minutes": 120
}
],
"checklist": [
{
"object": "work_order_checklist_item",
"id": "9d3f868b-c1a3-4c8a-9fde-e3f1a2b3c4dc",
"label": "Druk gecontroleerd",
"checked": false,
"required": true,
"note": null
}
],
"photos": [],
"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 a work order
/api/v1/work-orders/{work_order}
Changes only the fields you send. items and checklist replace the lines and the checklist as a whole; the lines get new ids. A line and the total may not exceed 99,999,999.99. An invoiced work order can no longer change.
- Scope
-
work_orders.write— Create and update work orders, and change their status (completing can send a review request) - Required feature
work_orders
Send the ETag in If-Match and you will never accidentally overwrite a newer version.
Path parameters
| Name | Type | Description |
|---|---|---|
work_order required |
string (uuid) | The id (UUID) of the work order. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
title
optional
|
string | Title. can be empty (null) · at most 255 characters |
type
optional
|
string | Kind of work, for example installation, repair or maintenance. can be empty (null) · at most 64 characters |
location
optional
|
string | Where the work takes place, if not at the address of the customer. can be empty (null) · at most 255 characters |
description
optional
|
string | Description of the work or the complaint. can be empty (null) · at most 20000 characters |
work_performed
optional
|
string | What was done. can be empty (null) · at most 20000 characters |
customer_notes
optional
|
string | Notes for the customer; shown on the work order. can be empty (null) · at most 20000 characters |
scheduled_at
optional
|
string (date-time) | When the work is planned (UTC). As input: ISO 8601 with a time zone. can be empty (null) |
assigned_user_id
optional
|
string | The technician or user doing the work, or null. can be empty (null) |
appointment_id
optional
|
string (uuid) | The appointment the work order belongs to, or null. can be empty (null) |
quote_id
optional
|
string (uuid) | The quote the work order comes from, or null. can be empty (null) |
language
optional
|
string | Language of the work order: nl, en, de or fr. one of: nl, en, de, fr |
items
optional
|
array | The lines (up to 200). As input: a list with per line name (required) and optionally type, description, sku, quantity, unit, unit_price, discount_percentage, discount_amount, tax_rate, is_taxable and minutes; the list replaces all lines. |
checklist
optional
|
array | The checklist (up to 200 points). As input: per point label (required) and optionally checked, required and note; the list replaces the whole checklist. |
Example request
curl -X PATCH "https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"work_performed": "Filter vervangen en de ketel ontlucht."
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('PATCH', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', [
'json' => [
'work_performed' => 'Filter vervangen en de ketel ontlucht.',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"work_performed": "Filter vervangen en de ketel ontlucht."
}),
});
const { data } = await response.json();import os
import requests
response = requests.patch(
"https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
},
json={
"work_performed": "Filter vervangen en de ketel ontlucht."
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "work_order",
"id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
"number": "WB-2026-00042",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"location": null,
"description": null,
"work_performed": null,
"customer_notes": null,
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"customer": {
"type": "business",
"name": "De Vries Bouw",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL"
},
"deal_id": null,
"appointment_id": null,
"quote_id": null,
"invoice_id": null,
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"language": "nl",
"currency": "EUR",
"subtotal": "90.00",
"tax_amount": "18.90",
"total": "108.90",
"scheduled_at": "2026-10-01T08:00:00Z",
"started_at": null,
"completed_at": null,
"sent_at": null,
"signature": {
"is_signed": false,
"signed_by_name": null,
"signed_at": null
},
"items": [
{
"object": "work_order_item",
"id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cb",
"type": "labor",
"name": "Arbeid",
"description": null,
"sku": null,
"quantity": "2.00",
"unit": "uur",
"unit_price": "45.00",
"discount_percentage": "0.00",
"discount_amount": "0.00",
"line_total": "90.00",
"is_taxable": true,
"tax_rate": "21.00",
"minutes": 120
}
],
"checklist": [
{
"object": "work_order_checklist_item",
"id": "9d3f868b-c1a3-4c8a-9fde-e3f1a2b3c4dc",
"label": "Druk gecontroleerd",
"checked": false,
"required": true,
"note": null
}
],
"photos": [],
"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. -
409
invalid_state_transition— This action is not possible in the current status.
Change the status of a work order
/api/v1/work-orders/{work_order}/status
Sets the work order to draft, planned, in progress, completed or cancelled. For in progress and completed, started_at and completed_at are set. Just like in the app, completing can send the customer a review request if the company has set that up. Only Klantly itself sets invoiced.
- Scope
-
work_orders.write— Create and update work orders, and change their status (completing can send a review request) - Required feature
work_orders
Send an Idempotency-Key and a retry after a timeout will never create a duplicate record.
Path parameters
| Name | Type | Description |
|---|---|---|
work_order required |
string (uuid) | The id (UUID) of the work order. |
Body (JSON)
| Field | Type | Description |
|---|---|---|
status
required
|
string | draft, planned, in_progress, completed, invoiced or cancelled. When creating: draft or planned. one of: draft, planned, in_progress, completed, cancelled |
Example request
curl -X POST "https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/status" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f" \
-d '{
"status": "completed"
}'$client = new \GuzzleHttp\Client([
'base_uri' => 'https://app.klantly.com/api/v1/',
'headers' => ['Authorization' => 'Bearer ' . getenv('KLANTLY_API_KEY')],
]);
$response = $client->request('POST', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/status', [
'headers' => [
'Idempotency-Key' => '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
],
'json' => [
'status' => 'completed',
],
]);
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/status', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KLANTLY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': '6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f',
},
body: JSON.stringify({
"status": "completed"
}),
});
const { data } = await response.json();import os
import requests
response = requests.post(
"https://app.klantly.com/api/v1/work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70/status",
headers={
"Authorization": f"Bearer {os.environ['KLANTLY_API_KEY']}",
"Idempotency-Key": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
},
json={
"status": "completed"
},
)
data = response.json()["data"]Response 200
{
"data": {
"object": "work_order",
"id": "9d3f8328-9ed0-4f5d-8cab-b0cedfe0f1a9",
"number": "WB-2026-00042",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"location": null,
"description": null,
"work_performed": null,
"customer_notes": null,
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"customer": {
"type": "business",
"name": "De Vries Bouw",
"email": "jan@example.com",
"phone": "+31 6 12345678",
"address": "Dorpsstraat 1",
"postal_code": "3511 AB",
"city": "Utrecht",
"country": "NL"
},
"deal_id": null,
"appointment_id": null,
"quote_id": null,
"invoice_id": null,
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"language": "nl",
"currency": "EUR",
"subtotal": "90.00",
"tax_amount": "18.90",
"total": "108.90",
"scheduled_at": "2026-10-01T08:00:00Z",
"started_at": null,
"completed_at": null,
"sent_at": null,
"signature": {
"is_signed": false,
"signed_by_name": null,
"signed_at": null
},
"items": [
{
"object": "work_order_item",
"id": "9d3f856a-b0f2-4b7f-8ecd-d2e0f1a2b3cb",
"type": "labor",
"name": "Arbeid",
"description": null,
"sku": null,
"quantity": "2.00",
"unit": "uur",
"unit_price": "45.00",
"discount_percentage": "0.00",
"discount_amount": "0.00",
"line_total": "90.00",
"is_taxable": true,
"tax_rate": "21.00",
"minutes": 120
}
],
"checklist": [
{
"object": "work_order_checklist_item",
"id": "9d3f868b-c1a3-4c8a-9fde-e3f1a2b3c4dc",
"label": "Druk gecontroleerd",
"checked": false,
"required": true,
"note": null
}
],
"photos": [],
"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 a work order
/api/v1/work-orders/{work_order}
Deletes the work order. An invoiced work order cannot be deleted.
- Scope
-
work_orders.delete— Delete work orders - Required feature
work_orders
Path parameters
| Name | Type | Description |
|---|---|---|
work_order required |
string (uuid) | The id (UUID) of the work order. |
Example request
curl -X DELETE "https://app.klantly.com/api/v1/work-orders/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', 'work-orders/9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70');
$data = json_decode((string) $response->getBody(), true)['data'];const response = await fetch('https://app.klantly.com/api/v1/work-orders/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/work-orders/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. -
409
invalid_state_transition— This action is not possible in the current status.
The object
All fields are always present; a field without a value is null.
| Field | Type | Description |
|---|---|---|
object |
string | Always "work_order". |
id |
string (uuid) | Unique id (UUID). |
number |
string | Work order number, for example WB-2026-00042; Klantly assigns it. |
status |
string | draft, planned, in_progress, completed, invoiced or cancelled. When creating: draft or planned. one of: draft, planned, in_progress, completed, invoiced, cancelled |
title |
string | Title. can be empty (null) |
type |
string | Kind of work, for example installation, repair or maintenance. can be empty (null) |
location |
string | Where the work takes place, if not at the address of the customer. can be empty (null) |
description |
string | Description of the work or the complaint. can be empty (null) |
work_performed |
string | What was done. can be empty (null) |
customer_notes |
string | Notes for the customer; shown on the work order. can be empty (null) |
customer_id |
string (uuid) | The customer. Required when creating; name, address and contact details come from the customer. can be empty (null) |
customer |
object | The customer details on the work order, as they were when it was created. |
customer.type |
string | individual or business. can be empty (null) |
customer.name |
string | Name; for a business, the company name. can be empty (null) |
customer.email |
string | Email address. can be empty (null) |
customer.phone |
string | Phone number. can be empty (null) |
customer.address |
string | Street and house number. can be empty (null) |
customer.postal_code |
string | Postal code. can be empty (null) |
customer.city |
string | City. can be empty (null) |
customer.country |
string | Country. can be empty (null) |
deal_id |
string (uuid) | The deal on the pipeline board, or null. can be empty (null) |
appointment_id |
string (uuid) | The appointment the work order belongs to, or null. can be empty (null) |
quote_id |
string (uuid) | The quote the work order comes from, or null. can be empty (null) |
invoice_id |
string (uuid) | The invoice of the work order, or null. can be empty (null) |
assigned_user_id |
string | The technician or user doing the work, or null. can be empty (null) |
language |
string | Language of the work order: nl, en, de or fr. one of: nl, en, de, fr |
currency |
string | Always "EUR". |
subtotal |
string | Total excluding VAT, as a string with two decimals. |
tax_amount |
string | VAT amount. |
total |
string | Total including VAT. |
scheduled_at |
string (date-time) | When the work is planned (UTC). As input: ISO 8601 with a time zone. can be empty (null) |
started_at |
string (date-time) | When the work started. can be empty (null) |
completed_at |
string (date-time) | When the work was finished. can be empty (null) |
sent_at |
string (date-time) | When the work order was sent to the customer. can be empty (null) |
signature |
object | The signature of the customer (data only, no image). |
signature.is_signed |
boolean | Whether the work order has been signed. |
signature.signed_by_name |
string | Name of the person who signed. can be empty (null) |
signature.signed_at |
string (date-time) | When it was signed. can be empty (null) |
items |
array<object> | The lines (up to 200). As input: a list with per line name (required) and optionally type, description, sku, quantity, unit, unit_price, discount_percentage, discount_amount, tax_rate, is_taxable and minutes; the list replaces all lines. |
items.object |
string | Always "work_order_item". |
items.id |
string (uuid) | Id of the line. Changes every time you send items (the lines are replaced). |
items.type |
string | labor, material or other. one of: labor, material, other |
items.name |
string | Description on the work order. |
items.description |
string | Details. can be empty (null) |
items.sku |
string | Item number. can be empty (null) |
items.quantity |
string | Quantity. |
items.unit |
string | Unit, for example hour or piece. can be empty (null) |
items.unit_price |
string | Price per unit excluding VAT. |
items.discount_percentage |
string | Discount as a percentage. |
items.discount_amount |
string | Fixed discount on the line excluding VAT; only counts when discount_percentage is 0. |
items.line_total |
string | Line total excluding VAT; calculated by Klantly. |
items.is_taxable |
boolean | Whether VAT applies to the line. |
items.tax_rate |
string | VAT percentage (default 21, also if the company uses a different rate: send it in that case). |
items.minutes |
integer | Minutes worked, for labour. can be empty (null) |
checklist |
array<object> | The checklist (up to 200 points). As input: per point label (required) and optionally checked, required and note; the list replaces the whole checklist. |
checklist.object |
string | Always "work_order_checklist_item". |
checklist.id |
string (uuid) | Id of the point. |
checklist.label |
string | The point, for example "Pressure checked". |
checklist.checked |
boolean | Ticked off. |
checklist.required |
boolean | Must be ticked off. |
checklist.note |
string | Note on the point. can be empty (null) |
photos |
array<object> | The photos of the work order (data only; the files are not in the API yet). |
photos.object |
string | Always "work_order_photo". |
photos.id |
string (uuid) | Id of the photo. |
photos.kind |
string | before, after or other. can be empty (null) · one of: before, after, other |
photos.caption |
string | Caption. can be empty (null) |
photos.created_at |
string (date-time) | Uploaded at (UTC). |
created_at |
string (date-time) | Created at (UTC). |
updated_at |
string (date-time) | Last updated at (UTC). |