Programmeercode op een laptopscherm
Voor ontwikkelaars

Incasso API
van factuur tot dossier in één request.

Koppel uw software, CRM of facturatiesysteem rechtstreeks aan nl.legal. Eén POST en uw vordering staat als dossier bij ons klaar, inclusief PDF-facturen, AI-factuurcontrole en een gratis testomgeving.

Gratis testomgeving

Met een nll_test_-key test u de volledige integratie, inclusief validatie en AI-controle, zonder dat er ooit een echt dossier ontstaat.

AI-factuurcontrole

Meegestuurde PDF's worden automatisch vergeleken met uw opgave. Wijkt een totaalbedrag af, dan krijgt u een waarschuwing terug. Uw opgave blijft altijd leidend.

WIK-proof samenvoegen

Stuurt u een vervolgfactuur met hetzelfde klantnummer? Dan voegen wij die conform de Wet Incassokosten automatisch toe aan het lopende dossier.

In drie stappen live

  1. 1
    Vraag een API-key aan via het contactformulier of uw accountmanager. U ontvangt een test-key (nll_test_…) en na acceptatie een live-key (nll_live_…).
  2. 2
    Test de verbinding met GET https://nl.legal/api/v1/ping. Met uw key in de Authorization-header ziet u direct of alles klopt.
  3. 3
    Dien uw eerste opdracht in met POST /api/v1/orders. U krijgt synchroon het dossiernummer terug.

Voorbeeld: opdracht indienen (curl)

curl -X POST https://nl.legal/api/v1/orders \
  -H "Authorization: Bearer nll_live_UW_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "type": "incasso",
    "reference": "ORDER-2026-0042",
    "description": "Onbetaalde facturen voor webdesign",
    "debtors": [{
      "type": "business",
      "company_name": "Voorbeeld Webshop B.V.",
      "customer_number": "KLANT-1234",
      "email": "administratie@voorbeeldwebshop.nl",
      "address": {
        "street": "Hoofdstraat",
        "house_number": "12",
        "postal_code": "1234 AB",
        "city": "Amsterdam"
      }
    }],
    "invoices": [{
      "invoice_number": "F2026-0815",
      "invoice_date": "2026-04-01",
      "due_date": "2026-04-15",
      "amount_cents": 125000,
      "description": "Webdesign april 2026"
    }]
  }'

Response (201 Created)

{
    "id": "ord_b2b332c6a34ebb12",
    "object": "order",
    "mode": "live",
    "type": "incasso",
    "reference": "ORDER-2026-0042",
    "case": {
        "case_number": "IN211543",
        "new": true
    },
    "debtors": [
        { "name": "Voorbeeld Webshop B.V.", "customer_number": "KLANT-1234" }
    ],
    "invoice_count": 1,
    "total_amount_cents": 125000,
    "warnings": [],
    "created_at": "2026-06-10T18:00:40+02:00"
}

Met een test-key (nll_test_…) krijgt u exact dezelfde response, maar dan met "mode": "test", "case_number": null en een test_mode-warning: er wordt dan geen dossier aangemaakt.

Endpoints

POST /api/v1/orders

Dien een incasso-opdracht in: type (pre-incasso of incasso), één of meer debiteuren, één of meer facturen en optioneel PDF-bijlagen. Synchroon antwoord met dossiernummer. Ondersteunt de Idempotency-Key-header tegen dubbele indiening.

GET /api/v1/orders

Lijst uw opdrachten met cursorpaginatie, voor reconciliatie. Filter op case_number (zoek op IN-nummer), reference, customer_number of type.

GET /api/v1/orders/{orderId}

Vraag de actuele status op: dossierstatus, of het dossier nog openstaat en het actuele openstaande saldo (inclusief rente en kosten) in centen.

POST /api/v1/orders/{orderId}/hold · /withdraw · /payments

Stuuracties op een lopend dossier: pauze aanvragen, intrekken, of een rechtstreeks aan u betaald bedrag melden. Elke actie zet een taak met instructies op het dossier die een behandelaar verwerkt; de API muteert zelf niets financieels.

GET /api/v1/ping

Verbindings- en authenticatietest. Werkt ook zonder key (dan authenticated: false).

Blader door alle endpoints, velden en voorbeelden in de interactieve API-reference. De onderliggende machine-leesbare specificatie staat op /api/v1/openapi.json (OpenAPI 3.1), direct bruikbaar in Postman, Insomnia of uw codegenerator.

Goed om te weten

Bedragen in centen

Alle bedragen zijn gehele getallen in eurocenten: € 1.250,00 is 125000. Geen floats, geen komma/punt-verwarring en geen afrondingsfouten, exact zoals Stripe en Mollie dat doen.

Opdrachtgever = uw API-key

U stuurt nooit een opdrachtgever mee. Uw API-key is gekoppeld aan uw bedrijf; elke opdracht komt automatisch op uw naam binnen.

Eén opdracht = één vordering

Een opdracht kan meerdere facturen en meerdere debiteuren bevatten, maar meerdere debiteuren betekent altijd: dezelfde vordering, samen aangesproken (bijvoorbeeld twee contractspartijen of partners). Losse vorderingen op verschillende debiteuren dient u in als losse opdrachten.

Klantnummer en de Wet Incassokosten

Geef per debiteur uw eigen customer_number mee. Dient u later een vervolgfactuur in voor dezelfde debiteur terwijl er nog een dossier openstaat, dan voegen wij die vordering automatisch aan dat dossier toe, zoals de Wet Incassokosten dat vraagt. U herkent dit aan case.new: false en de warning attached_to_existing_case.

Titel van de vordering

Geef desgewenst een title mee in de vorm "overeenkomst van opdracht voor ...". Laat u het veld leeg, dan genereert onze AI er automatisch een in dat formaat op basis van uw omschrijving en de facturen.

Eigen metadata

Het optionele metadata-object (vrije sleutel/waarde-paren) wordt opgeslagen, in elke response teruggegeven en als notitie bij het dossier gezet. Ideaal om uw eigen identifiers, zoals een PO-nummer of kostenplaats, mee te koppelen.

Maatwerkvelden (UDF)

Werkt u met dossiervelden die specifiek voor uw situatie zijn? Geef die mee in het optionele udf_fields-object als sleutel/waarde-paren; u kunt er meerdere tegelijk meesturen en ze worden als veld op het dossier gezet. Welke veldnamen voor uw account beschikbaar zijn, stemmen wij met u af; gebruik exact die sleutels. Een onbekende sleutel wordt overgeslagen met de waarschuwing udf_field_unknown. Voorbeeld: {"uw_veldnaam_1": "waarde", "uw_veldnaam_2": "2026-06-01"}.

Toegang en key-rotatie

Een key komt nooit bij gegevens van een andere opdrachtgever. Per key bepalen wij of die alleen de eigen ingediende opdrachten ziet, of alle API-opdrachten van uw organisatie. Met die tweede instelling blijven uw opdrachten zichtbaar nadat u een key vernieuwt.

Consument: WIK-aanmaning verplicht

Dient u een incasso (geen pre-incasso) in tegen een particulier (type: individual), dan vereist de wet dat de incassokosten zijn aangezegd met een 14-dagenbrief (art. 6:96 BW). Stuur die WIK-aanmaning als PDF mee en label de bijlage met document_type: "wik_letter". Ontbreekt de aanmaning, dan weigeren wij de opdracht met code wik_letter_required. Onze AI controleert de brief ook inhoudelijk. Nog geen 14-dagentermijn gehad? Dien dan in als pre-incasso; dan versturen wij de aanmaning.

Sturen op een lopend dossier

Na het indienen kunt u een dossier bijsturen: een pauze aanvragen (/hold), het laten intrekken (/withdraw) of melden dat de debiteur rechtstreeks aan u betaalde (/payments, bedrag in centen). Elke melding komt als taak met instructies op het dossier; een behandelaar verwerkt die. Zo blijft het traject juridisch zuiver en raakt een intrekking netjes verrekend met de no-cure-no-pay-afspraak.

PDF-bijlagen en AI-controle

Facturen stuurt u mee als base64-PDF. Limieten op de binaire inhoud: max. 10 MB per bestand en 25 MB per opdracht; daarnaast mag de volledige base64-aanvraag niet groter zijn dan 40 MB (base64 is circa 33% groter dan binair). Elk bestand wordt gevalideerd op echtheid en veiligheid. Daarna leest onze AI de facturen mee en vergelijkt die met uw opgave en onderling. Wijkt een totaalbedrag af, dan krijgt u ai_invoice_mismatch; verschillen tussen de documenten (bijv. een ander adres, factuurnummer of btw-nummer op de factuur dan op de WIK-brief) geven ai_document_mismatch. Uw opgave blijft leidend; wat u met de waarschuwing doet, bepaalt u zelf.

Idempotency

Stuur per opdracht een unieke Idempotency-Key-header mee (bijvoorbeeld een UUID). Valt de verbinding weg en doet uw systeem een retry, dan ontstaat er gegarandeerd geen dubbel dossier: u krijgt exact dezelfde response nogmaals.

Rate limits

Standaard 30 verzoeken per minuut en 500 per dag. Elke response op /orders bevat RateLimit-Limit, RateLimit-Remaining en RateLimit-Reset headers; bij overschrijding krijgt u een 429 met Retry-After. Hogere limieten nodig? Neem contact op.

Webhooks

Wij sturen events naar uw eigen endpoint zodra er iets verandert aan een van uw dossiers: payment.received, correspondence.sent, case.updated en case.closed. U laat uw endpoint-URL bij ons instellen en krijgt een signing-secret. Zo hoeft u niet te pollen. Zie hieronder hoe u de handtekening verifieert.

Voorbeelden in uw taal

PHP

<?php
$opdracht = [
    'type' => 'incasso',
    'reference' => 'ORDER-2026-0042',
    'debtors' => [[
        'type' => 'business',
        'company_name' => 'Voorbeeld Webshop B.V.',
        'customer_number' => 'KLANT-1234',
        'address' => [
            'street' => 'Hoofdstraat', 'house_number' => '12',
            'postal_code' => '1234 AB', 'city' => 'Amsterdam',
        ],
    ]],
    'invoices' => [[
        'invoice_number' => 'F2026-0815',
        'invoice_date' => '2026-04-01',
        'payment_term_days' => 14,
        'amount_cents' => 125000, // € 1.250,00
    ]],
    'attachments' => [[
        'filename' => 'F2026-0815.pdf',
        'content_base64' => base64_encode(file_get_contents('factuur.pdf')),
    ]],
];

$ch = curl_init('https://nl.legal/api/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('NLLEGAL_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode($opdracht),
    CURLOPT_TIMEOUT => 60,
]);
$result = json_decode(curl_exec($ch), true);

if (curl_getinfo($ch, CURLINFO_HTTP_CODE) === 201) {
    echo "Dossier aangemaakt: " . $result['case']['case_number'];
    foreach ($result['warnings'] as $warning) {
        echo "\nLet op: " . $warning['message'];
    }
} else {
    echo "Fout: " . $result['detail'];
}

Python

import base64
import os
import uuid

import requests

opdracht = {
    "type": "pre-incasso",
    "reference": "ORDER-2026-0042",
    "debtors": [{
        "type": "individual",
        "first_name": "Jan",
        "last_name": "Jansen",
        "customer_number": "KLANT-5678",
        "address": {
            "street": "Kerkstraat", "house_number": "1",
            "postal_code": "9712 AB", "city": "Groningen",
        },
    }],
    "invoices": [{
        "invoice_number": "F2026-0901",
        "invoice_date": "2026-05-01",
        "due_date": "2026-05-15",
        "amount_cents": 50000,  # € 500,00
    }],
}

with open("factuur.pdf", "rb") as f:
    opdracht["attachments"] = [{
        "filename": "F2026-0901.pdf",
        "content_base64": base64.b64encode(f.read()).decode(),
    }]

response = requests.post(
    "https://nl.legal/api/v1/orders",
    json=opdracht,
    headers={
        "Authorization": f"Bearer {os.environ['NLLEGAL_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=60,
)

if response.status_code == 201:
    data = response.json()
    print("Dossier aangemaakt:", data["case"]["case_number"])
else:
    print("Fout:", response.json()["detail"])

Foutafhandeling

Fouten volgen RFC 9457 (application/problem+json). Bij validatiefouten bevat het veld errors per veld wat er mis is, inclusief het exacte pad, zoals invoices[0].amount_cents. Bij elke fout vóór verwerking geldt: er is geen dossier aangemaakt.

Status Code Betekenis
400invalid_json / empty_bodyLege body of ongeldige JSON.
401unauthorizedAPI-key ontbreekt, is ongeldig of ingetrokken.
404not_foundOnbekend endpoint of opdracht-ID bestaat niet voor uw account.
405method_not_allowedVerkeerde HTTP-methode; zie de Allow-header.
409idempotency_conflictIdempotency-Key hergebruikt voor andere inhoud.
413payload_too_largeDe volledige (base64-gecodeerde) aanvraag is groter dan 40 MB.
415unsupported_media_typeContent-Type is geen application/json.
422validation_errorEén of meer velden ongeldig; zie errors.
422invalid_attachmentPDF geweigerd (geen PDF, beschadigd of wachtwoord-beveiligd).
429rate_limitedLimiet bereikt; wacht het aantal seconden in Retry-After.
502upstream_errorDossiersysteem tijdelijk niet beschikbaar; herhaal later met dezelfde Idempotency-Key.

Webhooks

In plaats van te pollen, ontvangt u een melding zodra er iets gebeurt op een van uw dossiers. U laat uw endpoint-URL bij ons instellen en krijgt een signing-secret. Wij sturen dan een POST met een JSON-event naar uw endpoint. Reageer binnen enkele seconden met een 2xx-status; lukt dat niet, dan proberen wij het opnieuw met oplopende tussenpozen (tot 6 keer).

Voorbeeld-event

POST https://uw-systeem.nl/webhooks/nllegal
X-NLLegal-Event: payment.received
X-NLLegal-Timestamp: 1749626412
X-NLLegal-Signature: 9f86d081884c7d659a2feaa0c55ad015...

{
  "id": "evt_2208770e3e58e856",
  "object": "event",
  "type": "payment.received",
  "created_at": "2026-06-11T06:50:12+00:00",
  "data": {
    "order_id": "ord_b2b332c6a34ebb12",
    "case_number": "IN211543",
    "reference": "ORDER-2026-0042",
    "case_event": "case_payments"
  }
}

Handtekening verifiëren

Elke aflevering bevat de headers X-NLLegal-Signature, X-NLLegal-Timestamp en X-NLLegal-Event. Bereken zelf de HMAC en vergelijk timing-safe. Verwerp afwijkende handtekeningen en oude timestamps.

<?php
$secret    = getenv('NLLEGAL_WEBHOOK_SECRET'); // whsec_...
$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NLLEGAL_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_NLLEGAL_TIMESTAMP'] ?? '';

// Verwerp oude berichten (replay-bescherming)
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400); exit;
}

$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
if (!hash_equals($expected, $signature)) {
    http_response_code(401); exit; // ongeldige handtekening
}

$event = json_decode($payload, true);
// ... verwerk $event['type'] en $event['data'] ...
http_response_code(200);

Event-types

payment.receivedEr is een betaling op het dossier geboekt.
correspondence.sentEr is correspondentie (brief/e-mail) verstuurd.
invoice.updatedEen factuur/vordering op het dossier is gewijzigd.
case.updatedHet dossier is bijgewerkt.
case.closedHet dossier is gesloten.

Versie & compatibiliteit

Dit is de eerste release van de API (versie v1). De versie staat in het pad (/api/v1). Niet-brekende toevoegingen, zoals nieuwe velden of waarschuwingscodes, doen wij binnen v1; een eventuele brekende wijziging krijgt een nieuwe major-versie in het pad, zodat uw bestaande integratie blijft werken. Behandel onbekende velden en waarschuwingscodes daarom tolerant.

Klaar om te koppelen?

Vraag vandaag een gratis test-key aan en dien morgen uw eerste opdracht in. Onze Moneybird-koppeling draait op precies dezelfde API, dus de techniek heeft zich al bewezen.

Chat via WhatsApp (opent in nieuw venster)
Chat via WhatsApp
1