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.
<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-vriesis datautobedrijf-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.
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.comtoe bijscript-src,frame-srcenconnect-src. - Een andere vorm voor de knop kan met CSS, bijvoorbeeld
.dutchcalendar-knop::part(knop) { border-radius: 8px; }
Beginnen met de API
- Maak in je dashboard bij Website een sleutel. Begin met een testsleutel.
- Stuur hem mee in elk verzoek, in de header
Authorization: Bearer sk_test_… - Alle adressen beginnen met
https://app.dutchcalendar.com/v1
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/jsonmee. - 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 is2026-10-29. - Veldnamen zijn Engels, meldingen Nederlands. Een
error.messagekun 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.
{
"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, ofgroupvoor een groepsafspraak zoals een open dag, metmax_participantsplekken.- location
on_site,onlineofphone.- requires_resource
- Er hoort een middel bij, zoals een auto.
resource_idszegt welke. - questions
- Eigen vragen, met
id,label,type(text,textarea,selectofcheckbox),requireden bij een keuzelijstoptions. - booking_url
- De boekingspagina, voor als je liever doorlinkt.
GET/v1/resources
De middelen, zoals auto's, kamers of apparaten.
{
"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.
{
"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.
{
"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,emailenphone, 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.
{
"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.
{
"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.
- Voeg in je dashboard bij Website een webhook toe: een https-adres op je website.
- Kies Test (alleen testboekingen) of Live (de echte).
- Bewaar het geheim (
whsec_…) op je server en controleer daarmee elke melding. - Klik op Stuur testmelding.
- booking.created
- Er is een afspraak gemaakt.
- booking.rescheduled
- Verplaatst.
data.previousheeft de oude tijd. - booking.cancelled
- Geannuleerd.
data.cancelled_byiscustomerofbusiness. - ping
- De testmelding uit je dashboard, met een lege data.
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.bookingis de afspraak op het moment van de melding. Meldingen kunnen in een andere volgorde aankomen; wil je zeker de stand van nu, vraag danGET /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.
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.
| Code | Status | Betekenis |
|---|---|---|
| missing_api_key | 401 | Er staat geen sleutel in de header Authorization. |
| invalid_api_key | 401 | De sleutel klopt niet of is ingetrokken. |
| account_blocked | 403 | Dit account is geblokkeerd wegens misbruik. Mail ons als dat niet klopt. |
| rate_limited | 429 | Meer dan 120 verzoeken per minuut. Wacht even. |
| invalid_json | 400 | De body is geen JSON-object. |
| body_too_large | 413 | De body is groter dan 50 kB. |
| invalid_request | 400 | Een veld ontbreekt of klopt niet. Welk veld staat in field. |
| event_type_not_found | 404 | Dit afspraaktype bestaat niet of staat uit. |
| booking_not_found | 404 | Deze afspraak bestaat niet, of hoort bij de andere soort sleutel. |
| invalid_resource | 400 | Dit middel hoort niet bij dit afspraaktype. |
| slot_unavailable | 409 | Deze tijd is niet (meer) vrij. Haal de vrije tijden opnieuw op. |
| booking_cancelled | 409 | De afspraak is geannuleerd en kan niet meer verplaatst worden. |
| booking_started | 409 | De afspraak is al begonnen of voorbij. |
| unconfirmed_limit | 403 | De eigenaar heeft zijn e-mailadres nog niet bevestigd. Tot dan neemt het account hoogstens 5 afspraken aan. |
| daily_limit | 429 | Een account jonger dan 7 dagen neemt hoogstens 50 nieuwe afspraken per dag aan. |
| customer_limit | 429 | Dit e-mailadres heeft vandaag al 5 afspraken gemaakt bij dit bedrijf. |
| server_error | 500 | Er ging bij ons iets mis. Probeer het zo opnieuw. |