Werkbonnen vanuit je ERP
Maak werkbonnen aan vanuit je ERP, laat je monteurs ze in Klantly afwerken en haal het afgeronde werk terug voor de facturatie.
Je ERP weet welk werk er gedaan moet worden; je monteurs werken in Klantly. Met de API zet je de werkbon klaar, met de regels en de checklist, en haal je hem terug zodra het werk af en ondertekend is.
Wat je nodig hebt
Een API-sleutel met deze scopes:
| Scope | Waarvoor |
|---|---|
customers.read |
De klant opzoeken op e-mailadres. |
customers.write |
Een klant aanmaken die nog niet bestaat. |
users.read |
Optioneel: de monteur opzoeken die het werk doet. |
work_orders.read |
Afgeronde werkbonnen ophalen. |
work_orders.write |
Werkbonnen aanmaken, bijwerken en de status wijzigen. |
De functie Werkbonnen moet actief zijn voor je bedrijf.
Stap 1: klant en monteur
Een werkbon hoort altijd bij een klant. Zoek hem op met filter[email] of maak hem aan, zoals in Websiteformulier naar lead. Wil je de werkbon meteen aan een monteur toewijzen, haal dan zijn id op met GET /users en bewaar die in je ERP.
Stap 2: de werkbon aanmaken
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: erp-order-20260142" \
-d '{
"customer_id": "9d3f6c1e-4b2a-4c8e-9f1a-2b3c4d5e6f70",
"status": "planned",
"title": "Onderhoud cv-ketel",
"type": "onderhoud",
"scheduled_at": "2026-10-06T08:00:00+02:00",
"assigned_user_id": "usr_0k3j9x21m4zq8p",
"items": [
{"type": "labor", "name": "Onderhoudsbeurt", "quantity": 1, "unit": "uur", "unit_price": 85},
{"type": "material", "name": "Filterset", "sku": "FLT-200", "quantity": 1, "unit_price": 24.5}
],
"checklist": [
{"label": "Druk gecontroleerd", "required": true},
{"label": "Rookgasmeting uitgevoerd", "required": true}
]
}'- Klantly geeft de werkbon een nummer (
number, bijvoorbeeldWB-2026-00042) en rekent de totalen uit. Prijzen zijn zonder btw. Een korting per regel geef je als percentage (discount_percentage) of als vast bedrag (discount_amount). Een regel en het totaal mogen niet boven 99.999.999,99 uitkomen. - Naam, adres en contactgegevens komen van de klant, zoals ze op dat moment zijn.
- Een nieuwe werkbon is
draft(standaard) ofplanned. - Hoort de werkbon bij een afspraak, stuur dan
appointment_idmee. - Zet het ordernummer uit je ERP in de
Idempotency-Key, en bewaar deiden hetnumberuit het antwoord in je ERP.
Stap 3: wijzigingen doorzetten
Je stuurt alleen wat verandert:
curl -X PATCH "https://app.klantly.com/api/v1/work-orders/5e2c8a91-3f4b-4d6e-8a7c-1b2d3e4f5a60" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scheduled_at": "2026-10-07T08:00:00+02:00"}'items en checklist vervangen altijd de hele lijst.
Waarschuwing
Stuur checklist alleen mee als je hem echt wilt vervangen: punten die je monteur al heeft afgevinkt, zijn anders weg. Hetzelfde geldt voor items en de regels die hij ter plekke heeft toegevoegd.
Gaat het werk niet door, annuleer de werkbon dan met de status:
curl -X POST "https://app.klantly.com/api/v1/work-orders/5e2c8a91-3f4b-4d6e-8a7c-1b2d3e4f5a60/status" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: erp-order-20260142-cancel" \
-d '{"status": "cancelled"}'Is de werkbon in Klantly al gefactureerd (invoiced), dan kan hij niet meer veranderen: je krijgt 409 met de code invalid_state_transition.
Stap 4: het afgeronde werk ophalen
Zodra je monteur de werkbon afrondt, stuurt Klantly het event work_order.completed. Tekent de klant op de werkbon, dan rondt dat hem meteen af: je krijgt work_order.signed en work_order.completed tegelijk, in willekeurige volgorde. Luister daarnaar met een webhook, dan weet je het direct.
Haal je liever zelf op, bijvoorbeeld elk kwartier, vraag dan de werkbonnen op die sinds je vorige ronde zijn afgerond:
curl --globoff "https://app.klantly.com/api/v1/work-orders?filter[status]=completed&filter[updated_since]=2026-10-06T00:00:00Z&limit=100" \
-H "Authorization: Bearer $KLANTLY_API_KEY"Blader verder met next_cursor tot die null is, zie Paginering. Onthoud het tijdstip waarop je ophaalronde begon, min een minuut marge voor klokverschil, en gebruik dat de volgende keer als filter[updated_since]: zo mis je niets wat tijdens het ophalen verandert. Een werkbon kan daardoor twee keer langskomen, bijvoorbeeld als de klant later nog tekent; ontdubbel op id.
Wat je terugkrijgt:
| Veld | Wat erin staat |
|---|---|
work_performed |
Wat de monteur heeft gedaan. |
items |
De regels, ook wat de monteur ter plekke heeft toegevoegd, met minutes bij arbeid. |
checklist |
Wat is afgevinkt, met opmerkingen. |
signature |
Of de klant heeft getekend, met naam en tijdstip. |
subtotal, tax_amount, total |
De totalen, als tekst met twee decimalen. |
Factureer je vanuit je ERP, dan blijft de werkbon in Klantly op completed staan: de status invoiced zet alleen Klantly zelf, als je in Klantly factureert.
Alles samen
use GuzzleHttp\Client;
/**
* Haalt de werkbonnen op die sinds $since zijn afgerond en geeft ze één voor één aan $handle.
* Geeft het tijdstip terug dat je de volgende keer als $since gebruikt.
*/
function fetchCompletedWorkOrders(Client $klantly, string $since, callable $handle): string
{
$startedAt = gmdate('Y-m-d\TH:i:s\Z', time() - 60); // Een minuut marge voor klokverschil.
$cursor = null;
do {
$page = json_decode((string) $klantly->get('work-orders', [
'query' => array_filter([
'filter' => ['status' => 'completed', 'updated_since' => $since],
'limit' => 100,
'cursor' => $cursor,
]),
])->getBody(), true);
foreach ($page['data'] as $workOrder) {
$handle($workOrder); // Bijvoorbeeld een factuur maken in je ERP; ontdubbel op $workOrder['id'].
}
$cursor = $page['meta']['next_cursor'];
} while ($cursor !== null);
return $startedAt;
}const BASE_URL = 'https://app.klantly.com/api/v1';
async function klantly(path) {
const response = await fetch(`${BASE_URL}/${path}`, {
headers: { Authorization: `Bearer ${process.env.KLANTLY_API_KEY}` },
});
const json = await response.json();
if (!response.ok) throw new Error(json.detail ?? json.title);
return json;
}
// Haalt de werkbonnen op die sinds `since` zijn afgerond en geeft ze één voor één aan `handle`.
// Geeft het tijdstip terug dat je de volgende keer als `since` gebruikt.
export async function fetchCompletedWorkOrders(since, handle) {
const startedAt = new Date(Date.now() - 60_000).toISOString(); // Een minuut marge voor klokverschil.
let cursor = null;
do {
const params = new URLSearchParams({ 'filter[status]': 'completed', 'filter[updated_since]': since, limit: '100' });
if (cursor) params.set('cursor', cursor);
const page = await klantly(`work-orders?${params}`);
for (const workOrder of page.data) {
await handle(workOrder); // Bijvoorbeeld een factuur maken in je ERP; ontdubbel op workOrder.id.
}
cursor = page.meta.next_cursor;
} while (cursor);
return startedAt;
}Fouten opvangen
422metvalidation_failed: bijvoorbeeld een regel zondernameof een onbekend veld in een regel. Inerrorsstaat per veld wat er mis is.409metinvalid_state_transition: de werkbon is al gefactureerd.429metrate_limited: wacht het aantal seconden uitRetry-Afteren probeer het opnieuw. Zie Rate limits.
Laatst bijgewerkt op 15 september 2026