Webhooks
Ontvang realtime events via HTTP POST naar jouw endpoint
Webhooks ontvangen realtime events via HTTP POST naar een door jou geconfigureerde URL. Dezelfde events zijn ook beschikbaar via SSE. Webhooks zijn per organisatie geconfigureerd - elke organisatie heeft zijn eigen webhooks.
Beheer
Zie de OpenAPI-specificatie voor volledige request- en response-schema's.
| Method | Pad | Beschrijving |
|---|---|---|
| GET | /api/orgs/:orgId/webhooks | Webhooks ophalen |
| GET | /api/orgs/:orgId/webhooks/delivery-stats | Leveringsstatistieken ophalen |
| POST | /api/orgs/:orgId/webhooks/retry-deliveries | Leveringen opnieuw proberen. Body: mode (failed of force) en optioneel id voor één webhook. failed: alleen definitief mislukte leveringen, direct. force: wachtende en definitief mislukte leveringen op de achtergrond, negeert wachttijden. |
| GET | /api/orgs/:orgId/webhooks/:id | Webhook op ID ophalen |
| POST | /api/orgs/:orgId/webhooks | Webhook aanmaken |
| PATCH | /api/orgs/:orgId/webhooks/:id | Webhook bijwerken |
| DELETE | /api/orgs/:orgId/webhooks/:id | Webhook verwijderen |
Webhook aanmaken
POST /api/orgs/org_abc123/webhooks
Content-Type: application/json
x-api-key: tkn_xxx
X-Tillor-Org-Id: org_abc123{
"url": "https://jouw-server.com/webhooks/tillor",
"subscribedEventKeys": ["invoice:created", "invoice:paid", "customer:updated"],
"enabled": true
}- url - HTTPS-endpoint die POST-requests accepteert
- subscribedEventKeys - Array van event keys of
["*"]voor alle events. De volledige lijst staat in Tillor onder Profielmenu → Ontwikkelaars bij Webhook toevoegen → Geabonneerde gebeurtenissen. - enabled -
trueom levering in te schakelen
Je mag alleen events kiezen waarvoor je organisatielid de juiste permissie heeft (bijv. bankoverschrijvingen voor bankAccountTransaction:*). Wildcard * is alleen beschikbaar voor leden met volledige toegang (*). Bij een ongeldige combinatie geeft de API een foutmelding.
Webhook-payload
Elke webhook-levering is een POST met:
Headers:
Content-Type: application/jsonX-Webhook-Event-ID- stabiele unieke ID voor deze logische levering (zelfde waarde bij retries)X-Webhook-Signature- HMAC-SHA256 handtekening van de body, formaatsha256=<hex>(zie Handtekening verifiëren)
Body:
{
"event": "invoice:created",
"data": {},
"timestamp": 1735689600000,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7"
}| Veld | Type | Beschrijving |
|---|---|---|
event | string | Event key (bijv. invoice:created) |
data | object | Event-specifieke payload |
timestamp | number | Unix timestamp (ms) wanneer het event werd uitgezonden |
traceId | string | OpenTelemetry trace-id van de Tillor-actie die dit event veroorzaakte. Geef deze door bij support; het is geen geheim. |
spanId | string (optioneel) | Span binnen die trace |
Zie Voorbeelden voor volledige payload-voorbeelden.
Handtekening verifiëren
Elke webhook bevat een eigen signing secret (64 hex-karakters, zichtbaar in de webhooktabel via Ondertekeningssleutel tonen op Profielmenu → Ontwikkelaars). Tillor ondertekent elke POST met die secret zodat jij kunt verifiëren dat het verzoek écht van Tillor komt en dat de body niet is aangepast.
Hoe Tillor ondertekent:
- Bereken
HMAC-SHA256(signingSecret, raw_request_body)(lowercase hex). - Stuur dat als
X-Webhook-Signature: sha256=<hex>.
Hoe jij verifieert:
- Lees de ruwe request-body (vóór JSON-parsen - anders matcht de hash niet).
- Bereken zelf
HMAC-SHA256(signingSecret, raw_body)in hex. - Vergelijk met de waarde achter
sha256=uit deX-Webhook-Signatureheader met een constant-time vergelijking.
import crypto from "node:crypto";
function verifyTillorWebhook(rawBody, headerSignature, signingSecret) {
const expected = crypto.createHmac("sha256", signingSecret).update(rawBody).digest("hex");
const received = headerSignature.replace(/^sha256=/, "");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Behandel de secret als wachtwoord
De signing secret kun je in Tillor tonen via Ondertekeningssleutel tonen in de webhooktabel. Sla 'm op als secret (env var, vault) en log 'm nooit. Vermoed je een lek? Verwijder de webhook en maak een nieuwe aan; daarmee krijg je een nieuwe secret.
Leveringsgedrag en retries
- Antwoord met een 2xx-status om succes te bevestigen.
- Bij een mislukte levering (niet-2xx, netwerkfout of geblokkeerd doel) probeert Tillor de levering automatisch opnieuw met steeds langere tussenpozen. Elke poging hergebruikt dezelfde payload, dezelfde handtekening en hetzelfde event-id (idempotent).
| Poging | Wachttijd sinds vorige poging |
|---|---|
| 1 | direct |
| 2 | 1 seconde |
| 3 | 2 seconden |
| 4 | 4 seconden |
| 5 | 8 seconden |
| 6 | 5 minuten |
| 7 | 30 minuten |
| 8 | 2 uur |
| 9 | 6 uur |
| 10 | 24 uur |
- Slaagt één van de pogingen (2xx) → de levering is afgerond.
- Slaagt ook poging 10 (na 24u) niet → de levering wordt definitief gemarkeerd als mislukt en niet verder geprobeerd.
- Wordt de webhook intussen uitgeschakeld, dan stopt Tillor met opnieuw proberen.
Idempotentie
Elke retry hergebruikt dezelfde X-Webhook-Event-ID en X-Webhook-Signature. Dedupliceer aan jouw kant op die header zodat een succesvolle retry na een trage 200 geen dubbele verwerking veroorzaakt.
Meldingen bij aanhoudende fouten
Als leveringen voor een webhook-endpoint blijven mislukken, stuurt Tillor maximaal twee meldingen per incident (per endpoint):
- Na 5 minuten aanhoudende fouten - e-mail naar het organisatie-e-mailadres (
supportEmail, andersemail) en naar organisatieleden met rechten om organisatie-instellingen te wijzigen, plus push (indien ingeschakeld) voor die leden. De e-mail vermeldt sinds wanneer de leveringen mislukken. Datums in de e-mail gebruiken de tijdzone uit Instellingen > Algemeen (standaardEurope/Brussels). - Bij definitieve mislukking - dezelfde ontvangers krijgen één melding wanneer het eerste event in dat incident na alle pogingen definitief mislukt is gemarkeerd.
Je krijgt geen melding per mislukt event. Zodra alle mislukte leveringen voor een endpoint zijn opgelost (succesvol afgeleverd), start een volgend incident opnieuw met dezelfde regels.
Op Profielmenu → Ontwikkelaars toont de webhooktabel per endpoint een compacte leveringsstatus (aantallen wachtend op opnieuw proberen en definitief mislukt). Klik op het ℹ-icoon voor details, inclusief de volledige foutmelding en de payload van de meest recente poging.
- Mislukte opnieuw proberen (↻): alleen definitief mislukte leveringen voor die webhook.
- Forceer opnieuw proberen (⚡): alle wachtende én definitief mislukte leveringen voor die webhook, ongeacht geplande wachttijden. Tillor start dit op de achtergrond zodat grote wachtrijen (bijv. duizenden events) veilig verwerkt worden.
- Alles forceer opnieuw proberen rechtsboven naast Webhook toevoegen: hetzelfde voor alle ingeschakelde webhooks in de organisatie.
Event types
Event keys gebruiken het formaat entity:action of entity:subresource:action. Hieronder de volledige lijst; dezelfde keys staan in Tillor onder Profielmenu → Ontwikkelaars bij Webhook toevoegen → Geabonneerde gebeurtenissen.
| Event Key | Beschrijving |
|---|---|
* (wildcard) | Alle events (alleen met volledige org-permissie) |
accessLogEntry:coupled | Toegangslog-entry gekoppeld aan klant of toegangsmethode |
accessLogEntry:processed | Toegangslog-entry verwerkt |
accessMethod:created | Toegangsmethode aangemaakt |
accessMethod:deleted | Toegangsmethode verwijderd |
accessMethod:updated | Toegangsmethode bijgewerkt |
accessMethodOperation:created | Toegangsmethode-operatie aangemaakt |
accessMethodOperation:updated | Toegangsmethode-operatie bijgewerkt |
barrier:created | Barrière aangemaakt |
barrier:deleted | Barrière verwijderd |
barrier:updated | Barrière bijgewerkt |
bankAccountTransaction:imported | Banktransacties geïmporteerd of gesynchroniseerd |
call:created | Gesprek aangemaakt |
call:updated | Gesprek bijgewerkt |
comment:created | Reactie aangemaakt |
comment:deleted | Reactie verwijderd |
comment:mentioned | Gebruiker genoemd in reactie |
comment:pinned | Reactie vastgepind |
comment:resolved | Reactie opgelost |
comment:updated | Reactie bijgewerkt |
controller:adoption:approved | Controller-adoptie goedgekeurd |
controller:adoption:rejected | Controller-adoptie afgewezen |
controller:adoption:requested | Controller-adoptie aangevraagd |
controller:created | Controller aangemaakt |
controller:deleted | Controller verwijderd |
controller:logs:submitted | Controller-logs ingediend |
controller:updated | Controller bijgewerkt |
customer:contact:created | Klantcontact aangemaakt |
customer:contact:deleted | Klantcontact verwijderd |
customer:contact:updated | Klantcontact bijgewerkt |
customer:created | Klant aangemaakt |
customer:deleted | Klant verwijderd |
customer:updated | Klant bijgewerkt |
document:created | Document aangemaakt |
document:deleted | Document verwijderd |
document:updated | Document bijgewerkt |
event-log:created | Eventlog-entry aangemaakt |
event-log:updated | Eventlog bijgewerkt |
identityDocumentScan:completed | Identiteitsdocument-scan voltooid |
identityDocumentScan:deleted | Identiteitsdocument-scan verwijderd |
identityDocumentScan:linked | Identiteitsdocument-scan gekoppeld aan klant |
identityDocumentScan:pendingQueueChanged | Wachtrij met openstaande scans gewijzigd |
identityDocumentScan:unlinked | Identiteitsdocument-scan ontkoppeld van klant |
inbox:message-created | Nieuw inbox-bericht (inkomend of uitgaand) |
inbox:thread-updated | Inbox-gesprek bijgewerkt (preview, archief, laatste bericht) |
invoice:created | Factuur aangemaakt |
invoice:deleted | Factuur verwijderd |
invoice:paid | Factuur betaald |
invoice:updated | Factuur bijgewerkt |
mandate:created | Machtiging aangemaakt |
mandate:deleted | Machtiging verwijderd |
mandate:updated | Machtiging bijgewerkt |
meter-alarm:created | Meteralarm aangemaakt |
meter-alarm:resolved | Meteralarm opgelost |
meter:updated | Meter bijgewerkt |
nfc-tag:blocked | NFC-tag geblokkeerd |
nfc-tag:presented | NFC-tag gepresenteerd |
nfc-tag:unblocked | NFC-tag gedeblokkeerd |
nfc-tag:updated | NFC-tag bijgewerkt |
notification-delivery:updated | Notificatielevering bijgewerkt |
payment-group:assigned-to-invoice | Betalingsgroep aan factuur gekoppeld |
payment-group:created | Betalingsgroep aangemaakt |
payment-group:deleted | Betalingsgroep verwijderd |
payment-group:payments-added | Betalingen toegevoegd aan betalingsgroep |
payment-group:payments-removed | Betalingen verwijderd uit betalingsgroep |
payment-group:unassigned-from-invoice | Betalingsgroep ontkoppeld van factuur |
payment-group:updated | Betalingsgroep bijgewerkt |
payment-report:updated | Betalingsrapport bijgewerkt |
payment:created | Betaling aangemaakt |
payment:deleted | Betaling verwijderd |
payment:updated | Betaling bijgewerkt |
img:clear-tablet | Afbeelding gewist van tablet |
img:to-tablet | Afbeelding naar tablet verzonden |
pdf:clear-tablet | PDF gewist van tablet |
pdf:to-tablet | PDF naar tablet verzonden |
print-queue-job:updated | Printwachtrijtaak bijgewerkt |
reservation:created | Reservering aangemaakt |
reservation:updated | Reservering bijgewerkt |
terrain:updated | Terrein bijgewerkt |
Webhooks vs SSE
| Use case | Aanbeveling |
|---|---|
| Server-side integratie | Webhooks - Jouw server ontvangt POSTs |
| Eenvoudige logging | Webhooks - Minimale setup |
| Realtime dashboard, live updates in app | SSE - Eén verbinding, lage latentie. |
| Veel event types, client-side filteren | SSE - Gebruik events query-param om verkeer te beperken. |