Webhooks
Zodra er iets met een kaart gebeurt sturen wij een bericht naar een adres dat jij opgeeft. Daarmee hoef je niet te pollen en blijft je dossier vanzelf gelijklopen.
De onderwerpen
- Name
CARDS_CREATE- Type
- cards/create
- Description
Er is een kaart aangemaakt. Handig als meerdere systemen bij hetzelfde partneraccount horen.
- Name
CARDS_UPDATE- Type
- cards/update
- Description
Een kaart is via de API gewijzigd.
- Name
CARDS_ORDERED- Type
- cards/ordered
- Description
De familie heeft besteld. Dit is het moment waarop jouw dossier van "in bewerking" naar "besteld" kan.
- Name
CARDS_ACTIVATED- Type
- cards/activated
- Description
De openbare kaart staat online. Vanaf nu is
cardUrlgevuld en kun je de link in je dossier tonen.
Bestellen en live gaan vallen vandaag op hetzelfde moment, maar het zijn twee gebeurtenissen en je krijgt ze als twee meldingen. Dat is bewust: als er ooit iets tussen komt te zitten, verandert er niets aan wat jij ontvangt.
Abonneren
Eén adres per onderwerp. Het adres moet openbaar bereikbaar zijn en over https gaan; adressen binnen een privénetwerk worden geweigerd.
Je kunt dit ook aan ons overlaten bij het aanvragen van je sleutel; dan zetten wij het meteen goed.
Zie Webhook-abonnementen voor opvragen en opzeggen.
Verzoek
mutation CreateWebhookSubscription(
$topic: WebhookSubscriptionTopic!
$subscription: WebhookSubscriptionInput!
) {
webhookSubscriptionCreate(
topic: $topic
webhookSubscription: $subscription
) {
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
}
userErrors {
field
message
code
}
}
}
Wat je ontvangt
Een POST met de kaart erin, in precies dezelfde vorm als card in de API.
Wat je uit een webhook haalt, kun je dus één op één vergelijken met wat een
query teruggeeft.
Headers
- Name
X-Rouwkaart-Topic- Type
- string
- Description
Het onderwerp, bijvoorbeeld
cards/ordered.
- Name
X-Rouwkaart-Webhook-Id- Type
- uuid
- Description
Uniek per bezorging. Hierop ontdubbel je.
- Name
X-Rouwkaart-Api-Version- Type
- string
- Description
2026-10.
- Name
X-Rouwkaart-Hmac-Sha256- Type
- base64
- Description
De handtekening over de ruwe body.
De body van cards/activated
{
"card": {
"id": "gid://rouwkaart/Card/069a4543-62ed-442a-b69f-489b3182431a",
"externalDossierId": "HS-2026-0042",
"externalFuneralDirectorId": "UO-7",
"status": "ACTIVE",
"cardUrl": "https://memoriam.rouwkaart-online.nl/memoriam/jan-jansen-1940-05-01",
"deceased": {
"firstName": "Jan",
"lastName": "Jansen",
"dateOfBirth": "1940-05-01",
"dateOfDeath": "2026-09-18",
"photoUrl": "https://media.rouwkaart-online.nl/memoriam/069a…/portret.jpg"
},
"livestreamUrl": "https://kerkdienstgemist.nl/jan",
"familyContact": {
"name": "Marieke de Vries",
"email": "marieke@voorbeeld.nl"
},
"test": false,
"createdAt": "2026-09-20T10:00:00.000Z",
"updatedAt": "2026-09-22T09:14:03.000Z"
}
}
Verifiëren
Reken de handtekening uit over de ruwe body, vóór je de JSON parseert. Parse je eerst en serialiseer je daarna opnieuw, dan verandert er een spatie en klopt de som niet meer.
Gebruik een vergelijking met vaste looptijd (timingSafeEqual,
hash_equals). Een gewone === verraadt met zijn snelheid hoeveel tekens
er klopten.
Klopt de handtekening niet, antwoord dan met 401 en doe verder niets.
Controleren
import {createHmac, timingSafeEqual} from 'node:crypto'
export function isVanRouwkaartOnline(rawBody, header, webhookSecret) {
const verwacht = createHmac('sha256', webhookSecret)
.update(rawBody, 'utf8')
.digest('base64')
const a = Buffer.from(verwacht)
const b = Buffer.from(header ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}
Een complete ontvanger (Express)
import express from 'express'
const app = express()
// Let op: express.raw, niet express.json. Je hebt de ruwe bytes nodig.
app.post(
'/hooks/rouwkaart',
express.raw({type: 'application/json'}),
async (req, res) => {
const handtekening = req.get('X-Rouwkaart-Hmac-Sha256')
if (!isVanRouwkaartOnline(req.body, handtekening, SECRET)) {
return res.sendStatus(401)
}
const bezorgId = req.get('X-Rouwkaart-Webhook-Id')
if (await alVerwerkt(bezorgId)) return res.sendStatus(200)
const {card} = JSON.parse(req.body.toString('utf8'))
await verwerk(req.get('X-Rouwkaart-Topic'), card)
await onthoud(bezorgId)
res.sendStatus(200)
},
)
Antwoorden en opnieuw proberen
Antwoord met een 2xx en doe het snel. Duurt je antwoord langer dan tien
seconden, dan beschouwen we de bezorging als mislukt. Zet zwaar werk dus
achter een wachtrij en antwoord meteen.
Bij een andere status of een timeout proberen wij het opnieuw, met oplopende wachttijden:
- Name
Poging 2- Type
- na 1 minuut
- Description
- Name
Poging 3- Type
- na 5 minuten
- Description
- Name
Poging 4- Type
- na 15 minuten
- Description
- Name
Poging 5- Type
- na 1 uur
- Description
- Name
Poging 6- Type
- na 3 uur
- Description
- Name
Poging 7- Type
- na 6 uur
- Description
- Name
Poging 8- Type
- na 24 uur
- Description
Daarna stoppen we. De gebeurtenis is dan niet weg; je kunt de stand altijd
ophalen met een card-query.
Bezorging is minstens één keer. Een melding die jij verwerkte maar
waarvan het antwoord onderweg verdween, komt terug. Bewaar daarom
X-Rouwkaart-Webhook-Id en sla een id over dat je al kent.
Ontdubbelen
// Dit id is uniek per bezorging, niet per abonnement.
const bezorgId = req.get('X-Rouwkaart-Webhook-Id')
const nieuw = await db.webhookBezorgingen.insertIfAbsent(bezorgId)
if (!nieuw) {
// Al gezien. Netjes bevestigen en verder niets doen.
return res.sendStatus(200)
}
Oefenen zonder te wachten
In de Postman-collectie zit een request die een correct ondertekende melding naar je eigen endpoint stuurt, met je eigen webhook-secret. Lukt de verificatie daarmee, dan lukt hij straks ook echt, zonder dat je op een bestelling hoeft te wachten.