# 🔗 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**.

> **ℹ️ Info**
>
> 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**.

![](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/import/1791976390469-1779403245386-hmuexklas44.webp)

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

![](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/import/1791976391405-1779403320643-ygmn79cdip.webp)

É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.

![](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/import/1791976390771-1779403415315-17x8hpqrmz1.webp)

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 :

```bash
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 :**

```bash
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 :**

```bash
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 :**

```bash
curl "https://app.cowlendar.com/public-api/v1/bookings?customer_email=jane@example.com" -H "Authorization: Bearer YOUR_TOKEN_HERE"
```

**Exemple : exporter tous les absents d'un service spécifique :**

```bash
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é) :**

```json
{
  "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": "jane@example.com",
        "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 :

```bash
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": "john@example.com",
    "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 :

```bash
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": "family@example.com",
    "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 :

```bash
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": "player@example.com",
    "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 :

```bash
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": "member@example.com",
    "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 :

```bash
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**.

![](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/import/1791976390997-1779403739210-v4gqdf2vbwq.webp)

É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 :

![](https://orka-chat.fra1.cdn.digitaloceanspaces.com/prod/help/671a5e245d077ea954b2cd4c/import/1791976391174-1779403957723-nibbuibb4dp.webp)

### 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` :

```json
{
  "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": "jane@example.com",
      "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):**

```js
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):**

```python
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
<?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 :

```json
{
  "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
