DutchCalendar●Naar je dashboard
Documentatie

Boeken via je eigen website

Er zijn twee manieren. Met de widget plak je één regel code op je website, en staat je boekingsflow daar in de kleur van je huisstijl. Met de API bouwt je programmeur het boeken helemaal in je eigen ontwerp. Allebei geven ze dezelfde vrije tijden als je boekingspagina, en klanten krijgen dezelfde bevestiging en herinnering.

Geen programmeur? Begin dan met de widget, of zet je boekingslink achter een knop op je website.

Widget

Maak de code in je dashboard bij Website en plak hem op de plek waar de widget moet komen. Je hebt geen sleutel nodig: de widget doet precies wat je boekingspagina doet.

Een proefrit plannen, in de pagina
<script src="https://app.dutchcalendar.com/widget.js" data-bedrijf="autobedrijf-de-vries" data-type="proefrit" async></script>
data-bedrijf
Verplicht. Het adres van je boekingspagina: bij https://app.dutchcalendar.com/autobedrijf-de-vries is dat autobedrijf-de-vries.
data-type
Eén afspraaktype, zoals proefrit. Zonder dit veld kiest de klant uit al je afspraaktypes.
data-weergave
pagina (standaard): de boekingsflow staat in je pagina, precies zo hoog als nodig. knop: een knop in de kleur van je huisstijl, die de boekingsflow in een venster opent. Op een telefoon vult dat venster het hele scherm.
data-tekst
De tekst op de knop, maximaal 60 tekens. Standaard "Afspraak maken".

In je websitebouwer

  • WordPress: een blok "Aangepaste HTML".
  • Squarespace: een "Code"-blok.
  • Wix: "Code insluiten". Wix zet code in een vak met een vaste hoogte: maak dat vak hoog genoeg, of zet je boekingslink achter een gewone Wix-knop. De knop-weergave past niet in zo'n vak.
  • Een andere website: plak de code in de HTML, op de plek waar de widget moet komen.

Na een boeking

Je pagina krijgt dan het event dutchcalendar:booked, bijvoorbeeld om een conversie te meten in Google Ads of Analytics. Er staan geen gegevens van de klant in; met het id haal je de afspraak op via de API, als je die gebruikt.

Een boeking meten met Google Analytics
window.addEventListener("dutchcalendar:booked", (event) => {
  // event.detail: { id, organization, event_type, start, end }
  gtag("event", "afspraak_gemaakt", { afspraaktype: event.detail.event_type });
});

Goed om te weten

  • De widget is altijd licht, ook als de bezoeker de donkere modus aan heeft. De kleur stel je in bij Bedrijf.
  • Een afspraak via de widget is echt: de klant krijgt een bevestiging en de afspraak komt in je agenda.
  • Heeft je website een Content Security Policy? Sta dan https://app.dutchcalendar.com toe bij script-src, frame-src en connect-src.
  • Een andere vorm voor de knop kan met CSS, bijvoorbeeld .dutchcalendar-knop::part(knop) { border-radius: 8px; }

Beginnen met de API

  1. Maak in je dashboard bij Website een sleutel. Begin met een testsleutel.
  2. Stuur hem mee in elk verzoek, in de header Authorization: Bearer sk_test_…
  3. Alle adressen beginnen met https://app.dutchcalendar.com/v1
Je eerste verzoek
curl https://app.dutchcalendar.com/v1/event-types \
  -H "Authorization: Bearer sk_test_…"

Wil je eerst zien hoe het eruitziet? De demo van een autobedrijf plant proefritten via de API en laat elk verzoek en antwoord zien.

Goed om te weten

  • Alleen vanaf je server. Met de sleutel kun je afspraken en klantgegevens zien en wijzigen. Zet hem nooit in JavaScript in de browser of in een app. Daarom neemt de API geen verzoeken uit de browser aan.
  • JSON erin en eruit. Stuur bij een POST de header Content-Type: application/json mee.
  • Tijden altijd met tijdzone, zoals 2026-10-29T10:30:00+01:00. Antwoorden staan in de tijdzone van je bedrijf: in de winter +01:00, in de zomer +02:00. Een datum is 2026-10-29.
  • Veldnamen zijn Engels, meldingen Nederlands. Een error.message kun je zo aan je klant laten zien.
  • Maximaal 120 verzoeken per minuut per sleutel.

Test-modus

Er zijn twee soorten sleutels.

sk_test_…
Boekingen zijn testboekingen. Niemand krijgt een e-mail of herinnering, ze komen niet in agenda's en niet bij Afspraken in je dashboard. Ze zien wel de echte vrije tijden, en houden een tijd bezet voor andere testboekingen (niet voor echte klanten). 30 dagen na de afspraak verdwijnen ze vanzelf; eerder wissen kan bij Website.
sk_live_…
Echte boekingen, precies als via je boekingspagina: met bevestiging, herinnering en de afspraak in de agenda. Ook de grenzen tegen misbruik gelden dan, zoals bij de boekingspagina (zie Foutcodes); voor een testsleutel niet.

Een testsleutel ziet alleen testboekingen en een live sleutel alleen echte. Een test kan dus nooit een echte afspraak verplaatsen of annuleren. Elke afspraak heeft "test": true of false.

Afspraaktypes en middelen

GET/v1/event-types

De afspraaktypes die klanten kunnen boeken.

Antwoord
{
  "data": [
    {
      "id": "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13",
      "name": "Proefrit",
      "slug": "proefrit",
      "description": "Maak een proefrit in de auto van je keuze. Neem je rijbewijs mee.",
      "duration_minutes": 30,
      "kind": "individual",
      "max_participants": 1,
      "location": "on_site",
      "requires_resource": true,
      "resource_ids": [
        "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
        "c3d5f7a9-2b4d-4f6a-9b8c-0d1e2f3a4b5c"
      ],
      "questions": [],
      "booking_url": "https://app.dutchcalendar.com/autobedrijf-de-vries/proefrit"
    }
  ]
}
kind
individual, of group voor een groepsafspraak zoals een open dag, met max_participants plekken.
location
on_site, online of phone.
requires_resource
Er hoort een middel bij, zoals een auto. resource_ids zegt welke.
questions
Eigen vragen, met id, label, type (text, textarea, select of checkbox), required en bij een keuzelijst options.
booking_url
De boekingspagina, voor als je liever doorlinkt.

GET/v1/resources

De middelen, zoals auto's, kamers of apparaten.

Antwoord
{
  "data": [
    {
      "id": "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
      "name": "Volkswagen Golf 8",
      "type": "vehicle",
      "event_type_ids": [
        "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13"
      ]
    },
    {
      "id": "c3d5f7a9-2b4d-4f6a-9b8c-0d1e2f3a4b5c",
      "name": "Volkswagen ID.3",
      "type": "vehicle",
      "event_type_ids": [
        "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13"
      ]
    }
  ]
}

type is vehicle, room, equipment of other.

Vrije tijden

GET/v1/availability?event_type=6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13&from=2026-10-29&to=2026-10-30

De tijden waarop een klant kan beginnen.

event_type
Verplicht: het id van het afspraaktype.
from, to
Datums, allebei inclusief, in de tijdzone van je bedrijf. Standaard vandaag en de 6 dagen erna; maximaal 31 dagen per verzoek.
resource
Alleen tijden waarop dit middel vrij is.
Antwoord
{
  "event_type": "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13",
  "from": "2026-10-29",
  "to": "2026-10-30",
  "time_zone": "Europe/Amsterdam",
  "slots": [
    {
      "start": "2026-10-29T09:00:00+01:00",
      "end": "2026-10-29T09:30:00+01:00",
      "spots_left": 1,
      "resource_ids": [
        "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
        "c3d5f7a9-2b4d-4f6a-9b8c-0d1e2f3a4b5c"
      ]
    },
    {
      "start": "2026-10-29T09:45:00+01:00",
      "end": "2026-10-29T10:15:00+01:00",
      "spots_left": 1,
      "resource_ids": [
        "c3d5f7a9-2b4d-4f6a-9b8c-0d1e2f3a4b5c"
      ]
    }
  ]
}

spots_left is het aantal plekken dat nog vrij is (bij een groepsafspraak meer dan 1). resource_ids zijn de middelen die op dat moment vrij zijn. Klanten boeken van 1 uur tot 60 dagen vooruit; daarbuiten komen er geen tijden.

Een tijd kan net weg zijn als je klant boekt. Dan krijg je slot_unavailable: haal de vrije tijden opnieuw op.

Boeken

POST/v1/bookings

Een afspraak maken. Net als op je boekingspagina krijgt een collega die op dat moment vrij is de afspraak. Zijn er meer vrij, dan krijgt wie die week de minste afspraken heeft hem; testafspraken tellen alleen mee voor andere testafspraken.

Body
{
  "event_type": "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13",
  "start": "2026-10-29T09:00:00+01:00",
  "resource": "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
  "customer": {
    "name": "Sanne Bakker",
    "email": "sanne@voorbeeld.nl",
    "phone": "06 12345678"
  },
  "answers": {}
}
event_type, start
Verplicht. De start is een van de vrije tijden.
resource
Het middel dat de klant koos. Laat je het weg, dan krijgt de klant een middel dat vrij is.
customer
Verplicht: name, email en phone, met dezelfde controles als op je boekingspagina.
answers
Antwoorden op de eigen vragen, met het id van de vraag als sleutel: tekst als string, een vinkje als true.
Antwoord: 201
{
  "id": "e5f7b9c1-4d6f-4b8c-9d0e-2f3a4b5c6d7e",
  "status": "confirmed",
  "event_type": {
    "id": "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13",
    "name": "Proefrit"
  },
  "start": "2026-10-29T09:00:00+01:00",
  "end": "2026-10-29T09:30:00+01:00",
  "host": {
    "id": "d4e6a8b0-3c5e-4a7b-8c9d-1e2f3a4b5c6d",
    "name": "Pieter Bakker"
  },
  "resource": {
    "id": "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
    "name": "Volkswagen Golf 8"
  },
  "customer": {
    "name": "Sanne Bakker",
    "email": "sanne@voorbeeld.nl",
    "phone": "06 12345678"
  },
  "answers": {},
  "meeting_url": null,
  "test": false,
  "created_at": "2026-10-26T14:02:11+01:00",
  "cancelled_at": null
}

Met een live sleutel krijgt de klant meteen een bevestiging met een link om te verplaatsen of te annuleren, en een dag van tevoren een herinnering. status is confirmed of cancelled.

meeting_url is bij een online afspraak de link van het videogesprek: Microsoft Teams, Google Meet of de vaste link van de collega. Teams en Meet maken die een paar seconden na het boeken; tot dan is hij null. De webhooks booking.created en booking.rescheduled wachten erop, hoogstens een minuut.

GET/v1/bookings/{id}

Een afspraak opvragen, met de stand van nu.

POST/v1/bookings/{id}/reschedule

Verplaatsen naar een andere vrije tijd. De afspraak blijft bij dezelfde collega als die dan vrij is. Het antwoord is de afspraak met de nieuwe tijd.

Body
{
  "start": "2026-10-30T14:00:00+01:00"
}

Laat je resource weg, dan blijft het middel hetzelfde; met null krijgt de klant een middel dat vrij is.

POST/v1/bookings/{id}/cancel

Annuleren, zonder body. Het antwoord is de afspraak met "status": "cancelled". Twee keer annuleren is geen fout.

Een afspraak die al begonnen is, kun je niet meer verplaatsen of annuleren. Bij een live sleutel krijgt de klant bij elke wijziging een e-mail.

Webhooks

Met een webhook hoort je website het meteen als er iets verandert: ook bij een boeking via de widget of je boekingspagina, als de klant zelf verplaatst via de link in de bevestiging, of als je een afspraak aanpast in het dashboard.

  1. Voeg in je dashboard bij Website een webhook toe: een https-adres op je website.
  2. Kies Test (alleen testboekingen) of Live (de echte).
  3. Bewaar het geheim (whsec_…) op je server en controleer daarmee elke melding.
  4. Klik op Stuur testmelding.
booking.created
Er is een afspraak gemaakt.
booking.rescheduled
Verplaatst. data.previous heeft de oude tijd.
booking.cancelled
Geannuleerd. data.cancelled_by is customer of business.
ping
De testmelding uit je dashboard, met een lege data.
Zo komt een melding binnen
POST /webhooks/dutchcalendar
Content-Type: application/json
DutchCalendar-Signature: t=1793088900,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "f6a8c0d2-5e7a-4c9d-8e1f-3a4b5c6d7e8f",
  "type": "booking.rescheduled",
  "created_at": "2026-10-27T09:15:00+01:00",
  "test": false,
  "data": {
    "booking": {
      "id": "e5f7b9c1-4d6f-4b8c-9d0e-2f3a4b5c6d7e",
      "status": "confirmed",
      "event_type": {
        "id": "6f1d3c2a-8b4e-4f7a-9c1d-2e5b7a9c0d13",
        "name": "Proefrit"
      },
      "start": "2026-10-30T14:00:00+01:00",
      "end": "2026-10-30T14:30:00+01:00",
      "host": {
        "id": "d4e6a8b0-3c5e-4a7b-8c9d-1e2f3a4b5c6d",
        "name": "Pieter Bakker"
      },
      "resource": {
        "id": "b2c4e6f8-1a3c-4e5f-8a7b-9c0d1e2f3a4b",
        "name": "Volkswagen Golf 8"
      },
      "customer": {
        "name": "Sanne Bakker",
        "email": "sanne@voorbeeld.nl",
        "phone": "06 12345678"
      },
      "answers": {},
      "meeting_url": null,
      "test": false,
      "created_at": "2026-10-26T14:02:11+01:00",
      "cancelled_at": null
    },
    "previous": {
      "start": "2026-10-29T09:00:00+01:00",
      "end": "2026-10-29T09:30:00+01:00"
    }
  }
}

Antwoorden en nieuwe pogingen

  • Antwoord binnen 10 seconden met een status in de 200. Doe zwaar werk pas daarna.
  • Lukt dat niet, dan proberen we het opnieuw na 1 minuut, 5 minuten, 30 minuten, 2 uur en 12 uur. Daarna staat de melding als mislukt bij Website in je dashboard.
  • We volgen geen doorverwijzing (zoals 301 of 302): geef het adres waar de melding echt moet zijn.
  • Elke melding heeft een eigen id. Komt hetzelfde id twee keer binnen, verwerk het dan één keer.
  • data.booking is de afspraak op het moment van de melding. Meldingen kunnen in een andere volgorde aankomen; wil je zeker de stand van nu, vraag dan GET /v1/bookings/{id}.

De handtekening controleren

Controleer bij elke melding dat hij echt van ons komt. De header DutchCalendar-Signature heeft t, het moment in Unix-seconden, en v1: een HMAC-SHA256 met je geheim over t, een punt en de body, in hex. Reken hetzelfde uit en vergelijk. Is t ouder dan 5 minuten, weiger de melding dan: zo kan niemand een oude melding opnieuw sturen.

Node.js met Express
import crypto from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.DUTCHCALENDAR_WEBHOOK_SECRET; // whsec_…

// Lees de body als tekst: de handtekening hoort bij precies deze tekens.
app.post("/webhooks/dutchcalendar", express.text({ type: "application/json" }), (req, res) => {
  const header = req.get("DutchCalendar-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${req.body}`)
    .digest("hex");
  const recent = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid =
    recent &&
    typeof parts.v1 === "string" &&
    parts.v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!valid) return res.status(400).send("Ongeldige handtekening");

  const event = JSON.parse(req.body);
  if (event.type === "booking.created") {
    // Zet de afspraak in je eigen systeem.
  }
  res.sendStatus(200);
});

app.listen(3000);

Foutcodes

Gaat er iets mis, dan krijg je een status van 400 of hoger en dit antwoord:

{
  "error": {
    "code": "slot_unavailable",
    "message": "Dit tijdstip is niet (meer) vrij. Kies een ander tijdstip.",
    "field": "start"
  }
}

code verandert nooit: daar let je code op. message is voor je klant. field staat erbij als het om één veld gaat, zoals customer.email.

CodeStatusBetekenis
missing_api_key401Er staat geen sleutel in de header Authorization.
invalid_api_key401De sleutel klopt niet of is ingetrokken.
account_blocked403Dit account is geblokkeerd wegens misbruik. Mail ons als dat niet klopt.
rate_limited429Meer dan 120 verzoeken per minuut. Wacht even.
invalid_json400De body is geen JSON-object.
body_too_large413De body is groter dan 50 kB.
invalid_request400Een veld ontbreekt of klopt niet. Welk veld staat in field.
event_type_not_found404Dit afspraaktype bestaat niet of staat uit.
booking_not_found404Deze afspraak bestaat niet, of hoort bij de andere soort sleutel.
invalid_resource400Dit middel hoort niet bij dit afspraaktype.
slot_unavailable409Deze tijd is niet (meer) vrij. Haal de vrije tijden opnieuw op.
booking_cancelled409De afspraak is geannuleerd en kan niet meer verplaatst worden.
booking_started409De afspraak is al begonnen of voorbij.
unconfirmed_limit403De eigenaar heeft zijn e-mailadres nog niet bevestigd. Tot dan neemt het account hoogstens 5 afspraken aan.
daily_limit429Een account jonger dan 7 dagen neemt hoogstens 50 nieuwe afspraken per dag aan.
customer_limit429Dit e-mailadres heeft vandaag al 5 afspraken gemaakt bij dit bedrijf.
server_error500Er ging bij ons iets mis. Probeer het zo opnieuw.