Developers

Technisch integratiedossier v1.2 · OpenAPI JSON

Registreer elke factuur vóór verzending. Plaats daarna de QR-code met verification_url op de PDF of in de e-mail.

POST /api/public/v1/invoices

curl -X POST https://<uw-domein>/api/public/v1/invoices \
  -H "Authorization: Bearer igk_live_XXXXXXXXXXXXXXXX" \
  -F "file=@factuur-2026-0042.pdf" \
  -F 'metadata={
    "invoice_number": "2026-0042",
    "invoice_date": "2026-03-31",
    "due_date": "2026-04-10",
    "supplier_name": "TB Consultancy BV",
    "supplier_vat": "BE0753659316",
    "customer_name": "DocIT Consult BV",
    "customer_vat": "BE0XXXXXXXXX",
    "amount_excl_vat": 11000.00,
    "vat_amount": 2310.00,
    "amount": 13310.00,
    "currency": "EUR",
    "iban": "<goedgekeurd bedrijfs-IBAN>",
    "bic": "KREDBEBB",
    "structured_reference": "+++250/0369/88209+++"
  }'

Antwoord (201)

{
  "duplicate": false,
  "invoice_id": "4b1c…",
  "sha256": "993a3401…",
  "status": "VERIFIED",
  "findings": [],
  "layers": { "ibanFormat": "pass", "approvedSupplierIban": "pass", "canonicalInvoiceMatch": "pass", "vopNameMatch": "not_configured" },
  "qr": { "status": "decoded", "count": 1, "payment": [{ "iban_masked": "BE80 •••• •••• 1377", "amount_cents": 1331000, "currency": "EUR", "reference": "+++250/0369/88209+++", "name": "TB Consultancy BV" }], "other": 0 },
  "verification_token": "igv_…",
  "verification_url": "https://<uw-domein>/v/igv_…"
}

Statussen

  • VERIFIED — alle controles ok.
  • REVIEW — manuele controle aanbevolen.
  • BLOCKED — onbekende of afwijkende IBAN, totaal, mededeling, valuta of betaal-QR; niet versturen/betalen zonder verificatie.

currency is verplicht (ISO 4217, wordt nooit verondersteld). Optioneel: invoice_date, due_date, customer_name, customer_vat, amount_excl_vat, vat_amount, bic, structured_reference. QR-codes worden server-side uit de PDF gelezen en als bewijs bewaard; QR-data in metadata wordt niet aanvaard.

Webhook-events (live)

  • invoice.registration_accepted — canonieke factuur geregistreerd (status, invoice_id, sha256)
  • invoice.registration_rejected — registratie geweigerd (foutcode, geen factuur aangemaakt)
  • finding.created — kritieke afwijking bij een ontvangerscontrole (regels, ernst)
  • verification.checked — ontvanger controleerde een PDF (resultaat VERIFIED/REVIEW/BLOCKED)
  • verification_token.rotated — nieuwe verificatielink; oude QR werkt niet meer
  • verification_token.revoked — verificatielink ingetrokken

Payload bevat nooit een volledig IBAN (alleen laatste 4). Header X-Signature: t=<unix>,v1=<hex> = HMAC-SHA256(geheim, t + "." + ruwe body); weiger ouder dan 5 min. Dedupliceer op X-Event-Id. Antwoord 2xx binnen 5 s; anders tot 8 retries (1 min → 6 u). Endpoints en geheim beheert een beheerder onder Dashboard → Integraties.

Terugkoppeling naar uw software

Twee kanalen met dezelfde events: push (ondertekende webhooks) en pull (events-API met cursor). Aanbevolen: webhooks voor snelheid, en periodiek /events?after= als vangnet zodat geen enkele melding gemist wordt. Toon “beschermd” in uw software alleen zolang protected: true; bij finding.created of verification_token.revoked het label intrekken.

POST <uw endpoint>
X-Event-Id: 6f2c…        X-Event-Type: finding.created
X-Signature: t=1791459000,v1=9b1e…

{ "id": "6f2c…", "sequence": 412, "type": "finding.created",
  "created_at": "2026-10-08T12:30:00Z", "invoice_id": "8f0c…",
  "data": { "invoice_number": "2026036", "external_ref": "…", "severity": "BLOCKED",
            "rules": ["IBAN_MISMATCH"], "found_iban_last4": ["2886"] } }
GET /api/public/v1/events?after=411&limit=100      (scope invoices:read)
→ { "events": [ …envelopes… ], "next_after": 412, "has_more": false }

GET /api/public/v1/invoices/8f0c…
→ { "status": "VERIFIED", "protected": true, "sha256": "…", "iban_masked": "BE10 •••• •••• 6604",
    "verification_link": { "active": true, "url": "…" }, "superseded_by": null,
    "checks": { "total": 3, "blocked": 1, "review": 0 }, "open_findings": [ … ] }
// Node / Workers – webhook-ontvanger
import crypto from "node:crypto";
function verify(secret, header, rawBody) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const exp = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(m[2]));
}

Integratie #1: MyDocIT

MyDocIT is gewoon een API-client. Billver heeft geen afhankelijkheid van de broncode van MyDocIT; andere systemen (Teamleader, Odoo, Exact, Billit) gebruiken dezelfde API.

Aanbevolen flow (twee stappen)

  1. Prepare: MyDocIT vraagt een verificatiereferentie aan → krijgt verification_url + QR.
  2. MyDocIT rendert de definitieve PDF mét die QR.
  3. Finalize: MyDocIT stuurt exact die definitieve PDF + metadata. Billver hasht en bewaart die als canoniek origineel en activeert dan pas de link.
  4. MyDocIT verstuurt exact dezelfde bytes (e-mail/Peppol). Elke wijziging achteraf geeft een hashverschil.

Zo is er geen cirkel “hash, daarna PDF aanpassen”: de URL is vooraf bekend (afgeleid van het gereserveerde ID), maar de hash wordt pas berekend over de definitieve PDF. Bij finalize wordt gecontroleerd of de verificatie-QR in de PDF terug te vinden is (verification_qr_embedded: true / false / null = vector-QR niet uitleesbaar). Eén stap (direct POST /invoices) mag alleen als de QR niet in de PDF komt, bv. enkel in de e-mail.

POST /api/public/v1/invoices/prepare
Authorization: Bearer igk_live_…        (alleen server-side bij MyDocIT)
Idempotency-Key: mydocit-<tenant>-<factuur-id>
X-Source: mydocit

201 {
  "reservation_id": "8f0c…",          // wordt het invoice_id
  "verification_url": "https://<uw-domein>/v/igv_…",
  "qr_svg": "<svg …>",
  "expires_at": "…",                 // 7 dagen
  "note": "Link is pas actief na finalize met de definitieve PDF."
}
POST /api/public/v1/invoices
Authorization: Bearer igk_live_…
Idempotency-Key: mydocit-<tenant>-<factuur-id>
X-Source: mydocit
multipart: file=<DEFINITIEVE PDF mét QR>, reservation_id=8f0c…, metadata={…}

201 { "invoice_id": "8f0c…", "sha256": "…", "status": "VERIFIED",
      "verification_url": "…", "verification_qr_embedded": true,
      "qr_list": [ … ], "findings": [], "protected": true }
200 { "duplicate": true, "idempotent_replay": true, "invoice_id": "8f0c…", … }   // retry
409 { "error": "idempotency_conflict", "protected": false }                       // andere PDF, zelfde sleutel

Idempotentie

Header Idempotency-Key (of external_ref), uniek per tenant. Retry met dezelfde PDF → hetzelfde invoice_id (200). Zelfde sleutel met een andere PDF → 409, nooit een tweede canonieke factuur. Correcties: nieuwe sleutel → nieuwe revisie die de vorige vervangt.

Authenticatie

API-sleutel per tenant, gehasht opgeslagen, intrekbaar, met scopes (invoices:write). Alleen server-side bij MyDocIT bewaren; nooit in een browser, PDF of e-mail.

Fouten & fallback

Elk antwoord zonder 200/201 en status = niet beschermd. Bij time-out, 5xx, 503 of 409: géén QR/“beschermd”-label tonen, retry met dezelfde Idempotency-Key (exponentiële backoff), of verzenden zonder verificatiecue. Toon “beschermd” alleen bij protected: true.

Fouten: 401 ongeldige sleutel · 403 onvoldoende rechten · 422 ongeldige metadata · 409 idempotency_conflict · 429 rate limit · 503 verification_domain_not_configured. Elk foutantwoord bevat "protected": false. Hetzelfde bestand opnieuw registreren is idempotent.