# Webhooks

Cijfr POST gebeurtenissen naar jouw HTTPS-endpoint, ondertekend met een eigen secret. Tot vijf pogingen met oplopende pauze.

## Endpoint instellen

Onder Instellingen, dan Integraties maak je een webhook met een HTTPS-URL en de events die je wilt ontvangen. Je krijgt één keer een `whsec_…`-secret om handtekeningen mee te controleren.

## Events

| Event | Wanneer |
| --- | --- |
| `invoice.sent` | Een factuur is uitgegeven en verstuurd |
| `invoice.viewed` | De ontvanger opende de publieke pagina |
| `invoice.paid` | De factuur is volledig betaald, ook via een checkout |
| `invoice.overdue` | De vervaldatum is voorbij |
| `invoice.reminder` | Een herinnering of aanmaning is verstuurd |
| `quote.accepted` | De klant accepteerde de offerte |
| `quote.declined` | De klant wees de offerte af |

## Payload en handtekening

Elke levering is een `POST` met JSON-body en twee headers: `X-Cijfr-Event` met de eventnaam en `X-Cijfr-Signature` met `sha256=<hmac>`. De HMAC is de hex van `HMAC-SHA256(secret, ruwe body)`.

```json
{
  "event": "invoice.paid",
  "occurredAt": "2026-03-01T10:24:11.000Z",
  "data": {
    "documentId": "7f2b…",
    "amount": 12100,
    "method": "ideal"
  }
}
```

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCijfrWebhook(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Lees de ruwe body, niet de geparste JSON:
// const ok = verifyCijfrWebhook(rawBody, req.headers["x-cijfr-signature"], secret);
```

## Retries

Antwoordt je endpoint niet met een `2xx`, dan probeert Cijfr het opnieuw: na ongeveer 1, 5 en 15 minuten, daarna na 1 en 4 uur, maximaal vijf pogingen. De payload van een retry is identiek, dus maak je endpoint idempotent op `data.documentId` plus `event`.

