Aan de slag
- Maak een sleutel aan via Instellen → Integraties (op Starter, Pro en tijdens de proefperiode). Sleutels beginnen met
qh_ en worden één keer getoond. - Test hem:
GET /v1/me antwoordt met de naam van je organisatie. - Bouw verder met de naslag hieronder, of koppel Zapier zonder code te schrijven.
curl "https://qouh.com/api/v1/me" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Sleutels en rechten
Stuur de sleutel mee in de header Authorization: Bearer van elk verzoek, via https. Een sleutel kan alleen wat je toestond toen je hem maakte:
bookings:read: Afspraken en vrije tijden bekijkenbookings:write: Afspraken maken, verplaatsen en annulerencustomers:read: Klanten bekijkencustomers:write: Klanten toevoegen en bijwerkenwebhooks:manage: Webhooks instellen (voor Zapier)
Elke sleutel mag 120 verzoeken per minuut doen; daarboven antwoordt de API met 429 en een header Retry-After. Een sleutel die je intrekt op de pagina Integraties werkt meteen niet meer.
Tijden en pagina’s
Tijden zijn ISO 8601 in UTC. Lijsten komen per pagina: geef de nextCursor uit het antwoord mee als cursor tot die null is.
Fouten
Een fout antwoordt met een HTTP-status en een body met een vaste code en een leesbare message:
{
"error": {
"code": "slot_unavailable",
"message": "This time is not offered (any more). Pick one from GET /v1/slots."
}
}
- 400 The query or body is not valid (
invalid_request). - 401 No key, or a key that is not valid or was revoked (
unauthorized). - 403 The plan has no API (
plan_required) or the key lacks the permission (insufficient_scope). - 404 Not found, or it belongs to another organization (
not_found). - 409 The time is not offered (
slot_unavailable), the booking is cancelled or has started (not_changeable), another customer has the email (email_taken), the webhook limit is reached (limit_reached), or a request with this Idempotency-Key is still running (idempotency_in_progress). - 422 The address could not be found (
address_not_found), or the Idempotency-Key was used for another request (idempotency_key_reused). - 429 More than 120 requests a minute with this key (
rate_limited); see Retry-After. - 503 Address lookup or routing is unavailable for a moment (
unavailable).
Veilig opnieuw proberen
Schrijfverzoeken nemen een optionele header Idempotency-Key (bijvoorbeeld een UUID). Dezelfde sleutel opnieuw binnen 24 uur krijgt het eerste antwoord terug, gemarkeerd met Idempotent-Replayed: true, zodat een nieuwe poging nooit twee keer boekt.
Webhooks
Voeg een endpoint toe via Instellen → Integraties, of abonneer via de API, om afspraken en klanten te volgen terwijl ze veranderen. Elke gebeurtenis wordt als JSON gePOST met deze headers. Een mislukte levering wordt ongeveer een dag opnieuw geprobeerd; een endpoint dat 3 dagen blijft falen wordt uitgezet en de eigenaars krijgen een e-mail.
| Veld | Type | Omschrijving |
|---|
webhook-idverplicht | string | The event’s id (msg_…), the same on every retry: use it to skip repeats. |
webhook-timestampverplicht | string | When this attempt was sent, in seconds since 1970 (UTC). |
webhook-signatureverplicht | string | Space-separated v1,<base64> signatures: HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<body> keyed with the endpoint’s secret (the part after whsec_, base64-decoded). After “Roll secret” the old secret signs too for 24 hours. |
Controleer de handtekening (Standard Webhooks) voor je een gebeurtenis vertrouwt, weiger tijdstempels die meer dan 5 minuten afwijken, en gebruik webhook-id om herhalingen over te slaan. In Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto'
// secret: the whsec_… shown when you added the endpoint
// rawBody: the request body exactly as received, before parsing
function verify(secret, headers, rawBody) {
const id = headers['webhook-id']
const timestamp = headers['webhook-timestamp']
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
const key = Buffer.from(secret.slice('whsec_'.length), 'base64')
const expected = Buffer.from(
createHmac('sha256', key).update(id + '.' + timestamp + '.' + rawBody).digest('base64'),
)
return headers['webhook-signature'].split(' ').some((part) => {
const given = Buffer.from(part.slice('v1,'.length))
return part.startsWith('v1,') && given.length === expected.length && timingSafeEqual(given, expected)
})
}
Zapier
Onze Zapier-app is voorlopig privé: vraag ons een uitnodiging via de contactpagina. Koppel hem met een sleutel met “Volledige toegang”. Zaps kunnen starten bij een nieuwe, gewijzigde of geannuleerde afspraak of een nieuwe klant, en kunnen een klant toevoegen of bijwerken, een klant zoeken en de eerste vrije tijd boeken.
Naslag
Elk verzoek dat de API aanneemt. Paden zijn relatief tot de basis-URL.
Je sleutel
The key and its organization
get/v1/me
Elke geldige sleutel mag dit aanroepen.
Any valid key may call this. Use it to test a connection.
Antwoord
200 met Me. Fouten: 401, 403, 429.
Voorbeeld
curl "https://qouh.com/api/v1/me" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Afspraaktypes
List appointment types
get/v1/appointment-types
Recht: bookings:read (Afspraken en vrije tijden bekijken)
Every appointment type, active or not, sorted by name.
Queryparameters
| Veld | Type | Omschrijving |
|---|
localeoptioneel | string | Language of appointment type names (default: the organization’s language). Een van:ennlfr |
Voorbeeld
curl "https://qouh.com/api/v1/appointment-types" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Vrije tijden
Find bookable times
get/v1/slots
Recht: bookings:read (Afspraken en vrije tijden bekijken)
The times an active appointment type can be booked at a place, as the booking page offers them: working hours, existing visits, busy time and the drive between visits are taken into account. Give lat and lng, or an address to look up in the organization's country. Sorted by time; rank 0 fits the route best.
Queryparameters
| Veld | Type | Omschrijving |
|---|
typeverplicht | integer | The appointment type’s id. |
latoptioneel | number | The visit’s latitude (with lng). |
lngoptioneel | number | The visit’s longitude (with lat). |
addressoptioneel | string | The visit’s address, looked up when lat and lng are not given. |
fromoptioneel | string (date-time) | Start of the search (default: now). |
tooptioneel | string (date-time) | End of the search (default: a week after from; at most 31 days). |
Antwoord
200 met SlotList. Fouten: 400, 401, 403, 404, 422, 429, 503.
Voorbeeld
curl "https://qouh.com/api/v1/slots?type=12&address=Grote%20Markt%201%2C%202000%20Antwerpen" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Afspraken
List bookings
get/v1/bookings
Recht: bookings:read (Afspraken en vrije tijden bekijken)
Bookings sorted by start time, a page at a time. Follow nextCursor for the next page.
Queryparameters
| Veld | Type | Omschrijving |
|---|
statusoptioneel | string | One status or a comma-separated list: pending, confirmed, completed, cancelled, no_show. |
fromoptioneel | string (date-time) | Bookings starting at or after this time. |
tooptioneel | string (date-time) | Bookings starting before this time. |
updatedSinceoptioneel | string (date-time) | Bookings changed at or after this time (to sync changes). |
customeroptioneel | integer | Only this customer’s bookings. |
localeoptioneel | string | Language of appointment type names (default: the organization’s language). Een van:ennlfr |
limitoptioneel | integer | Items per page, 1–100. |
cursoroptioneel | string | The nextCursor of the previous page. |
Antwoord
200 met BookingList. Fouten: 400, 401, 403, 429.
Voorbeeld
curl "https://qouh.com/api/v1/bookings?status=confirmed&limit=20" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Get a booking
get/v1/bookings/:id
Recht: bookings:read (Afspraken en vrije tijden bekijken)
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
Queryparameters
| Veld | Type | Omschrijving |
|---|
localeoptioneel | string | Language of appointment type names (default: the organization’s language). Een van:ennlfr |
Antwoord
200 met Booking. Fouten: 400, 401, 403, 404, 429.
Voorbeeld
curl "https://qouh.com/api/v1/bookings/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Make a booking
post/v1/bookings
Recht: bookings:write (Afspraken maken, verplaatsen en annuleren)
Books a time from GET /v1/slots for the customer, as the booking page would: the engine picks who comes and checks the time is still offered. The customer is matched by email (or added) and gets the confirmation email.
JSON-body
| Veld | Type | Omschrijving |
|---|
appointmentTypeverplicht | integer | The appointment type’s id. |
startverplicht | string (date-time) | The start, as a time from GET /v1/slots. ISO 8601 with an offset or Z. |
customerverplicht | object | |
customer.nameverplicht | string | |
customer.emailverplicht | string (email) | Customers are matched by email address (any case). |
customer.phoneoptioneel | string | Stored with its country code (+32470123456) when it can be read as a number of the organization’s country or with a country code; otherwise as sent. |
addressverplicht | string | Where the visit takes place. |
locationoptioneel | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latverplicht | number | |
location.lngverplicht | number | |
localeoptioneel | string | The language for the customer’s emails (default: the organization’s). Een van:ennlfr |
Antwoord
201 met Booking. Fouten: 400, 401, 403, 404, 409, 422, 429, 503.
Voorbeeld
curl -X POST "https://qouh.com/api/v1/bookings" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Cancel a booking
post/v1/bookings/:id/cancel
Recht: bookings:write (Afspraken maken, verplaatsen en annuleren)
Cancels a booking that has not started. The customer and the rep are told.
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
Antwoord
200 met Booking. Fouten: 400, 401, 403, 404, 409, 422, 429.
Voorbeeld
curl -X POST "https://qouh.com/api/v1/bookings/123/cancel" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)"
Move a booking
post/v1/bookings/:id/reschedule
Recht: bookings:write (Afspraken maken, verplaatsen en annuleren)
Moves a booking that has not started to another time, same person and address, if that time is offered. The customer gets the new time and a new link to manage it.
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
JSON-body
| Veld | Type | Omschrijving |
|---|
startverplicht | string (date-time) | The start, as a time from GET /v1/slots. ISO 8601 with an offset or Z. |
Antwoord
200 met Booking. Fouten: 400, 401, 403, 404, 409, 422, 429, 503.
Voorbeeld
curl -X POST "https://qouh.com/api/v1/bookings/123/reschedule" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Klanten
List customers
get/v1/customers
Recht: customers:read (Klanten bekijken)
Customers in the order they were added, a page at a time. Pass email to find one customer.
Queryparameters
| Veld | Type | Omschrijving |
|---|
emailoptioneel | string | Find the customer with this email address. |
updatedSinceoptioneel | string (date-time) | Customers changed at or after this time (to sync changes). |
limitoptioneel | integer | Items per page, 1–100. |
cursoroptioneel | string | The nextCursor of the previous page. |
Voorbeeld
curl "https://qouh.com/api/v1/customers?email=jan%40example.com" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Get a customer
get/v1/customers/:id
Recht: customers:read (Klanten bekijken)
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
Antwoord
200 met Customer. Fouten: 401, 403, 404, 429.
Voorbeeld
curl "https://qouh.com/api/v1/customers/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Add or update a customer
post/v1/customers
Recht: customers:write (Klanten toevoegen en bijwerken)
Adds the customer (201), or updates the one with this email (200).
JSON-body
| Veld | Type | Omschrijving |
|---|
emailverplicht | string (email) | Customers are matched by email address (any case). |
nameoptioneel | string | |
phoneoptioneel | string | null | Stored with its country code (+32470123456) when it can be read as a number of the organization’s country or with a country code; otherwise as sent. Null clears it. |
addressoptioneel | string | The customer’s address. Without location it is looked up in the background (location is null meanwhile). |
locationoptioneel | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latverplicht | number | |
location.lngverplicht | number | |
localeoptioneel | string | The language emails to this customer are sent in. Een van:ennlfr |
Antwoord
200 / 201 met Customer. Fouten: 400, 401, 403, 409, 422, 429.
Voorbeeld
curl -X POST "https://qouh.com/api/v1/customers" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Update a customer
patch/v1/customers/:id
Recht: customers:write (Klanten toevoegen en bijwerken)
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
JSON-body
| Veld | Type | Omschrijving |
|---|
emailoptioneel | string (email) | Customers are matched by email address (any case). |
nameoptioneel | string | |
phoneoptioneel | string | null | Stored with its country code (+32470123456) when it can be read as a number of the organization’s country or with a country code; otherwise as sent. Null clears it. |
addressoptioneel | string | The customer’s address. Without location it is looked up in the background (location is null meanwhile). |
locationoptioneel | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latverplicht | number | |
location.lngverplicht | number | |
localeoptioneel | string | The language emails to this customer are sent in. Een van:ennlfr |
Antwoord
200 met Customer. Fouten: 400, 401, 403, 404, 409, 422, 429.
Voorbeeld
curl -X PATCH "https://qouh.com/api/v1/customers/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Webhook-abonnementen
Subscribe a webhook
post/v1/webhooks
Recht: webhooks:manage (Webhooks instellen (voor Zapier))
Adds a webhook endpoint for the given events (REST hooks, as Zapier uses them) and answers its signing secret once. An organization may have 50 endpoints added through the API (409 limit_reached beyond). An endpoint that answers 410 Gone is removed.
JSON-body
| Veld | Type | Omschrijving |
|---|
urlverplicht | string | Where to POST the events: https, on a public address. |
eventsverplicht | string[] | The events to send there. |
descriptionoptioneel | string | Shown on the Integrations page. |
Voorbeeld
curl -X POST "https://qouh.com/api/v1/webhooks" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @body.json
Unsubscribe a webhook
delete/v1/webhooks/:id
Recht: webhooks:manage (Webhooks instellen (voor Zapier))
Removes an endpoint added through the API, with its delivery log. Endpoints added on the Integrations page are not found here.
Padparameters
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | The id. |
Antwoord
200 met WebhookDeleted. Fouten: 400, 401, 403, 404, 409, 422, 429.
Voorbeeld
curl -X DELETE "https://qouh.com/api/v1/webhooks/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)"
Objecten
Waarmee de API antwoordt. Een veld dat leeg kan zijn heeft null in zijn type.
Location
| Veld | Type | Omschrijving |
|---|
latverplicht | number | |
lngverplicht | number | |
AppointmentType
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | |
nameverplicht | string | |
descriptionverplicht | string | null | |
slugverplicht | string | |
durationMinutesverplicht | integer | |
bookingWindowDaysverplicht | integer | How many days ahead customers may book. |
minNoticeHoursverplicht | integer | The shortest notice customers may book with. |
activeverplicht | boolean | Inactive types take no bookings and have no slots. |
bookingUrlverplicht | string (uri) | The public booking page for this type. |
Customer
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | |
nameverplicht | string | |
emailverplicht | string | |
phoneverplicht | string | null | |
addressverplicht | string | null | |
locationverplicht | Location | null | Null while the address is not placed on the map. |
localeverplicht | string | The language emails to this customer are sent in. Een van:ennlfr |
createdAtverplicht | string (date-time) | When the customer was added. ISO 8601, in UTC. |
updatedAtverplicht | string (date-time) | When the customer last changed. ISO 8601, in UTC. |
Booking
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | |
statusverplicht | string | Een van:pendingconfirmedcompletedcancelledno_show |
startverplicht | string (date-time) | The start of the visit. ISO 8601, in UTC. |
endverplicht | string (date-time) | The end of the visit. ISO 8601, in UTC. |
addressverplicht | string | Where the visit takes place. |
locationverplicht | Location | |
appointmentTypeverplicht | object | |
appointmentType.idverplicht | integer | |
appointmentType.nameverplicht | string | |
repverplicht | object | The person who makes the visit. |
rep.idverplicht | integer | |
rep.nameverplicht | string | null | |
customerverplicht | Customer | |
sourceverplicht | string | Where the booking came from. Een van:bookingPagemaintenanceInviteapicrm |
localeverplicht | string | The language the customer booked in. Een van:ennlfr |
cancelledAtverplicht | string (date-time) | null | |
createdAtverplicht | string (date-time) | When the booking was made. ISO 8601, in UTC. |
updatedAtverplicht | string (date-time) | When the booking last changed. ISO 8601, in UTC. |
Slot
| Veld | Type | Omschrijving |
|---|
startverplicht | string (date-time) | The start of the visit. ISO 8601, in UTC. |
endverplicht | string (date-time) | The end of the visit. ISO 8601, in UTC. |
rankverplicht | integer | How well the time fits the route: 0 is the best fit. |
BookingList
| Veld | Type | Omschrijving |
|---|
dataverplicht | Booking[] | |
nextCursorverplicht | string | null | Pass as cursor to get the next page; null on the last page. |
CustomerList
| Veld | Type | Omschrijving |
|---|
dataverplicht | Customer[] | |
nextCursorverplicht | string | null | Pass as cursor to get the next page; null on the last page. |
AppointmentTypeList
SlotList
| Veld | Type | Omschrijving |
|---|
timeZoneverplicht | string | The organization’s time zone (IANA), for showing the times. |
dataverplicht | Slot[] | |
Me
| Veld | Type | Omschrijving |
|---|
organizationverplicht | object | |
organization.idverplicht | integer | |
organization.nameverplicht | string | |
organization.slugverplicht | string | |
keyverplicht | object | |
key.idverplicht | integer | |
key.labelverplicht | string | |
key.prefixverplicht | string | |
key.scopesverplicht | string[] | |
Error
| Veld | Type | Omschrijving |
|---|
errorverplicht | object | |
error.codeverplicht | string | unauthorized, plan_required, insufficient_scope, rate_limited, not_found, invalid_request, address_not_found, slot_unavailable, not_changeable, email_taken, limit_reached, idempotency_key_reused, idempotency_in_progress, unavailable or internal_error. |
error.messageverplicht | string | |
BookingEvent
| Veld | Type | Omschrijving |
|---|
typeverplicht | string | Een van:booking.createdbooking.updatedbooking.cancelled |
timestampverplicht | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataverplicht | Booking | |
changeoptioneel | string | Only on booking.updated: rescheduled (a new time, by the customer or the API), moved (staff re-planned the day) or reassigned (another person makes the visit).Een van:rescheduledmovedreassigned |
CustomerEvent
| Veld | Type | Omschrijving |
|---|
typeverplicht | string | Een van:customer.createdcustomer.updated |
timestampverplicht | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataverplicht | Customer | |
TestEvent
| Veld | Type | Omschrijving |
|---|
typeverplicht | string | Een van:webhook.test |
timestampverplicht | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataverplicht | object | |
data.endpointIdverplicht | integer | |
data.messageverplicht | string | |
WebhookSubscription
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | |
urlverplicht | string | |
eventsverplicht | string[] | |
descriptionverplicht | string | null | |
statusverplicht | string | Een van:activedisabled |
secretverplicht | string | The signing secret (whsec_…) for webhook-signature. Only shown in this answer. |
createdAtverplicht | string (date-time) | When it was added. ISO 8601, in UTC. |
WebhookDeleted
| Veld | Type | Omschrijving |
|---|
idverplicht | integer | |
deletedverplicht | boolean | Een van:true |
Webhook-gebeurtenissen
De gebeurtenissen waarop een endpoint zich kan abonneren, met het object dat elk ervan meestuurt.