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 cardUrl gevuld en kun je de link in je dossier tonen.


MUTATIONwebhookSubscriptionCreate

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
webhookSubscriptionCreate
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.

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.

Had je hier iets aan?