Développeurs

L’API Qouh

Reliez votre CRM, votre site ou votre logiciel comptable : lisez les types de rendez-vous, les créneaux libres, les rendez-vous et les clients, réservez et déplacez des visites aux heures que vous proposez, et recevez chaque changement sous forme de webhook signé.

URL de base
https://qouh.com/api

Télécharger le fichier OpenAPILa référence ci-dessous est en anglais, comme l’API elle-même.

Premiers pas

  1. 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.
  2. Testez-la : GET /v1/me répond avec le nom de votre organisation.
  3. 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 libres
  • bookings:write: Créer, déplacer et annuler des rendez-vous
  • customers:read: Voir les clients
  • customers:write: Ajouter et modifier des clients
  • webhooks: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.

ChampTypeDescription
webhook-id
obligatoire
stringThe event’s id (msg_…), the same on every retry: use it to skip repeats.
webhook-timestamp
obligatoire
stringWhen this attempt was sent, in seconds since 1970 (UTC).
webhook-signature
obligatoire
stringSpace-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
ChampTypeDescription
locale
facultatif
stringLanguage of appointment type names (default: the organization’s language).
L’une de ces valeurs :ennlfr
Réponse

200 avec AppointmentTypeList. Erreurs : 400, 401, 403, 429.

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
ChampTypeDescription
type
obligatoire
integerThe appointment type’s id.
lat
facultatif
numberThe visit’s latitude (with lng).
lng
facultatif
numberThe visit’s longitude (with lat).
address
facultatif
stringThe visit’s address, looked up when lat and lng are not given.
from
facultatif
string (date-time)Start of the search (default: now).
to
facultatif
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
ChampTypeDescription
status
facultatif
stringOne status or a comma-separated list: pending, confirmed, completed, cancelled, no_show.
from
facultatif
string (date-time)Bookings starting at or after this time.
to
facultatif
string (date-time)Bookings starting before this time.
updatedSince
facultatif
string (date-time)Bookings changed at or after this time (to sync changes).
customer
facultatif
integerOnly this customer’s bookings.
locale
facultatif
stringLanguage of appointment type names (default: the organization’s language).
L’une de ces valeurs :ennlfr
limit
facultatif
integerItems per page, 1–100.
cursor
facultatif
stringThe 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
ChampTypeDescription
id
obligatoire
integerThe id.
Paramètres de requête
ChampTypeDescription
locale
facultatif
stringLanguage 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
ChampTypeDescription
appointmentType
obligatoire
integerThe appointment type’s id.
start
obligatoire
string (date-time)The start, as a time from GET /v1/slots. ISO 8601 with an offset or Z.
customer
obligatoire
object
customer.name
obligatoire
string
customer.email
obligatoire
string (email)Customers are matched by email address (any case).
customer.phone
facultatif
stringStored 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.
address
obligatoire
stringWhere the visit takes place.
location
facultatif
objectCoordinates (WGS 84). Without them the address is looked up.
location.lat
obligatoire
number
location.lng
obligatoire
number
locale
facultatif
stringThe 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
ChampTypeDescription
id
obligatoire
integerThe 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
ChampTypeDescription
id
obligatoire
integerThe id.
Corps JSON
ChampTypeDescription
start
obligatoire
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
ChampTypeDescription
email
facultatif
stringFind the customer with this email address.
updatedSince
facultatif
string (date-time)Customers changed at or after this time (to sync changes).
limit
facultatif
integerItems per page, 1–100.
cursor
facultatif
stringThe nextCursor of the previous page.
Réponse

200 avec CustomerList. Erreurs : 400, 401, 403, 429.

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
ChampTypeDescription
id
obligatoire
integerThe 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
ChampTypeDescription
email
obligatoire
string (email)Customers are matched by email address (any case).
name
facultatif
string
phone
facultatif
string | nullStored 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.
address
facultatif
stringThe customer’s address. Without location it is looked up in the background (location is null meanwhile).
location
facultatif
objectCoordinates (WGS 84). Without them the address is looked up.
location.lat
obligatoire
number
location.lng
obligatoire
number
locale
facultatif
stringThe 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
ChampTypeDescription
id
obligatoire
integerThe id.
Corps JSON
ChampTypeDescription
email
facultatif
string (email)Customers are matched by email address (any case).
name
facultatif
string
phone
facultatif
string | nullStored 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.
address
facultatif
stringThe customer’s address. Without location it is looked up in the background (location is null meanwhile).
location
facultatif
objectCoordinates (WGS 84). Without them the address is looked up.
location.lat
obligatoire
number
location.lng
obligatoire
number
locale
facultatif
stringThe 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
ChampTypeDescription
url
obligatoire
stringWhere to POST the events: https, on a public address.
events
obligatoire
string[]The events to send there.
description
facultatif
stringShown on the Integrations page.
Réponse

201 avec WebhookSubscription. Erreurs : 400, 401, 403, 409, 422, 429.

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
ChampTypeDescription
id
obligatoire
integerThe 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

ChampTypeDescription
lat
obligatoire
number
lng
obligatoire
number

AppointmentType

ChampTypeDescription
id
obligatoire
integer
name
obligatoire
string
description
obligatoire
string | null
slug
obligatoire
string
durationMinutes
obligatoire
integer
bookingWindowDays
obligatoire
integerHow many days ahead customers may book.
minNoticeHours
obligatoire
integerThe shortest notice customers may book with.
active
obligatoire
booleanInactive types take no bookings and have no slots.
bookingUrl
obligatoire
string (uri)The public booking page for this type.

Customer

ChampTypeDescription
id
obligatoire
integer
name
obligatoire
string
email
obligatoire
string
phone
obligatoire
string | null
address
obligatoire
string | null
location
obligatoire
Location | nullNull while the address is not placed on the map.
locale
obligatoire
stringThe language emails to this customer are sent in.
L’une de ces valeurs :ennlfr
createdAt
obligatoire
string (date-time)When the customer was added. ISO 8601, in UTC.
updatedAt
obligatoire
string (date-time)When the customer last changed. ISO 8601, in UTC.

Booking

ChampTypeDescription
id
obligatoire
integer
status
obligatoire
string
L’une de ces valeurs :pendingconfirmedcompletedcancelledno_show
start
obligatoire
string (date-time)The start of the visit. ISO 8601, in UTC.
end
obligatoire
string (date-time)The end of the visit. ISO 8601, in UTC.
address
obligatoire
stringWhere the visit takes place.
location
obligatoire
Location
appointmentType
obligatoire
object
appointmentType.id
obligatoire
integer
appointmentType.name
obligatoire
string
rep
obligatoire
objectThe person who makes the visit.
rep.id
obligatoire
integer
rep.name
obligatoire
string | null
customer
obligatoire
Customer
source
obligatoire
stringWhere the booking came from.
L’une de ces valeurs :bookingPagemaintenanceInviteapicrm
locale
obligatoire
stringThe language the customer booked in.
L’une de ces valeurs :ennlfr
cancelledAt
obligatoire
string (date-time) | null
createdAt
obligatoire
string (date-time)When the booking was made. ISO 8601, in UTC.
updatedAt
obligatoire
string (date-time)When the booking last changed. ISO 8601, in UTC.

Slot

ChampTypeDescription
start
obligatoire
string (date-time)The start of the visit. ISO 8601, in UTC.
end
obligatoire
string (date-time)The end of the visit. ISO 8601, in UTC.
rank
obligatoire
integerHow well the time fits the route: 0 is the best fit.

BookingList

ChampTypeDescription
data
obligatoire
Booking[]
nextCursor
obligatoire
string | nullPass as cursor to get the next page; null on the last page.

CustomerList

ChampTypeDescription
data
obligatoire
Customer[]
nextCursor
obligatoire
string | nullPass as cursor to get the next page; null on the last page.

SlotList

ChampTypeDescription
timeZone
obligatoire
stringThe organization’s time zone (IANA), for showing the times.
data
obligatoire
Slot[]

Me

ChampTypeDescription
organization
obligatoire
object
organization.id
obligatoire
integer
organization.name
obligatoire
string
organization.slug
obligatoire
string
key
obligatoire
object
key.id
obligatoire
integer
key.label
obligatoire
string
key.prefix
obligatoire
string
key.scopes
obligatoire
string[]

Error

ChampTypeDescription
error
obligatoire
object
error.code
obligatoire
stringunauthorized, 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.message
obligatoire
string

BookingEvent

ChampTypeDescription
type
obligatoire
string
L’une de ces valeurs :booking.createdbooking.updatedbooking.cancelled
timestamp
obligatoire
string (date-time)When the event happened. ISO 8601, in UTC.
data
obligatoire
Booking
change
facultatif
stringOnly 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

ChampTypeDescription
type
obligatoire
string
L’une de ces valeurs :customer.createdcustomer.updated
timestamp
obligatoire
string (date-time)When the event happened. ISO 8601, in UTC.
data
obligatoire
Customer

TestEvent

ChampTypeDescription
type
obligatoire
string
L’une de ces valeurs :webhook.test
timestamp
obligatoire
string (date-time)When the event happened. ISO 8601, in UTC.
data
obligatoire
object
data.endpointId
obligatoire
integer
data.message
obligatoire
string

WebhookSubscription

ChampTypeDescription
id
obligatoire
integer
url
obligatoire
string
events
obligatoire
string[]
description
obligatoire
string | null
status
obligatoire
string
L’une de ces valeurs :activedisabled
secret
obligatoire
stringThe signing secret (whsec_…) for webhook-signature. Only shown in this answer.
createdAt
obligatoire
string (date-time)When it was added. ISO 8601, in UTC.

WebhookDeleted

ChampTypeDescription
id
obligatoire
integer
deleted
obligatoire
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.