Premiers pas
- Créez une clé dans Configuration → Intégrations (sur Starter, Pro et pendant l’essai). Les clés commencent par
qh_ et ne sont affichées qu’une fois. - Testez-la :
GET /v1/me répond avec le nom de votre organisation. - Appuyez-vous sur la référence ci-dessous, ou connectez Zapier sans écrire de code.
curl "https://qouh.com/api/v1/me" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Clés et autorisations
Envoyez la clé dans l’en-tête Authorization: Bearer de chaque requête, en https. Une clé ne peut faire que ce que vous avez autorisé en la créant :
bookings:read: Voir les rendez-vous et les créneaux libresbookings:write: Créer, déplacer et annuler des rendez-vouscustomers:read: Voir les clientscustomers:write: Ajouter et modifier des clientswebhooks:manage: Configurer des webhooks (pour Zapier)
Chaque clé peut faire 120 requêtes par minute ; au-delà, l’API répond 429 avec un en-tête Retry-After. Une clé révoquée sur la page Intégrations cesse aussitôt de fonctionner.
Heures et pages
Les heures sont au format ISO 8601 en UTC. Les listes arrivent page par page : passez le nextCursor de la réponse comme cursor jusqu’à ce qu’il soit null.
Erreurs
Une erreur répond avec un statut HTTP et un corps contenant un code stable et un message lisible :
{
"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).
Réessayer sans risque
Les écritures acceptent un en-tête facultatif Idempotency-Key (un UUID, par exemple). La même clé renvoyée dans les 24 heures obtient la première réponse, marquée Idempotent-Replayed: true : un nouvel essai ne réserve jamais deux fois.
Webhooks
Ajoutez un endpoint dans Configuration → Intégrations, ou abonnez-vous via l’API, pour suivre les rendez-vous et les clients au fil de leurs changements. Chaque événement est envoyé en POST au format JSON avec ces en-têtes. Une livraison échouée est réessayée pendant environ un jour ; un endpoint qui échoue pendant 3 jours est désactivé et les propriétaires reçoivent un e-mail.
| Champ | Type | Description |
|---|
webhook-idobligatoire | string | The event’s id (msg_…), the same on every retry: use it to skip repeats. |
webhook-timestampobligatoire | string | When this attempt was sent, in seconds since 1970 (UTC). |
webhook-signatureobligatoire | 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. |
Vérifiez la signature (Standard Webhooks) avant de faire confiance à un événement, refusez les horodatages décalés de plus de 5 minutes, et utilisez webhook-id pour ignorer les doublons. En 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
Notre application Zapier est privée pour l’instant : demandez-nous une invitation via la page de contact. Connectez-la avec une clé « Accès complet ». Les Zaps peuvent démarrer quand un rendez-vous est pris, modifié ou annulé ou qu’un client est ajouté, et peuvent ajouter ou mettre à jour un client, trouver un client et réserver le premier créneau libre.
Référence
Toutes les requêtes acceptées par l’API. Les chemins sont relatifs à l’URL de base.
Votre clé
The key and its organization
get/v1/me
Toute clé valide peut appeler ceci.
Any valid key may call this. Use it to test a connection.
Réponse
200 avec Me. Erreurs : 401, 403, 429.
Exemple
curl "https://qouh.com/api/v1/me" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Types de rendez-vous
List appointment types
get/v1/appointment-types
Autorisation : bookings:read (Voir les rendez-vous et les créneaux libres)
Every appointment type, active or not, sorted by name.
Paramètres de requête
| Champ | Type | Description |
|---|
localefacultatif | string | Language of appointment type names (default: the organization’s language). L’une de ces valeurs :ennlfr |
Exemple
curl "https://qouh.com/api/v1/appointment-types" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Créneaux libres
Find bookable times
get/v1/slots
Autorisation : bookings:read (Voir les rendez-vous et les créneaux libres)
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.
Paramètres de requête
| Champ | Type | Description |
|---|
typeobligatoire | integer | The appointment type’s id. |
latfacultatif | number | The visit’s latitude (with lng). |
lngfacultatif | number | The visit’s longitude (with lat). |
addressfacultatif | string | The visit’s address, looked up when lat and lng are not given. |
fromfacultatif | string (date-time) | Start of the search (default: now). |
tofacultatif | string (date-time) | End of the search (default: a week after from; at most 31 days). |
Réponse
200 avec SlotList. Erreurs : 400, 401, 403, 404, 422, 429, 503.
Exemple
curl "https://qouh.com/api/v1/slots?type=12&address=Grote%20Markt%201%2C%202000%20Antwerpen" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Rendez-vous
List bookings
get/v1/bookings
Autorisation : bookings:read (Voir les rendez-vous et les créneaux libres)
Bookings sorted by start time, a page at a time. Follow nextCursor for the next page.
Paramètres de requête
| Champ | Type | Description |
|---|
statusfacultatif | string | One status or a comma-separated list: pending, confirmed, completed, cancelled, no_show. |
fromfacultatif | string (date-time) | Bookings starting at or after this time. |
tofacultatif | string (date-time) | Bookings starting before this time. |
updatedSincefacultatif | string (date-time) | Bookings changed at or after this time (to sync changes). |
customerfacultatif | integer | Only this customer’s bookings. |
localefacultatif | string | Language of appointment type names (default: the organization’s language). L’une de ces valeurs :ennlfr |
limitfacultatif | integer | Items per page, 1–100. |
cursorfacultatif | string | The nextCursor of the previous page. |
Réponse
200 avec BookingList. Erreurs : 400, 401, 403, 429.
Exemple
curl "https://qouh.com/api/v1/bookings?status=confirmed&limit=20" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Get a booking
get/v1/bookings/:id
Autorisation : bookings:read (Voir les rendez-vous et les créneaux libres)
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Paramètres de requête
| Champ | Type | Description |
|---|
localefacultatif | string | Language of appointment type names (default: the organization’s language). L’une de ces valeurs :ennlfr |
Réponse
200 avec Booking. Erreurs : 400, 401, 403, 404, 429.
Exemple
curl "https://qouh.com/api/v1/bookings/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Make a booking
post/v1/bookings
Autorisation : bookings:write (Créer, déplacer et annuler des rendez-vous)
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.
Corps JSON
| Champ | Type | Description |
|---|
appointmentTypeobligatoire | integer | The appointment type’s id. |
startobligatoire | string (date-time) | The start, as a time from GET /v1/slots. ISO 8601 with an offset or Z. |
customerobligatoire | object | |
customer.nameobligatoire | string | |
customer.emailobligatoire | string (email) | Customers are matched by email address (any case). |
customer.phonefacultatif | 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. |
addressobligatoire | string | Where the visit takes place. |
locationfacultatif | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latobligatoire | number | |
location.lngobligatoire | number | |
localefacultatif | string | The language for the customer’s emails (default: the organization’s). L’une de ces valeurs :ennlfr |
Réponse
201 avec Booking. Erreurs : 400, 401, 403, 404, 409, 422, 429, 503.
Exemple
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
Autorisation : bookings:write (Créer, déplacer et annuler des rendez-vous)
Cancels a booking that has not started. The customer and the rep are told.
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Réponse
200 avec Booking. Erreurs : 400, 401, 403, 404, 409, 422, 429.
Exemple
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
Autorisation : bookings:write (Créer, déplacer et annuler des rendez-vous)
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.
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Corps JSON
| Champ | Type | Description |
|---|
startobligatoire | string (date-time) | The start, as a time from GET /v1/slots. ISO 8601 with an offset or Z. |
Réponse
200 avec Booking. Erreurs : 400, 401, 403, 404, 409, 422, 429, 503.
Exemple
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
Clients
List customers
get/v1/customers
Autorisation : customers:read (Voir les clients)
Customers in the order they were added, a page at a time. Pass email to find one customer.
Paramètres de requête
| Champ | Type | Description |
|---|
emailfacultatif | string | Find the customer with this email address. |
updatedSincefacultatif | string (date-time) | Customers changed at or after this time (to sync changes). |
limitfacultatif | integer | Items per page, 1–100. |
cursorfacultatif | string | The nextCursor of the previous page. |
Exemple
curl "https://qouh.com/api/v1/customers?email=jan%40example.com" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Get a customer
get/v1/customers/:id
Autorisation : customers:read (Voir les clients)
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Réponse
200 avec Customer. Erreurs : 401, 403, 404, 429.
Exemple
curl "https://qouh.com/api/v1/customers/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY"
Add or update a customer
post/v1/customers
Autorisation : customers:write (Ajouter et modifier des clients)
Adds the customer (201), or updates the one with this email (200).
Corps JSON
| Champ | Type | Description |
|---|
emailobligatoire | string (email) | Customers are matched by email address (any case). |
namefacultatif | string | |
phonefacultatif | 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. |
addressfacultatif | string | The customer’s address. Without location it is looked up in the background (location is null meanwhile). |
locationfacultatif | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latobligatoire | number | |
location.lngobligatoire | number | |
localefacultatif | string | The language emails to this customer are sent in. L’une de ces valeurs :ennlfr |
Réponse
200 / 201 avec Customer. Erreurs : 400, 401, 403, 409, 422, 429.
Exemple
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
Autorisation : customers:write (Ajouter et modifier des clients)
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Corps JSON
| Champ | Type | Description |
|---|
emailfacultatif | string (email) | Customers are matched by email address (any case). |
namefacultatif | string | |
phonefacultatif | 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. |
addressfacultatif | string | The customer’s address. Without location it is looked up in the background (location is null meanwhile). |
locationfacultatif | object | Coordinates (WGS 84). Without them the address is looked up. |
location.latobligatoire | number | |
location.lngobligatoire | number | |
localefacultatif | string | The language emails to this customer are sent in. L’une de ces valeurs :ennlfr |
Réponse
200 avec Customer. Erreurs : 400, 401, 403, 404, 409, 422, 429.
Exemple
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
Abonnements webhook
Subscribe a webhook
post/v1/webhooks
Autorisation : webhooks:manage (Configurer des webhooks (pour 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.
Corps JSON
| Champ | Type | Description |
|---|
urlobligatoire | string | Where to POST the events: https, on a public address. |
eventsobligatoire | string[] | The events to send there. |
descriptionfacultatif | string | Shown on the Integrations page. |
Exemple
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
Autorisation : webhooks:manage (Configurer des webhooks (pour Zapier))
Removes an endpoint added through the API, with its delivery log. Endpoints added on the Integrations page are not found here.
Paramètres de chemin
| Champ | Type | Description |
|---|
idobligatoire | integer | The id. |
Réponse
200 avec WebhookDeleted. Erreurs : 400, 401, 403, 404, 409, 422, 429.
Exemple
curl -X DELETE "https://qouh.com/api/v1/webhooks/123" \
-H "Authorization: Bearer $ROUTE_TRIM_KEY" \
-H "Idempotency-Key: $(uuidgen)"
Objets
Ce que l’API renvoie. Un champ qui peut être vide a null dans son type.
Location
| Champ | Type | Description |
|---|
latobligatoire | number | |
lngobligatoire | number | |
AppointmentType
| Champ | Type | Description |
|---|
idobligatoire | integer | |
nameobligatoire | string | |
descriptionobligatoire | string | null | |
slugobligatoire | string | |
durationMinutesobligatoire | integer | |
bookingWindowDaysobligatoire | integer | How many days ahead customers may book. |
minNoticeHoursobligatoire | integer | The shortest notice customers may book with. |
activeobligatoire | boolean | Inactive types take no bookings and have no slots. |
bookingUrlobligatoire | string (uri) | The public booking page for this type. |
Customer
| Champ | Type | Description |
|---|
idobligatoire | integer | |
nameobligatoire | string | |
emailobligatoire | string | |
phoneobligatoire | string | null | |
addressobligatoire | string | null | |
locationobligatoire | Location | null | Null while the address is not placed on the map. |
localeobligatoire | string | The language emails to this customer are sent in. L’une de ces valeurs :ennlfr |
createdAtobligatoire | string (date-time) | When the customer was added. ISO 8601, in UTC. |
updatedAtobligatoire | string (date-time) | When the customer last changed. ISO 8601, in UTC. |
Booking
| Champ | Type | Description |
|---|
idobligatoire | integer | |
statusobligatoire | string | L’une de ces valeurs :pendingconfirmedcompletedcancelledno_show |
startobligatoire | string (date-time) | The start of the visit. ISO 8601, in UTC. |
endobligatoire | string (date-time) | The end of the visit. ISO 8601, in UTC. |
addressobligatoire | string | Where the visit takes place. |
locationobligatoire | Location | |
appointmentTypeobligatoire | object | |
appointmentType.idobligatoire | integer | |
appointmentType.nameobligatoire | string | |
repobligatoire | object | The person who makes the visit. |
rep.idobligatoire | integer | |
rep.nameobligatoire | string | null | |
customerobligatoire | Customer | |
sourceobligatoire | string | Where the booking came from. L’une de ces valeurs :bookingPagemaintenanceInviteapicrm |
localeobligatoire | string | The language the customer booked in. L’une de ces valeurs :ennlfr |
cancelledAtobligatoire | string (date-time) | null | |
createdAtobligatoire | string (date-time) | When the booking was made. ISO 8601, in UTC. |
updatedAtobligatoire | string (date-time) | When the booking last changed. ISO 8601, in UTC. |
Slot
| Champ | Type | Description |
|---|
startobligatoire | string (date-time) | The start of the visit. ISO 8601, in UTC. |
endobligatoire | string (date-time) | The end of the visit. ISO 8601, in UTC. |
rankobligatoire | integer | How well the time fits the route: 0 is the best fit. |
BookingList
| Champ | Type | Description |
|---|
dataobligatoire | Booking[] | |
nextCursorobligatoire | string | null | Pass as cursor to get the next page; null on the last page. |
CustomerList
| Champ | Type | Description |
|---|
dataobligatoire | Customer[] | |
nextCursorobligatoire | string | null | Pass as cursor to get the next page; null on the last page. |
AppointmentTypeList
SlotList
| Champ | Type | Description |
|---|
timeZoneobligatoire | string | The organization’s time zone (IANA), for showing the times. |
dataobligatoire | Slot[] | |
Me
| Champ | Type | Description |
|---|
organizationobligatoire | object | |
organization.idobligatoire | integer | |
organization.nameobligatoire | string | |
organization.slugobligatoire | string | |
keyobligatoire | object | |
key.idobligatoire | integer | |
key.labelobligatoire | string | |
key.prefixobligatoire | string | |
key.scopesobligatoire | string[] | |
Error
| Champ | Type | Description |
|---|
errorobligatoire | object | |
error.codeobligatoire | 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.messageobligatoire | string | |
BookingEvent
| Champ | Type | Description |
|---|
typeobligatoire | string | L’une de ces valeurs :booking.createdbooking.updatedbooking.cancelled |
timestampobligatoire | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataobligatoire | Booking | |
changefacultatif | 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).L’une de ces valeurs :rescheduledmovedreassigned |
CustomerEvent
| Champ | Type | Description |
|---|
typeobligatoire | string | L’une de ces valeurs :customer.createdcustomer.updated |
timestampobligatoire | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataobligatoire | Customer | |
TestEvent
| Champ | Type | Description |
|---|
typeobligatoire | string | L’une de ces valeurs :webhook.test |
timestampobligatoire | string (date-time) | When the event happened. ISO 8601, in UTC. |
dataobligatoire | object | |
data.endpointIdobligatoire | integer | |
data.messageobligatoire | string | |
WebhookSubscription
| Champ | Type | Description |
|---|
idobligatoire | integer | |
urlobligatoire | string | |
eventsobligatoire | string[] | |
descriptionobligatoire | string | null | |
statusobligatoire | string | L’une de ces valeurs :activedisabled |
secretobligatoire | string | The signing secret (whsec_…) for webhook-signature. Only shown in this answer. |
createdAtobligatoire | string (date-time) | When it was added. ISO 8601, in UTC. |
WebhookDeleted
| Champ | Type | Description |
|---|
idobligatoire | integer | |
deletedobligatoire | boolean | L’une de ces valeurs :true |
Événements webhook
Les événements auxquels un endpoint peut s’abonner, avec l’objet que chacun transporte.