Facturen naar je boekhouding
Boek de facturen uit Klantly automatisch in je boekhoudpakket, met de pdf erbij, en meld betalingen uit de bank terug.
Je maakt en verstuurt facturen in Klantly; je boekhouding gebeurt ergens anders. Met de API boek je elke verstuurde factuur in je boekhoudpakket, bewaar je de pdf bij de boeking en zet je betalingen die op de bank binnenkomen terug op de factuur in Klantly.
Wat je nodig hebt
Een API-sleutel met deze scopes:
| Scope | Waarvoor |
|---|---|
invoices.read |
Facturen en hun pdf ophalen. |
webhooks.manage |
Optioneel: een webhook aanmaken voor de factuur-events. |
invoices.write |
Optioneel: betalingen uit de bank terugmelden, of een geaccepteerde offerte omzetten naar een factuur. |
invoices.send |
Optioneel: de betaallink ophalen om de klant online te laten betalen. |
De functie Facturen moet actief zijn voor je bedrijf.
Stap 1: weten wanneer je moet boeken
Boek een factuur pas als hij verstuurd is. Een concept (draft) kan nog veranderen of verdwijnen; een verstuurde factuur ligt vast.
Luister met een webhook naar deze events:
| Event | Wat je doet |
|---|---|
invoice.sent |
De factuur boeken als verkoopfactuur. Komt ook bij opnieuw versturen (een herinnering): boek op id, niet twee keer. |
invoice.paid |
De factuur is volledig betaald. Markeer de boeking als voldaan. |
invoice.cancelled |
De factuur is geannuleerd. Maak in je boekhouding een creditboeking. |
Haal je liever zelf op, bijvoorbeeld elk uur, vraag dan de facturen op die sinds je vorige ronde zijn gewijzigd:
curl --globoff "https://app.klantly.com/api/v1/invoices?filter[updated_since]=2026-10-06T00:00:00Z&sort=updated_at&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, en gebruik dat de volgende keer als filter[updated_since]. Sla concepten over en kijk per factuur naar status.
Stap 2: de factuur boeken
Alles wat je voor de boeking nodig hebt, staat in de factuur:
| Veld | Wat erin staat |
|---|---|
number |
Het factuurnummer, bijvoorbeeld FAC-2026-00042. Gebruik het als kenmerk van de boeking. |
invoice_date, due_date |
Factuurdatum en vervaldatum (JJJJ-MM-DD). |
customer |
Naam, bedrijfsnaam, adres, btw-nummer en KvK-nummer zoals ze op de factuur staan. |
items |
De regels, met line_total (zonder btw), tax_rate en is_taxable. |
discount_amount |
Een korting op de hele factuur, als bedrag. |
subtotal, tax_amount, total |
De totalen. Er geldt altijd: subtotal − discount_amount + tax_amount = total. |
is_term_invoice, term_percentage |
Een termijnfactuur: een deel van een offerte. |
Bedragen zijn tekst met twee decimalen, bijvoorbeeld "1305.79". Reken ermee als decimaal getal, niet als kommagetal, anders krijg je afrondingsverschillen.
Boek per btw-tarief de som van line_total van de regels met dat tarief. Heeft de factuur een discount_amount, verdeel die dan naar rato over de tarieven of boek hem als aparte regel. Controleer daarna dat je boeking op total uitkomt.
Let op
Een regel met unit_price_incl is ingevoerd als prijs inclusief btw. line_total_incl is dan het bedrag dat de klant voor die regel betaalt, tot op de cent. Voor de boeking gebruik je nog steeds line_total en tax_rate.
Stap 3: de pdf bij de boeking bewaren
Haal de factuur op zoals de klant hem kreeg:
curl "https://app.klantly.com/api/v1/invoices/7a1d9e42-5c3b-4f6a-9d8e-2c4b6a8d0e13/pdf" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-o FAC-2026-00042.pdfHet antwoord is het bestand zelf. De eerste keer maakt Klantly de pdf aan, dat kan een paar seconden duren; daarna komt hij uit de cache. Deze aanvraag valt onder de limiet voor zware acties, dus haal de pdf één keer op en bewaar hem.
Stap 4: betalingen terugmelden
Komt een betaling binnen op de bank, meld hem dan aan Klantly. Dan ziet je team dat de factuur betaald is en stuurt Klantly geen betaalherinneringen meer:
curl -X POST "https://app.klantly.com/api/v1/invoices/7a1d9e42-5c3b-4f6a-9d8e-2c4b6a8d0e13/payments" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bank-20261006-000318" \
-d '{
"amount": "1943.00",
"payment_method": "bank_transfer",
"reference": "NL91ABNA0417164300 / FAC-2026-00042"
}'- Zet het transactienummer van de bank in de
Idempotency-Key: dan wordt een betaling nooit twee keer geregistreerd, ook niet als je het verzoek herhaalt. - Een deelbetaling mag. De factuur staat dan op
partialtot het hele bedrag binnen is; daarna oppaid, en je krijgtinvoice.paid. - Het bedrag kan nooit hoger zijn dan
amount_due. Is het hoger, dan krijg je422opamount.
Registreer je betalingen alleen in je boekhouding, sla deze stap dan over.
De klant online laten betalen
Heeft je bedrijf Mollie gekoppeld en de functie Online betalen, dan betaalt de klant een verstuurde factuur op een betaalpagina. Die link haal je op om hem zelf te delen, bijvoorbeeld in een herinnering vanuit je boekhouding of via WhatsApp:
curl "https://app.klantly.com/api/v1/invoices/7a1d9e42-5c3b-4f6a-9d8e-2c4b6a8d0e13/payment-link" \
-H "Authorization: Bearer $KLANTLY_API_KEY"Het antwoord bevat url en amount_due. Heeft de factuur een betaalschema, dan kiest de klant op die pagina de eerstvolgende termijn. Een betaling via die pagina komt vanzelf op de factuur: je krijgt invoice.paid zodra alles binnen is, en je hoeft hem niet met stap 4 terug te melden.
Waarschuwing
Wie de link heeft, kan de factuur bekijken en betalen. Deel hem alleen met de klant. Daarom vraagt hij de scope invoices.send.
Is de factuur nog een concept, al betaald of geannuleerd, dan krijg je 409 met invalid_state_transition; is Mollie nog niet gekoppeld, dan 409 met conflict.
Van offerte naar factuur
Heeft de klant een offerte geaccepteerd, dan maak je er in één keer een conceptfactuur van. Klant, regels, korting en teksten gaan mee:
curl -X POST "https://app.klantly.com/api/v1/quotes/3b8e5f20-6a1c-4d9e-8b7a-5c4d3e2f1a09/invoice" \
-H "Authorization: Bearer $KLANTLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: offerte-OF-26457-factuur" \
-d '{"payment_term_days": 14}'Een offerte heeft hooguit één factuur. Vraag je het nog een keer, dan krijg je 409 met de code conflict en het id van de bestaande factuur in invoice_id. Versturen doe je daarna met POST /invoices/{id}/send.
Alles samen
use GuzzleHttp\Client;
/**
* Boekt de facturen die sinds $since zijn gewijzigd en geeft het tijdstip terug voor de volgende ronde.
* $book krijgt de factuur en een functie die de pdf ophaalt: roep die alleen aan als de factuur nog niet
* geboekt is. $markPaid en $credit werken je boekhouding bij.
*/
function syncInvoices(Client $klantly, string $since, callable $book, callable $markPaid, callable $credit): 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('invoices', [
'query' => array_filter([
'filter' => ['updated_since' => $since],
'sort' => 'updated_at',
'limit' => 100,
'cursor' => $cursor,
]),
])->getBody(), true);
foreach ($page['data'] as $invoice) {
match ($invoice['status']) {
'draft' => null, // Nog niet boeken.
'cancelled' => $credit($invoice),
default => $book($invoice, fn () => downloadPdf($klantly, $invoice)), // Ontdubbel op $invoice['id'].
};
if ($invoice['status'] === 'paid') {
$markPaid($invoice);
}
}
$cursor = $page['meta']['next_cursor'];
} while ($cursor !== null);
return $startedAt;
}
function downloadPdf(Client $klantly, array $invoice): string
{
$path = sys_get_temp_dir() . '/' . $invoice['number'] . '.pdf';
$klantly->get("invoices/{$invoice['id']}/pdf", ['sink' => $path]);
return $path;
}import { writeFile } from 'node:fs/promises';
const BASE_URL = 'https://app.klantly.com/api/v1';
const headers = { Authorization: `Bearer ${process.env.KLANTLY_API_KEY}` };
async function klantly(path) {
const response = await fetch(`${BASE_URL}/${path}`, { headers });
const json = await response.json();
if (!response.ok) throw new Error(json.detail ?? json.title);
return json;
}
async function downloadPdf(invoice) {
const response = await fetch(`${BASE_URL}/invoices/${invoice.id}/pdf`, { headers });
if (!response.ok) throw new Error(`Pdf van ${invoice.number} niet opgehaald (${response.status})`);
const path = `/tmp/${invoice.number}.pdf`;
await writeFile(path, Buffer.from(await response.arrayBuffer()));
return path;
}
// Boekt de facturen die sinds `since` zijn gewijzigd en geeft het tijdstip terug voor de volgende ronde.
export async function syncInvoices(since, { book, markPaid, credit }) {
const startedAt = new Date(Date.now() - 60_000).toISOString(); // Een minuut marge voor klokverschil.
let cursor = null;
do {
const params = new URLSearchParams({ 'filter[updated_since]': since, sort: 'updated_at', limit: '100' });
if (cursor) params.set('cursor', cursor);
const page = await klantly(`invoices?${params}`);
for (const invoice of page.data) {
if (invoice.status === 'draft') continue; // Nog niet boeken.
if (invoice.status === 'cancelled') {
await credit(invoice);
continue;
}
// book krijgt een functie die de pdf ophaalt: roep die alleen aan als de factuur nog niet geboekt is.
await book(invoice, () => downloadPdf(invoice)); // Ontdubbel op invoice.id.
if (invoice.status === 'paid') await markPaid(invoice);
}
cursor = page.meta.next_cursor;
} while (cursor);
return startedAt;
}Fouten opvangen
404metnot_found: de factuur bestaat niet (meer), bijvoorbeeld een verwijderd concept.409metinvalid_state_transition: je registreert een betaling op een geannuleerde factuur, of je zet een offerte om die niet geaccepteerd is.422metvalidation_failed: bijvoorbeeld een betaling die hoger is danamount_due. Inerrorsstaat per veld wat er mis is.429metrate_limited: wacht het aantal seconden uitRetry-Afteren probeer het opnieuw. Zie Rate limits.
Laatst bijgewerkt op 17 september 2026