Klantly Developers

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
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
curl "https://app.klantly.com/api/v1/invoices/7a1d9e42-5c3b-4f6a-9d8e-2c4b6a8d0e13/pdf" \
  -H "Authorization: Bearer $KLANTLY_API_KEY" \
  -o FAC-2026-00042.pdf

Het 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
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 partial tot het hele bedrag binnen is; daarna op paid, en je krijgt invoice.paid.
  • Het bedrag kan nooit hoger zijn dan amount_due. Is het hoger, dan krijg je 422 op amount.

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

PHP
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;
}
Node.js
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

  • 404 met not_found: de factuur bestaat niet (meer), bijvoorbeeld een verwijderd concept.
  • 409 met invalid_state_transition: je registreert een betaling op een geannuleerde factuur, of je zet een offerte om die niet geaccepteerd is.
  • 422 met validation_failed: bijvoorbeeld een betaling die hoger is dan amount_due. In errors staat per veld wat er mis is.
  • 429 met rate_limited: wacht het aantal seconden uit Retry-After en probeer het opnieuw. Zie Rate limits.

Laatst bijgewerkt op 17 september 2026