🔗 Connectez Cowlendar à votre CRM, ERP ou application personnalisée (API Publique & Webhooks)
Utilisez l'API Publique de Cowlendar et les Webhooks pour synchroniser les réservations avec votre CRM, ERP ou outils personnalisés.
Si vous gérez une entreprise qui dépend des réservations, vous avez probablement besoin de vos données de réservation à plusieurs endroits. Votre CRM doit savoir quand un nouveau client réserve. Votre ERP a besoin des données de revenus. Votre outil de marketing doit déclencher un e-mail de bienvenue. Votre application personnalisée doit créer des réservations de manière programmatique.
L'API Publique et les Webhooks de Cowlendar rendent tout cela possible. L'API vous permet de lire vos services et réservations, et de créer de nouvelles réservations à partir de n'importe quel système externe. Les webhooks envoient des notifications en temps réel à votre serveur chaque fois qu'un événement se produit : une réservation est créée, confirmée, annulée, reprogrammée, ou un événement d'abonnement se produit.
Ces deux fonctionnalités sont actuellement en Beta.
La référence API interactive complète avec tous les schémas et exemples de réponse se trouve à https://app.cowlendar.com/public-api/docs
Que pouvez-vous construire avec l'API de Cowlendar ?
Voici des scénarios d'intégration réels que les commerçants de Cowlendar construisent actuellement :
Synchroniser chaque réservation avec HubSpot, Salesforce ou tout CRM. Lorsqu'un client réserve une coupe de cheveux, un cours de yoga ou une location, un webhook se déclenche instantanément avec toutes les données de réservation (nom, e-mail, téléphone, service, date, prix, membre de l'équipe). Votre CRM crée ou met à jour le contact automatiquement, sans saisie manuelle de données.
Créez votre propre page de réservation ou application mobile. Utilisez l'API pour récupérer vos services, y compris les durées, les membres de l'équipe, l'équipement et les catégories de participants, et créez des réservations à partir de votre propre interface. La réservation déclenche les mêmes e-mails de confirmation, notifications SMS et synchronisation avec Google Calendar que pour toute réservation effectuée via le widget Cowlendar.
Automatisez des workflows avec Zapier ou Make sans code. Dirigez un webhook Cowlendar vers votre URL de webhook Zapier ou Make. Chaque événement de réservation devient un déclencheur. Envoyez une notification Slack lorsqu'une réservation est effectuée, ajoutez une ligne à Google Sheets, créez une carte Trello, ou déclenchez une séquence d'e-mails dans Klaviyo ou Mailchimp.
Alimentez vos données de réservation dans votre outil de reporting ou BI. Utilisez le point de terminaison Liste des réservations pour extraire toutes les réservations dans une plage de dates, filtrées par statut, service, présence, ou e-mail du client. Exportez vers votre entrepôt de données, Google Sheets ou Airtable pour des tableaux de bord personnalisés.
Liez les réservations à des crédits d'abonnement. Lorsque vous créez une réservation via l'API, passez un subscription_id pour déduire automatiquement un crédit du plan du client, par exemple un pass yoga de 10 cours.
Connectez les réservations POS ou administratives à des systèmes externes. Les webhooks se déclenchent pour toutes les sources de réservation : le widget de la vitrine, le panneau d'administration, le POS et l'API elle-même. Rien ne tombe dans les cracks.
Authentification
Chaque requête API nécessite un jeton Bearer généré à partir de votre admin Cowlendar. Les jetons ont un accès complet en lecture et écriture aux réservations et services de votre boutique. Traitez-les comme des mots de passe : ne les engagez pas dans le contrôle de version et ne les partagez pas dans des canaux publics.
Comment générer votre jeton API
Étape 1 : Dans votre admin Shopify, ouvrez l'application Cowlendar.
Étape 2 : Allez dans Paramètres > API Publique.
Étape 3 : Cliquez sur Générer un jeton.

Étape 4 : Donnez à votre jeton un nom descriptif afin de pouvoir l'identifier plus tard, par exemple "synchronisation HubSpot", "Zapier" ou "application mobile".

Étape 5 : Cliquez sur Générer. Cowlendar vous montre le jeton complet une seule fois. Copiez-le immédiatement et conservez-le dans un endroit sûr comme un gestionnaire de mots de passe ou les variables d'environnement de votre serveur.

Vous pouvez révoquer n'importe quel jeton à tout moment depuis la même page. Révoquer un jeton arrête immédiatement toutes les requêtes API qui l'utilisent.
Utilisation de votre jeton
Incluez le jeton dans l'en-tête Authorization de chaque requête API :
Authorization: Bearer YOUR_TOKEN_HERE
Exemple avec curl :
curl https://app.cowlendar.com/public-api/v1/services -H "Authorization: Bearer YOUR_TOKEN_HERE"
Si le jeton est absent ou invalide, l'API renvoie 401.
Points de terminaison de l'API
URL de base : https://app.cowlendar.com
Lister les services
GET /public-api/v1/services
Renvoie tous les services non archivés de votre boutique. Utilisez ce point de terminaison pour découvrir vos ID de service, types, durées, membres de l'équipe, équipements et catégories de participants avant de créer des réservations via l'API.
Paramètres de requête :
limit (facultatif, 1-100) : nombre de résultats par page.
cursor (facultatif) : curseur de pagination d'une réponse précédente.
is_active (facultatif, vrai ou faux) : filtre par services actifs ou inactifs.
type (facultatif) : filtre par type de service. Valeurs : classic-checkout, classic-no-checkout, multiday-checkout, multiday-no-checkout, multislot-checkout, multislot-no-checkout, fullday-checkout, fullday-no-checkout.
q (facultatif, max 120 caractères) : recherchez des services par titre.
sort (facultatif) : -id (par défaut, le plus récent en premier), id (le plus ancien en premier), title (A à Z), -title (Z à A).
Exemple de requête :
curl "https://app.cowlendar.com/public-api/v1/services?is_active=true&limit=10" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Exemple de réponse (abrégé) :
Comprendre les champs clés du service
avs_type vous indique comment ce service gère la disponibilité et détermine si vous devez passer un teammate_id lors de la création de réservations :
avs_type: "teammates" signifie que chaque membre de l'équipe a son propre calendrier indépendant. Lors de la création d'une réservation, passez un teammate_id du tableau teammates. Si vous l'omettez, le propriétaire du magasin est utilisé comme option par défaut.
avs_type: "service" signifie que le service lui-même détient sa disponibilité. Le membre de l'équipe assigné est automatiquement déterminé en fonction de l'heure de réservation. Vous pouvez toujours passer un teammate_id pour assigner de force une personne spécifique.
avs_type: "equipment" signifie que la disponibilité est par équipement. Similaire au mode service, mais lié à la capacité de l'équipement.
enable_participants vous indique si le service utilise des catégories de participants typées comme "Adulte", "Enfant" ou "Senior". Lorsque true, le tableau participants contient les catégories avec leurs ID, quantités minimum et maximum, et valeurs par défaut. Vous avez besoin de ces ID lors de la création de réservations avec une répartition détaillée des participants.
equipments contient des ressources réservables telles que des salles, des courts, des véhicules ou des vélos avec leurs variantes et SKU. Pour l'équipement avec tracking_type: "same", toutes les unités sont interchangeables et vous pouvez utiliser n'importe quel SKU, généralement base. Pour tracking_type: "unique", chaque unité a son propre SKU et vous devez choisir parmi variants[].sku.
type vous indique le modèle de réservation. Le suffixe -checkout ou -no-checkout indique si le service utilise le paiement Shopify. Le préfixe indique le modèle de planification : classic pour un créneau horaire unique, multiday pour plusieurs jours comme des séjours d'hôtel, multislot pour plusieurs créneaux consécutifs, et fullday pour une réservation d'une journée entière.
Liste des réservations
GET /public-api/v1/bookings
Retourne des réservations pour votre magasin. C'est le point de terminaison que vous utilisez pour les synchronisations CRM, les exports de reporting et les tableaux de bord de réservation personnalisés.
Paramètres de requête :
limit (optionnel, 1-100) : résultats par page.
cursor (optionnel) : curseur de pagination.
start (optionnel, date et heure ISO 8601) : uniquement les réservations commençant à ou après cette date.
end (optionnel, date et heure ISO 8601) : uniquement les réservations commençant avant cette date.
service_id (optionnel, tableau d'ID) : filtrer par un ou plusieurs services.
subscription_id (optionnel, tableau d'ID) : filtrer par abonnements.
status (optionnel, tableau) : confirmed, pending, declined, canceled.
attendance (optionnel, tableau) : booked, pending, arrived, started, completed, no-show, delayed, paid.
customer_email (optionnel) : filtrer par email client exact.
sort (optionnel) : -id (par défaut, le plus récent d'abord), id (le plus ancien d'abord), start_date (le plus ancien d'abord), -start_date (le plus récent d'abord).
Exemple : obtenir toutes les réservations confirmées pour la semaine prochaine :
curl "https://app.cowlendar.com/public-api/v1/bookings?status=confirmed&start=2026-05-25T00:00:00Z&end=2026-06-01T00:00:00Z&sort=start_date" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Exemple : trouver toutes les réservations pour un client spécifique :
curl "https://app.cowlendar.com/public-api/v1/[email protected]" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Exemple : exporter tous les absents d'un service spécifique :
curl "https://app.cowlendar.com/public-api/v1/bookings?attendance=no-show&service_id=6789abcdef0123456789abcd" -H "Authorization: Bearer YOUR_TOKEN_HERE"
Exemple de réponse (abrégé) :
{
"data": [
{
"id": "abc123def456ghi789jkl012",
"booking_str": "COW-1234",
"service": {
"id": "6789abcdef0123456789abcd",
"title": "Haircut",
"type": "classic-checkout"
},
"start_date": "2026-05-27T14:00:00.000Z",
"end_date": "2026-05-27T14:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+33612345678",
"locale": "en"
},
"form_data": {
"Demandes spéciales": "Siège près de la fenêtre"
},
"quantity": 1,
"unit_quantity": 1,
"quantity_details": [],
"price": { "amount": 35, "currency": "EUR" },
"confirmation_status": "confirmed",
"attendance": "booked",
"financial_status": "paid",
"is_canceled": false,
"teammates": [
{
"id": "aaa111bbb222ccc333ddd444",
"firstname": "Marie",
"lastname": "Dupont"
}
],
"order_id": "gid://shopify/Order/123456789",
"subscription_id": null,
"created_at": "2026-05-20T09:15:00.000Z",
"updated_at": "2026-05-20T09:15:00.000Z"
}
],
"pagination": {
"has_more": false,
"next_cursor": null
}
}
Comprendre les champs clés des réservations
quantity et unit_quantity fonctionnent ensemble. Pour un service classique comme une coupe de cheveux, quantity est le nombre de clients présents et unit_quantity est 1. Pour un service de plusieurs jours comme un séjour à l'hôtel, quantity est le nombre de chambres et unit_quantity est le nombre de nuits. Pour un service multislot, quantity est toujours 1 et unit_quantity est le nombre de créneaux réservés. Le nombre total d'unités facturables est toujours quantity x unit_quantity.
quantity_details est la répartition détaillée. Elle peut contenir trois types d'entrées : default pour un compte anonyme, participant pour un participant typé comme "Adulte" ou "Enfant", ou equipment pour une ressource réservable comme "Court 3". Les réservations héritées peuvent avoir un tableau vide ici.
confirmation_status est confirmed, pending, ou declined. Certains services nécessitent une confirmation manuelle par l'administrateur avant qu'une réservation ne devienne active.
attendance suit le cycle de vie de la présence du client : booked, pending, arrived, started, completed, no-show, delayed, paid.
order_id est l'ID de commande Shopify lorsque la réservation a été effectuée via le paiement Shopify. Il est null pour les réservations manuelles et les réservations créées par API.
subscription_id lie la réservation à un plan d'abonnement, par exemple un pass de 10 cours.
Créer une réservation via l'API
POST /public-api/v1/bookings
Crée une réservation de manière programmatique. Cela est considéré comme une réservation manuelle, ce qui signifie qu'aucune commande Shopify n'est créée. Cependant, toutes les automatisations habituelles se déclenchent normalement : courriels de confirmation au client, courriels et SMS de notification à l'équipe, et synchronisation du calendrier avec Google Calendar ou Outlook.
Ce point de terminaison est ce que vous utilisez lorsque vous construisez un frontend de réservation personnalisé, une application mobile, un kiosque, ou lorsque vous importez des réservations depuis un autre système.
Champs requis :
service_id (string) : l'ID du service, que vous obtenez à partir de la Liste des services.
start_date (ISO 8601 datetime) : date et heure de début de la réservation.
end_date (ISO 8601 datetime) : date et heure de fin de la réservation.
timezone (string) : fuseau horaire IANA, par exemple Europe/Paris, America/New_York, Asia/Tokyo.
customer (object) : doit contenir au moins email ou phone. Il peut également inclure name, firstname, lastname, et locale, qui est le code de langue pour les courriels de notification du client.
Champs optionnels :
quantity (integer, default 1) : unités de niveau supérieur réservées. Ignoré si quantity_details est fourni.
unit_quantity (integer, default 1) : sous-unités par unité de niveau supérieur.
quantity_details (array) : répartition détaillée par type de participant ou par équipement. Lorsqu'il est fourni, quantity est automatiquement calculé comme la somme.
teammate_id (string) : assigner de force un membre d'équipe spécifique.
subscription_id (string) : lier cette réservation à un abonnement existant et déduire un crédit.
form_data (object) : paires clé ou valeur personnalisées de votre formulaire de réservation.
price (number, default 0) : prix total net pour la réservation.
currency (string) : code ISO 4217 comme USD, EUR, ou GBP. Par défaut, il s'agit de la devise de votre boutique.
Exemple : créer une réservation simple
Un client réserve une coupe de cheveux de 30 minutes avec un styliste spécifique :
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-01T10:00:00.000Z",
"end_date": "2026-06-01T10:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"email": "[email protected]",
"name": "John Doe",
"phone": "+33612345678",
"locale": "fr"
},
"teammate_id": "aaa111bbb222ccc333ddd444",
"quantity": 1,
"price": 35,
"currency": "EUR"
}'
Cowlendar crée la réservation et déclenche l'email de confirmation, le SMS et l'invitation au calendrier pour le client ainsi que le membre de l'équipe.
Exemple : réservation de groupe avec participants typés
Une famille réserve une visite guidée : 3 adultes et 2 enfants à différents niveaux de prix :
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-15T09:00:00.000Z",
"end_date": "2026-06-15T11:00:00.000Z",
"timezone": "America/New_York",
"customer": {
"email": "[email protected]",
"firstname": "Sarah",
"lastname": "Miller",
"phone": "+15551234567",
"locale": "fr"
},
"quantity_details": [
{
"type": "participant",
"name": "Adult",
"quantity": 3,
"participant_id": "adult_id_from_service"
},
{
"type": "participant",
"name": "Enfant (moins de 12 ans)",
"quantity": 2,
"participant_id": "child_id_from_service"
}
],
"price": 175,
"currency": "USD"
}'
Les valeurs participant_id proviennent du tableau participants[] du service dans la réponse de la Liste des services. La quantity totale est automatiquement fixée à 5 (3 + 2).
Exemple : réservation avec équipement
Un client réserve un court de tennis pour 1 heure :
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-10T16:00:00.000Z",
"end_date": "2026-06-10T17:00:00.000Z",
"timezone": "Europe/London",
"customer": {
"email": "[email protected]",
"name": "Tom Wilson"
},
"quantity_details": [
{
"type": "equipment",
"name": "Court 3",
"quantity": 1,
"equipment_id": "eee555fff666ggg777hhh888",
"equipment_sku": "COURT3",
"capacity": 4
}
],
"price": 25,
"currency": "GBP"
}'
L'equipment_id et le equipment_sku proviennent du tableau equipments[] du service. Pour les équipements avec tracking_type: "same", utilisez n'importe quel SKU, généralement base. Pour tracking_type: "unique", utilisez le variants[].sku spécifique pour l'unité que vous souhaitez.
Un membre du studio de yoga utilise un crédit de son pass mensuel de 10 cours :
curl -X POST https://app.cowlendar.com/public-api/v1/bookings -H "Authorization: Bearer YOUR_TOKEN_HERE" -H "Content-Type: application/json" -d '{
"service_id": "6789abcdef0123456789abcd",
"start_date": "2026-06-05T08:00:00.000Z",
"end_date": "2026-06-05T09:00:00.000Z",
"timezone": "America/Los_Angeles",
"customer": {
"email": "[email protected]",
"name": "Lisa Chen",
"locale": "fr"
},
"subscription_id": "fff000eee111ddd222ccc333",
"price": 0
}'
L'abonnement doit être actif, appartenir au même service, et avoir remaining_credits > 0. Cowlendar décrémente automatiquement le nombre de crédits et envoie l'e-mail de mise à jour de l'abonnement au client.
Codes d'erreur
400 Erreur de validation. Un champ requis est manquant ou au mauvais format. Le corps de l'erreur vous indique quel champ.
401 Jeton manquant ou invalide.
403 La boutique est inactive ou l'accès est restreint.
404 Service non trouvé.
422 Réservation rejetée. Le créneau horaire est déjà pris, le membre de l'équipe n'est pas disponible, l'équipement est à capacité, ou l'abonnement n'a plus de crédits restants. Vérifiez le message d'erreur pour la raison spécifique.
429 Limite de fréquence dépassée. Échelonnez vos requêtes et réessayez après un moment.
Pagination
Tous les points de terminaison de liste utilisent la pagination basée sur le curseur. Lorsque la réponse inclut "has_more": true, passez la valeur next_cursor comme paramètre cursor dans votre prochaine requête :
curl "https://app.cowlendar.com/public-api/v1/bookings?cursor=eyJpZCI6IjY3ODlhYmNkIn0=&limit=50" -H "Authorization: Bearer YOUR_TOKEN_HERE"
L'ordre de tri est préservé à travers les pages. La taille maximale de la page est de 100 résultats.
Webhooks : notifications de réservation en temps réel
Alors que l'API vous permet de tirer des données à la demande, les webhooks vous envoient instantanément des données. Chaque fois qu'une réservation est créée, confirmée, annulée ou reprogrammée, Cowlendar envoie un POST HTTP signé à l'URL que vous configurez.
Les webhooks sont essentiels pour construire des intégrations en temps réel : synchronisations CRM, notifications Slack, séquences d'e-mails automatisées, tableaux de bord en direct, ou tout flux de travail qui doit réagir aux événements de réservation à mesure qu'ils se produisent.
Configurer un point de terminaison de webhook
Étape 1 : Dans votre admin Shopify, ouvrez l'application Cowlendar.
Étape 2 : Allez dans Paramètres > Webhooks.
Étape 3 : Cliquez sur Ajouter un point de terminaison.

Étape 4 : Entrez un nom pour votre point de terminaison, par exemple "HubSpot CRM", "Zapier" ou "Pipeline Analyse".
Étape 5 : Collez votre URL HTTPS. L'URL doit utiliser HTTPS. Si vous utilisez Zapier ou Make, collez l'URL du webhook qu'ils vous fournissent.
Étape 6 : Cochez les événements que vous souhaitez recevoir. Vous pouvez choisir n'importe quelle combinaison.
Étape 7 : Cliquez sur Créer.
Cowlendar affiche un secret de signature commençant par whsec_. Copiez-le immédiatement et conservez-le en toute sécurité. Ce secret est affiché une seule fois et vous en aurez besoin pour vérifier les requêtes de webhook entrantes sur votre serveur.
Vous pouvez modifier n'importe quel point de terminaison ultérieurement pour changer l'URL ou activer ou désactiver des événements spécifiques :

Les 8 événements de webhook
booking.created se déclenche lorsqu'une nouvelle réservation est créée à partir de n'importe quelle source : le widget de la vitrine, le panneau d'administration, le PDV, ou l'API publique.
booking.confirmed se déclenche lorsqu'une réservation en attente est confirmée manuellement par l'administrateur.
booking.declined se déclenche lorsqu'une réservation en attente est refusée.
booking.canceled se déclenche lorsqu'une réservation est annulée par le client ou l'administrateur.
booking.rescheduled se déclenche lorsqu'une date de réservation ou les membres d'équipe assignés sont modifiés.
booking.attendance_changed se déclenche lorsque le statut de présence change, par exemple de "réservé" à "terminé" ou lorsqu'il est marqué comme "non-présent".
subscription.created se déclenche lorsque un nouvel abonnement récurrent est créé.
subscription.billing_failed se déclenche lorsqu'une tentative de facturation d'abonnement échoue. Après 3 échecs consécutifs, le contrat d'abonnement est automatiquement annulé sur Shopify et le statut local passe à annulé.
À quoi ressemble une charge utile de webhook
Chaque livraison de webhook est un POST HTTP avec un corps JSON. Voici un exemple complet de booking.created :
{
"id": "evt_2b86f80a-1c3d-4e5f-9a8b-7c6d5e4f3a2b",
"type": "booking.created",
"created_at": "2026-05-20T09:15:00.000Z",
"shop_domain": "yourshop.myshopify.com",
"data": {
"id": "abc123def456ghi789jkl012",
"booking_str": "COW-1234",
"service": {
"id": "6789abcdef0123456789abcd",
"title": "Coupe de cheveux",
"type": "classic-checkout"
},
"start_date": "2026-05-27T14:00:00.000Z",
"end_date": "2026-05-27T14:30:00.000Z",
"timezone": "Europe/Paris",
"customer": {
"name": "Jane Smith",
"email": "[email protected]",
"phone": "+33612345678",
"locale": "fr"
},
"form_data": {},
"quantity": 1,
"unit_quantity": 1,
"quantity_details": [],
"price": { "amount": 35, "currency": "EUR" },
"confirmation_status": "confirmed",
"attendance": "booked",
"financial_status": null,
"is_canceled": false,
"teammates": [
{
"id": "aaa111bbb222ccc333ddd444",
"firstname": "Marie",
"lastname": "Dupont"
}
],
"order_id": null,
"subscription_id": null,
"created_at": "2026-05-20T09:15:00.000Z",
"updated_at": "2026-05-20T09:15:00.000Z"
}
}
Le champ data contient l'objet complet de réservation ou d'abonnement dans exactement la même structure que la réponse de l'API.
Pour les événements subscription.billing_failed, la charge utile inclut un objet previous supplémentaire avec le failure_count avant la tentative actuelle, afin que vous puissiez suivre l'escalade, par exemple en envoyant un e-mail urgent après le deuxième échec.
En-têtes de requête à chaque livraison de webhook
X-Cowlendar-Event-Id est l'ID unique de l'événement. Cowlendar réutilise le même ID lors de la nouvelle tentative d'une livraison échouée. Utilisez-le pour dédupliquer de votre côté.
X-Cowlendar-Event-Type est la chaîne de type d'événement, par exemple booking.created.
X-Cowlendar-Timestamp est l'horodatage Unix en secondes lorsque Cowlendar a signé la requête.
X-Cowlendar-Signature est la signature HMAC-SHA256 au format t=<timestamp>,v1=<hex>.
Vérifier la signature du webhook
Chaque requête webhook est signée afin que vous puissiez confirmer qu'elle provient réellement de Cowlendar et n'a pas été falsifiée. La signature est calculée comme HMAC-SHA256(secret, timestamp + "." + raw_body) en utilisant le secret de signature de votre point de terminaison.
Vous devez également rejeter les requêtes avec un horodatage supérieur à 5 minutes pour éviter les attaques par rejeu.
Avertissement
Calculez toujours la signature sur le corps brut de la requête, les octets exacts que Cowlendar a envoyés, et non sur un objet JSON analysé et re-sérialisé. L'analyse et la re-sérialisation peuvent modifier les espaces ou l'ordre des clés, ce qui rompt la signature.
Node.js (Express):
import crypto from "crypto";
import express from "express";
const app = express();
app.post(
"/webhooks/cowlendar",
express.raw({ type: "application/json" }),
(req, res) => {
const sigHeader =
req.header("X-Cowlendar-Signature") || "";
const timestamp =
req.header("X-Cowlendar-Timestamp") || "";
const rawBody = req.body.toString("utf8");
// Rejeter les requêtes de plus de 5 minutes
if (
Math.abs(Date.now() / 1000 - parseInt(timestamp, 10))
> 300
) {
return res.status(400).send("Horodatage trop ancien");
}
const expected = crypto
.createHmac(
"sha256",
process.env.COWLENDAR_WEBHOOK_SECRET
)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = sigHeader
.split(",")
.find((p) => p.startsWith("v1="))
?.slice(3);
if (
!received ||
!crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(received)
)
) {
return res.status(401).send("Signature invalide");
}
const event = JSON.parse(rawBody);
// Test de court-circuit pour les pings
if (event.type === "webhook.ping") {
return res.status(200).send("pong");
}
// Traitez les événements réels ici
res.status(200).send("ok");
}
);
Python (Flask):
import hmac, hashlib, os, time, json
from flask import Flask, request
app = Flask(__name__)
@app.post("/webhooks/cowlendar")
def cowlendar_webhook():
sig_header = request.headers.get(
"X-Cowlendar-Signature", ""
)
timestamp = request.headers.get(
"X-Cowlendar-Timestamp", ""
)
raw_body = request.get_data(as_text=True)
# Rejeter les requêtes de plus de 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return "Horodatage trop ancien", 400
expected = hmac.new(
os.environ["COWLENDAR_WEBHOOK_SECRET"].encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
received = next(
(p[3:] for p in sig_header.split(",")
if p.startswith("v1=")),
None,
)
if not received or not hmac.compare_digest(
expected, received
):
return "Signature invalide", 401
event = json.loads(raw_body)
if event["type"] == "webhook.ping":
return "pong", 200
# Traitez les événements réels ici
return "ok", 200
PHP:
<?php
$secret = getenv("COWLENDAR_WEBHOOK_SECRET");
$rawBody = file_get_contents("php://input");
$sigHeader = $_SERVER["HTTP_X_COWLENDAR_SIGNATURE"] ?? "";
$timestamp = $_SERVER["HTTP_X_COWLENDAR_TIMESTAMP"] ?? "";
// Rejeter les requêtes de plus de 5 minutes
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit("Horodatage trop ancien");
}
$expected = hash_hmac(
"sha256",
$timestamp . "." . $rawBody,
$secret
);
$received = null;
foreach (explode(",", $sigHeader) as $part) {
if (str_starts_with($part, "v1=")) {
$received = substr($part, 3);
break;
}
}
if (!$received || !hash_equals($expected, $received)) {
http_response_code(401);
exit("Signature invalide");
}
$event = json_decode($rawBody, true);
if ($event["type"] === "webhook.ping") {
http_response_code(200);
exit("pong");
}
// Traitez les événements réels ici
http_response_code(200);
echo "ok";
Testez votre webhook avant de le mettre en service
Chaque point de terminaison de webhook a un bouton Test dans l'administration. En cliquant dessus, un événement webhook.ping synthétique est envoyé à votre URL. Cet événement utilise la même signature, les mêmes en-têtes et le même comportement de nouvelle tentative que les événements réels, mais avec une charge utile de test inoffensive :
{
"id": "evt_2b86f80a...",
"type": "webhook.ping",
"created_at": "2026-05-13T14:57:57.704Z",
"shop_domain": "yourshop.myshopify.com",
"data": {
"message": "Ceci est un événement de test de Cowlendar. Si vous pouvez lire ceci, votre point de terminaison est accessible."
}
}
Utilisez cela pour confirmer que Cowlendar peut atteindre votre URL, que les paramètres DNS, HTTPS et le pare-feu fonctionnent, et pour valider votre code de vérification de signature HMAC sans créer une véritable réservation. Une réelle réservation enverrait des courriels à un client, créerait des événements de calendrier et pourrait potentiellement déclencher une commande Shopify.
Après avoir cliqué sur Tester, vérifiez le bouton Voir les livraisons pour voir le code de statut HTTP que votre point de terminaison a renvoyé. Un 200 vert signifie que tout fonctionne correctement.
Comportement de réessai et désactivation automatique
Si votre point de terminaison renvoie une erreur 5xx, un 429, ou un délai d'attente de réseau, Cowlendar réessaie selon ce calendrier :
Après 1 minute, après 5 minutes, après 25 minutes, après 2 heures, après 10 heures, après 50 heures. Cela fait 6 tentatives au total s'étendant sur environ 3 jours.
Après environ 20 échecs consécutifs sur tous les événements, le point de terminaison est automatiquement désactivé et Cowlendar envoie un e-mail au propriétaire de la boutique. Vous pouvez réactiver le point de terminaison depuis Paramètres > Webhooks une fois que votre serveur est de nouveau en ligne.
Le même X-Cowlendar-Event-Id est réutilisé lors des tentatives. Stockez toujours les IDs d'événements traités et passez les doublons.
Toute réponse 2xx compte comme succès. Vous n'avez pas besoin de retourner un corps ou un type de contenu spécifique.
Meilleures pratiques pour les webhooks
Répondez dans les 10 secondes. Si votre gestionnaire doit effectuer un travail lourd, tel qu'appeler une autre API, écrire dans une base de données ou exécuter une opération complexe, faites-le de manière asynchrone. Acceptez immédiatement le webhook avec un 200, puis traitez-le dans une tâche d'arrière-plan ou une file d'attente.
Dédupliquez en utilisant l'ID d'événement. Stockez chaque X-Cowlendar-Event-Id que vous traitez. Si vous recevez à nouveau le même ID, passez-le. Cowlendar réessaie avec le même ID en cas d'échec.
Utilisez une comparaison en temps constant pour les signatures. crypto.timingSafeEqual dans Node.js, hmac.compare_digest en Python, et hash_equals en PHP. La comparaison de chaînes régulières comme === est vulnérable aux attaques par temporisation.
Ne réessayez pas de votre côté. Cowlendar gère automatiquement les réessais. Si votre serveur a rencontré un problème temporaire, retournez un 5xx et Cowlendar reviendra selon son calendrier de réessai.
Traitez webhook.ping explicitement. Retournez 200 immédiatement pour les événements de ping. Cela vous permet d'utiliser le bouton Test sans déclencher votre logique métier.
Limitations actuelles
Les réservations créées via l'API sont des réservations manuelles. Elles ne créent pas de commande Shopify. Si votre service utilise normalement le processus de paiement Shopify, l'API le contourne. Les e-mails de confirmation, les SMS et la synchronisation de calendrier fonctionnent toujours normalement.
Pas encore de point de terminaison de disponibilité. L'API actuelle vous permet de lire les services et les réservations, et de créer des réservations. Elle n'expose pas de créneaux horaires disponibles. Pour vérifier si un créneau est disponible, essayez de créer la réservation et gérez la réponse 422 si le créneau est occupé. Un point de terminaison de disponibilité pourrait être ajouté dans une future mise à jour.
Pas encore de points de terminaison pour mettre à jour ou supprimer. Vous pouvez créer et lire des réservations, mais vous ne pouvez pas les mettre à jour, les reprogrammer ou les annuler via l'API. Gérez ces actions via l'administration Cowlendar pour l'instant.
Des limites de taux s'appliquent. Si vous envoyez trop de demandes dans un court délai, l'API retourne un 429. Pour des opérations de masse comme l'exportation de toutes les réservations, ajoutez un court délai entre les demandes paginées, par exemple de 200 à 500 ms.
Les secrets de webhook ne sont affichés qu'une seule fois. Si vous perdez votre secret de signature, supprimez le point de terminaison et créez-en un nouveau pour obtenir un secret frais.
Les webhooks nécessitent HTTPS. Les points de terminaison HTTP ne sont pas acceptés.
FAQ
Puis-je utiliser l'API Cowlendar avec Zapier ou Make sans écrire de code ? Oui. Pour les webhooks, en recevant des événements de Cowlendar, créez un déclencheur "Webhook personnalisé" dans Zapier ou Make, copiez l'URL qu'ils vous donnent, et ajoutez-la comme un point de terminaison dans Cowlendar Paramètres > Webhooks. Chaque événement de réservation déclenche votre automatisation. Pour l'API, envoyant des requêtes à Cowlendar, utilisez l'action "Demande HTTP" dans Zapier ou Make pour appeler n'importe quel point de terminaison de l'API Cowlendar avec votre token Bearer.
Les réservations créées via l'API envoient-elles des e-mails aux clients ? Oui. Les réservations créées par l'API sont traitées exactement comme les réservations manuelles. Les e-mails de confirmation, les notifications SMS et la synchronisation du calendrier sont tous déclenchés automatiquement, tout comme pour une réservation effectuée via le panneau d'administration.
Que se passe-t-il si mon point de terminaison de webhook tombe en panne ? Cowlendar réessaie les livraisons échouées 6 fois sur environ 3 jours. Après environ 20 échecs consécutifs, le point de terminaison est automatiquement désactivé et vous recevez un e-mail. Réactivez-le depuis Paramètres > Webhooks une fois que votre serveur est de nouveau opérationnel.
Puis-je avoir plusieurs points de terminaison de webhook ?
Oui. Créez autant de points de terminaison que vous en avez besoin, chacun avec des sélections d'événements et des URL différentes. Par exemple, un point de terminaison pour votre CRM écoutant booking.created et booking.canceled, et un autre pour votre pipeline d'analyse écoutant tous les événements.
Puis-je révoquer un token API ? Oui. Allez dans Paramètres > API Publique et cliquez sur le bouton Révoquer. Le token cesse de fonctionner immédiatement.
Y a-t-il un environnement de test ou de pré-production ?
Pas pour l'instant. Utilisez le bouton Test webhook.ping pour valider votre configuration de webhook sans créer de réservations réelles. Pour le test de l'API, nous recommandons de créer un service de test dans votre administration Cowlendar que vous utilisez uniquement pour le développement.
Où est la référence API complète ? La documentation API interactive avec tous les schémas, paramètres et exemples de réponse est à https://app.cowlendar.com/public-api/docs
Cette réponse a-t-elle résolu votre question ?