Système de Webhooks TAP4BOOK

Version : 2.0

Les webhooks TAP4BOOK permettent de recevoir des notifications HTTP en temps réel pour les événements importants du système de gestion de rendez-vous.

Temps Réel

Notifications instantanées lors des événements

Sécurisé

Signatures HMAC-SHA256

Multi-Source

API, Widget, MyBooking, Console

Fonctionnalités Clés

  • 10 événements supportés couvrant tout le cycle de vie des rendez-vous et le tracking des visiteurs
  • Signatures HMAC-SHA256 pour validation et sécurité
  • Méthode HTTP POST uniquement avec payload JSON sécurisé
  • Tracking de source pour identifier l'origine des événements
  • Payload standardisé JSON avec métadonnées complètes
  • Configuration flexible simple ou avancée

Configuration

Configuration via l'interface Console

La configuration de l'url webhook devant recevoir les évènements se fait exclusivement via l'interface de gestion TAP4BOOK.

Prérequis :
  • Avoir un compte actif avec le rôle Administrateur ou Manager
  • Accès à la Console TAP4BOOK
  • Une URL HTTPS valide pour recevoir les webhooks
Étapes de configuration :
  1. Connexion - Connectez-vous à votre Console TAP4BOOK
  2. Navigation - Allez dans Compte → Vos infos (index.php?page=account)
  3. Section Webhooks - Descendez jusqu'à la section "Webhooks" (visible uniquement pour Admin/Manager)
  4. Configuration - Saisissez votre URL webhook (HTTPS obligatoire)
  5. Sauvegarde - Cliquez sur "Mettre à jour" pour enregistrer
Fonctionnalités disponibles dans la console :
Test en temps réel

Testez votre webhook avec un payload d'exemple

Statistiques

Visualisez les métriques des 30 derniers jours

Logs détaillés

Consultez l'historique des envois

Comportement par défaut

Une fois configuré, votre webhook recevra :

  • Tous les événements supportés (10 types) via requêtes POST exclusivement
  • Payload JSON standardisé avec signature HMAC-SHA256
  • Timeout de 10 secondes par requête
  • Headers d'authentification et d'identification

Événements Supportés

Événement Sources Description Données Incluses
appointment.created API Widget Un nouveau rendez-vous a été créé par un visiteur appointment, client
appointment.updated API MyBooking Console Un rendez-vous a été modifié appointment, client, changes
appointment.cancelled API MyBooking Console Un rendez-vous a été annulé appointment, client, reason
appointment.completed API Console Un rendez-vous a été marqué comme terminé appointment, client
appointment.deleted API Console Un rendez-vous a été supprimé appointment, client, reason
appointment.no_show API Console Client absent au rendez-vous appointment, client
widget.newvisitor Widget Nouveau visiteur détecté sur widget (notification immédiate) visitor (avec tous les tags)
widget.visitornoappointment Widget Visiteur n'a pas pris de rendez-vous avant le timeout visitor (avec tous les tags)
widget.visitorconverted Widget Visiteur a pris un rendez-vous (conversion réussie) visitor, appointment, client
client.created API Widget Console Un nouveau client a été ajouté client

Format du Payload

Structure Générale

Tous les webhooks TAP4BOOK suivent cette structure standardisée :

{
  "event": "appointment.created",
  "event_id": "evt_684305a64d0ce3.39098263",
  "timestamp": "2025-06-06T14:30:00+00:00",
  "api_version": "2.0",
  "source": "API|Widget|MyBooking|Console",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    // Données spécifiques à l'événement
  },
  "signature": "sha256_hmac_signature_here"
}

Champs Communs

Champ Type Description
event string Type d'événement (voir liste ci-dessus)
event_id string Identifiant unique de l'événement (pour déduplication)
timestamp string Horodatage ISO 8601 de l'événement
api_version string Version du format webhook (actuellement "2.0")
source string Origine de l'événement : API, Widget, MyBooking, Console
entity object Informations sur l'entité (cabinet, entreprise)
data object Données spécifiques à l'événement
signature string Signature HMAC-SHA256 pour validation

Champs Spécifiques aux Données

Objet Client

L'objet client inclut maintenant de nombreux champs personnalisés :

Champ Type Description
client_id / id integer Identifiant unique du client
entity_id integer Identifiant de l'entité propriétaire
first_name string Prénom du client
last_name string Nom de famille du client
name string Nom complet (prénom + nom)
email string Adresse email
phone string Numéro de téléphone au format E164 (sans le +)
tag1 à tag10 string|null Champs personnalisés libres pour étiqueter les clients
optin integer Consentement marketing (0 = non, 1 = oui)
ip_address string Adresse IP de création (peut être masquée selon la configuration)
created_at string Date de création du client
updated_at string Date de dernière modification

Objet Appointment

L'objet appointment inclut maintenant le champ timezone :

Champ Type Description
timezone string Timezone utilisée pour start_time et end_time (ex: "Europe/Paris")
Masquage des données (RGPD/Confidentialité) :

TAP4BOOK offre une fonctionnalité de masquage automatique des données sensibles dans les webhooks pour assurer la conformité RGPD et protéger la vie privée des clients.

Configuration

Pour activer le masquage des données :

  1. Accédez à la console d'administration TAP4BOOK
  2. Allez dans Configuration → Entités
  3. Modifiez l'entité concernée
  4. Activez l'option mask_clientsData
  5. Sauvegardez les modifications
Données masquées

Lorsque le masquage est activé, les champs suivants sont automatiquement masqués :

  • email : "marie.dupont@XXX.XXX" (préserve le nom utilisateur)
  • client_email : "marie.dupont@XXX.XXX"
  • phone : "336123XXXX" (masque les 4 derniers chiffres)
  • client_phone : "336123XXXX"
  • ip_address : "xx.xx.xx.xx" (masque complètement l'IP)
Avantages
  • Conformité RGPD : Réduit les risques liés aux données personnelles
  • Sécurité : Limite l'exposition des données sensibles
  • Automatique : Appliqué sur tous les types d'événements webhooks
  • Transparent : Fonctionne avec tous les systèmes existants
Quand utiliser le masquage :
  • Intégrations tierces : Avec des systèmes externes moins sécurisés
  • Développement/Test : Pour protéger les données en environnement de test
  • Logs et monitoring : Éviter l'exposition dans les systèmes de surveillance
  • Conformité réglementaire : Respecter les exigences RGPD/CCPA

Nouveauté API v2.1 : Détails enrichis des statuts

Nouveau : Objet status_details

Depuis la version 2.1 de l'API, les webhooks incluent un objet status_details qui fournit des informations enrichies sur le statut du rendez-vous, incluant les labels personnalisés configurés pour votre entité.

Important : Cette information est additionnelle et n'affecte pas le champ status existant pour garantir la rétrocompatibilité.

Structure de l'objet status_details

"status_details": {
  "key": "pending",              // Identifiant technique (identique au champ status)
  "label": "En attente",         // Libellé personnalisé ou par défaut
  "color": "#FFA500",           // Code couleur hexadécimal
  "type": "pending",            // Type métier : pending, confirmed, completed, cancelled
  "is_custom": false,           // true si c'est un statut personnalisé
  "scope": "default"            // Portée : default, global, entity, calendar
}

Exemples avec statuts personnalisés

Statut par défaut
{
  "status": "pending",
  "status_details": {
    "key": "pending",
    "label": "En attente",
    "color": "#FFA500",
    "type": "pending",
    "is_custom": false,
    "scope": "default"
  }
}
Statut personnalisé
{
  "status": "en_validation",
  "status_details": {
    "key": "en_validation",
    "label": "En cours de validation",
    "color": "#FF9800",
    "type": "pending",
    "is_custom": true,
    "scope": "entity"
  }
}

Exemples de Payloads par Événement

Note importante sur les timestamps :
  • timestamp : Toujours en UTC au format ISO 8601
  • start_time et end_time : Dans la timezone du calendrier
  • timezone : Timezone utilisée pour les heures du rendez-vous

appointment.created

{
  "event": "appointment.created",
  "event_id": "evt_684305a64d0ce3.39098263",
  "timestamp": "2025-06-06T14:30:00+00:00",
  "api_version": "2.0",
  "source": "API",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "client_firstName": "Marie",
      "client_lastName": "Dupont",
      "client_name": "Marie Dupont",
      "client_email": "marie.dupont@example.com",
      "client_phone": "33612345678",
      "contract_number": "",
      "date": "2025-06-11",
      "start_time": "16:00:00",
      "end_time": "17:00:00",
      "entity_id": 5,
      "calendar_id": 15,
      "clientCancel_option": 1,
      "client_id": "123",
      "cancel_token": "a1b2c3d4e5f6789abcdef123456789",
      "tags": {
        "tag8": "campaign_source",
        "tag9": "widget_conversion"
      },
      "newClientCreated": true,
      "timezone": "Europe/Paris"
    },
    "client": {
      "id": 123,
      "entity_id": 5,
      "name": "Marie Dupont",
      "firstName": "Marie",
      "lastName": "Dupont",
      "phone": "33612345678",
      "email": "marie.dupont@example.com",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "campaign_source",
      "tag9": "widget_conversion",
      "tag10": null,
      "optin": true,
      "ip_address": "192.168.1.100",
      "created_at": "2025-06-06 14:30:00",
      "updated_at": "2025-06-06 14:30:00"
    }
  }
}
Exemple avec masquage activé (mask_clientsData = 1)
{
  "event": "appointment.created",
  "event_id": "evt_684305a64d0ce3.39098263",
  "timestamp": "2025-06-06T14:30:00+00:00",
  "api_version": "2.0",
  "source": "API",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "client_firstName": "Marie",
      "client_lastName": "Dupont",
      "client_name": "Marie Dupont",
      "client_email": "marie.dupont@XXX.XXX",
      "client_phone": "336123XXXX",
      "contract_number": "",
      "date": "2025-06-11",
      "start_time": "16:00:00",
      "end_time": "17:00:00",
      "entity_id": 5,
      "calendar_id": 15,
      "clientCancel_option": 1,
      "client_id": "123",
      "cancel_token": "a1b2c3d4e5f6789abcdef123456789",
      "tags": {
        "tag8": "campaign_source",
        "tag9": "widget_conversion"
      },
      "newClientCreated": true,
      "timezone": "Europe/Paris"
    },
    "client": {
      "id": 123,
      "entity_id": 5,
      "name": "Marie Dupont",
      "firstName": "Marie",
      "lastName": "Dupont",
      "phone": "336123XXXX",
      "email": "marie.dupont@XXX.XXX",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "campaign_source",
      "tag9": "widget_conversion",
      "tag10": null,
      "optin": true,
      "ip_address": "xx.xx.xx.xx",
      "created_at": "2025-06-06 14:30:00",
      "updated_at": "2025-06-06 14:30:00"
    }
  }
}
Champs masqués de manière cohérente :
  • appointment.client_email : "marie.dupont@example.com" → "marie.dupont@XXX.XXX"
  • appointment.client_phone : "33612345678" → "336123XXXX"
  • client.email : "marie.dupont@example.com" → "marie.dupont@XXX.XXX"
  • client.phone : "33612345678" → "336123XXXX"
  • client.ip_address : "192.168.1.100" → "xx.xx.xx.xx"

appointment.updated

{
  "event": "appointment.updated",
  "event_id": "evt_684306a64d1ce4.40098264",
  "timestamp": "2025-06-06T15:00:00+00:00",
  "api_version": "2.0",
  "source": "Console",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 457,
      "client_id": 123,
      "date": "2025-06-05",
      "start_time": "09:00:00",
      "end_time": "10:00:00",
      "status": "confirmed",
      "status_details": {
        "key": "confirmed",
        "label": "Confirmé",
        "color": "#0FA66C",
        "type": "confirmed",
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Première consultation - reportée",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 123,
      "id": 123,
      "entity_id": 5,
      "first_name": "Marie",
      "last_name": "Dupont",
      "name": "Marie Dupont",
      "email": "marie.dupont@example.com",
      "phone": "33612345678",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-01-15 10:30:00",
      "updated_at": "2025-06-06 15:00:00"
    },
    "changes": {
      "status": {"from": "pending", "to": "confirmed"},
      "date": {"from": "2025-06-04", "to": "2025-06-05"},
      "start_time": {"from": "16:00:00", "to": "09:00:00"},
      "slot_id": {"from": 456, "to": 457}
    }
  },
  "signature": "b2c3d4e5f6g789abcdef123456789..."
}

appointment.cancelled

{
  "event": "appointment.cancelled",
  "event_id": "evt_684307a64d2ce5.41098265",
  "timestamp": "2025-06-06T15:30:00+00:00",
  "api_version": "2.0",
  "source": "MyBooking",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 457,
      "client_id": 123,
      "date": "2025-06-05",
      "start_time": "09:00:00",
      "end_time": "10:00:00",
      "status": "cancelled",
      "status_details": {
        "key": "cancelled",
        "label": "Annulé",
        "color": "#FF0000",
        "type": "cancelled",
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Première consultation - reportée",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 123,
      "id": 123,
      "entity_id": 5,
      "first_name": "Marie",
      "last_name": "Dupont",
      "name": "Marie Dupont",
      "email": "marie.dupont@example.com",
      "phone": "33612345678",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-01-15 10:30:00",
      "updated_at": "2025-06-06 15:30:00"
    },
    "reason": "Cancelled by client via MyBooking"
  },
  "signature": "c3d4e5f6g789abcdef123456789..."
}

appointment.completed

{
  "event": "appointment.completed",
  "event_id": "evt_684308a64d3ce6.42098266",
  "timestamp": "2025-06-06T16:00:00+00:00",
  "api_version": "2.0",
  "source": "Console",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 457,
      "client_id": 123,
      "date": "2025-06-05",
      "start_time": "09:00:00",
      "end_time": "10:00:00",
      "status": "completed",
      "status_details": {
        "key": "completed",
        "label": "Terminé",
        "color": "#00FF00",
        "type": "completed",
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Consultation terminée avec succès",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 123,
      "id": 123,
      "entity_id": 5,
      "first_name": "Marie",
      "last_name": "Dupont",
      "name": "Marie Dupont",
      "email": "marie.dupont@example.com",
      "phone": "33612345678",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-01-15 10:30:00",
      "updated_at": "2025-06-06 16:00:00"
    }
  },
  "signature": "d4e5f6g789abcdef123456789..."
}

appointment.deleted

{
  "event": "appointment.deleted",
  "event_id": "evt_684309a64d4ce7.43098267",
  "timestamp": "2025-06-06T16:30:00+00:00",
  "api_version": "2.0",
  "source": "API",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 457,
      "client_id": 123,
      "date": "2025-06-05",
      "start_time": "09:00:00",
      "end_time": "10:00:00",
      "status": "deleted",
      "status_details": {
        "key": "deleted",
        "label": "Supprimé",
        "color": "#666666",
        "type": null,
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Consultation supprimée",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 123,
      "id": 123,
      "entity_id": 5,
      "first_name": "Marie",
      "last_name": "Dupont",
      "name": "Marie Dupont",
      "email": "marie.dupont@example.com",
      "phone": "33612345678",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-01-15 10:30:00",
      "updated_at": "2025-06-06 16:30:00"
    },
    "reason": "Deleted via API"
  },
  "signature": "e5f6g789abcdef123456789..."
}

appointment.no_show

{
  "event": "appointment.no_show",
  "event_id": "evt_684310a64d5ce8.44098268",
  "timestamp": "2025-06-06T17:00:00+00:00",
  "api_version": "2.0",
  "source": "Console",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "appointment": {
      "appointment_id": 789,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 457,
      "client_id": 123,
      "date": "2025-06-05",
      "start_time": "09:00:00",
      "end_time": "10:00:00",
      "status": "no_show",
      "status_details": {
        "key": "no_show",
        "label": "Absent",
        "color": "#9C27B0",
        "type": null,
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Client absent - consultation manquée",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 123,
      "id": 123,
      "entity_id": 5,
      "first_name": "Marie",
      "last_name": "Dupont",
      "name": "Marie Dupont",
      "email": "marie.dupont@example.com",
      "phone": "33612345678",
      "tag1": "VIP",
      "tag2": "Urgence",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-01-15 10:30:00",
      "updated_at": "2025-06-06 17:00:00"
    }
  },
  "signature": "f6g789abcdef123456789..."
}

client.created

{
  "event": "client.created",
  "event_id": "evt_684310a64d5ce8.44098268",
  "timestamp": "2025-06-06T17:00:00+00:00",
  "api_version": "2.0",
  "source": "Widget",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "client": {
      "client_id": 124,
      "id": 124,
      "entity_id": 5,
      "first_name": "Jean",
      "last_name": "Martin",
      "name": "Jean Martin",
      "email": "jean.martin@example.com",
      "phone": "33698765432",
      "tag1": "Nouveau",
      "tag2": null,
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": null,
      "tag9": null,
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.101",
      "created_at": "2025-06-06 17:00:00",
      "updated_at": "2025-06-06 17:00:00"
    }
  },
  "signature": "f6g789abcdef123456789..."
}

widget.newvisitor

Conditions d'envoi
  • Déclenchement : Immédiat lors de la détection d'un nouveau visiteur
  • Prérequis : Widget avec système de tracking activé ET notification immédiate activée
  • Fréquence : Une seule fois par visiteur unique (déduplication par fingerprint)
  • Configuration : Option "Notification Immédiate" dans l'admin du widget
{
  "event": "widget.newvisitor",
  "event_id": "evt_684311a64d6ce9.45098269",
  "timestamp": "2025-06-06T17:30:00+00:00",
  "api_version": "2.0",
  "source": "Widget",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "visitor": {
      "entity_id": 5,
      "widget_id": "644660339401",
      "tracking_id": 458,
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
      "visit_timestamp": "2025-06-06T17:30:00+00:00",
      "status": "tracking",
      "tag1": null,
      "tag2": null,
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "external_client_123",
      "tag9": "campaign_step_1",
      "tag10": null
    }
  },
  "signature": "g789abcdef123456789..."
}

widget.visitornoappointment

Notification Différée avec Timeout
  • Déclenchement : Décalé selon le timeout configuré (défaut: 5 minutes)
  • Prérequis : Widget avec système de tracking activé (Tracking Visiteur)
  • Condition : Visiteur n'a pas pris de rendez-vous avant expiration du délai
  • Configuration : Timeout personnalisable (1-60 minutes) dans l'admin du widget
  • Annulation : Webhook non envoyé si le visiteur convertit avant timeout
{
  "event": "widget.visitornoappointment",
  "event_id": "evt_684312a64d7cea.46098270",
  "timestamp": "2025-06-06T18:00:00+00:00",
  "api_version": "2.0",
  "source": "Widget",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "visitor": {
      "entity_id": 5,
      "widget_id": "644660339401",
      "tracking_id": 458,
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
      "first_visit_at": "2025-06-06T17:30:00+00:00",
      "timeout_at": "2025-06-06T18:00:00+00:00",
      "status": "timeout_no_conversion",
      "tag1": null,
      "tag2": null,
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "external_client_123",
      "tag9": "campaign_step_1",
      "tag10": null
    }
  },
  "signature": "h789abcdef123456789..."
}

widget.visitorconverted

Conversion Réussie
  • Déclenchement : Immédiat lors de la prise de rendez-vous par un visiteur tracké
  • Prérequis : Widget avec système de tracking activé
  • Condition : Visiteur complète avec succès la prise de rendez-vous
  • Données incluses : Informations du visiteur, rendez-vous créé et client
  • Utilité : Mesurer les conversions et l'efficacité du widget
{
  "event": "widget.visitorconverted",
  "event_id": "evt_684313a64d8ceb.47098271",
  "timestamp": "2025-06-06T17:45:00+00:00",
  "api_version": "2.0",
  "source": "Widget",
  "entity": {
    "entity_id": 5,
    "entity_name": "Cabinet Dr. Martin",
    "entity_account_id": "account_abc123"
  },
  "data": {
    "visitor": {
      "entity_id": 5,
      "widget_id": "644660339401",
      "tracking_id": 458,
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
      "first_visit_at": "2025-06-06T17:30:00+00:00",
      "converted_at": "2025-06-06T17:45:00+00:00",
      "status": "converted",
      "conversion_time_seconds": 900,
      "tag1": null,
      "tag2": null,
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "external_client_123",
      "tag9": "campaign_step_1",
      "tag10": null
    },
    "appointment": {
      "appointment_id": 790,
      "entity_id": 5,
      "calendar_id": 15,
      "slot_id": 459,
      "client_id": 125,
      "date": "2025-06-12",
      "start_time": "10:00:00",
      "end_time": "11:00:00",
      "status": "pending",
      "status_details": {
        "key": "pending",
        "label": "En attente",
        "color": "#FFA500",
        "type": "pending",
        "is_custom": false,
        "scope": "default"
      },
      "notes": "Nouveau client via widget",
      "timezone": "Europe/Paris"
    },
    "client": {
      "client_id": 125,
      "id": 125,
      "entity_id": 5,
      "first_name": "Sophie",
      "last_name": "Bernard",
      "name": "Sophie Bernard",
      "email": "sophie.bernard@example.com",
      "phone": "33687654321",
      "tag1": "Widget",
      "tag2": "Conversion",
      "tag3": null,
      "tag4": null,
      "tag5": null,
      "tag6": null,
      "tag7": null,
      "tag8": "external_client_123",
      "tag9": "campaign_step_1",
      "tag10": null,
      "optin": 1,
      "ip_address": "192.168.1.100",
      "created_at": "2025-06-06 17:45:00",
      "updated_at": "2025-06-06 17:45:00"
    }
  },
  "signature": "i789abcdef123456789..."
}

Sécurité

Méthode HTTP : POST Uniquement

Les webhooks TAP4BOOK utilisent exclusivement la méthode HTTP POST.

  • GET non supporté : Les requêtes GET seront ignorées
  • Payload JSON : Envoyé dans le body de la requête POST
  • Content-Type : application/json
  • Signature : Transmise via header HTTP X-TAP4BOOK-Signature

Validation des Signatures HMAC

Chaque webhook inclut une signature HMAC-SHA256 générée avec votre entity_secret_key :

Important

Validez toujours la signature pour vous assurer que le webhook provient bien de TAP4BOOK.

Algorithme de génération :

// Génération côté TAP4BOOK (méthode actuelle v2.0)
$jsonPayload = json_encode($payload, JSON_UNESCAPED_UNICODE);
$secret = $entity['entity_secret_key'];
$signature = hash_hmac('sha256', $jsonPayload, $secret);

Validation côté récepteur :

function validateWebhookSignature($rawPayload, $receivedSignature, $secret) {
    if (empty($receivedSignature) || empty($secret)) {
        return false;
    }
    
    try {
        // Décoder le payload pour validation
        $data = json_decode($rawPayload, true);
        if (!$data) {
            return false;
        }
        
        // TAP4BOOK signe le payload JSON complet
        $jsonPayload = json_encode($data, JSON_UNESCAPED_UNICODE);
        $expectedSignature = hash_hmac('sha256', $jsonPayload, $secret);
        
        // Comparaison sécurisée
        return hash_equals($expectedSignature, $receivedSignature);
    } catch (Exception $e) {
        return false;
    }
}
Important : Changement de méthode de signature

Version 2.0 : TAP4BOOK signe maintenant le payload JSON complet, plus la méthode event|data utilisée dans les versions antérieures.

  • Ancienne méthode (v1.x) : hash_hmac('sha256', $event . '|' . json_encode($data), $secret)
  • Nouvelle méthode (v2.0) : hash_hmac('sha256', json_encode($payload, JSON_UNESCAPED_UNICODE), $secret)
  • Migration : Mettez à jour votre validation pour utiliser la nouvelle méthode

Headers HTTP de Sécurité

Chaque webhook inclut des headers spécifiques :

Header Valeur Description
Content-Type application/json Type de contenu du payload
User-Agent TAP4BOOK-Webhook/2.0 Identification du client webhook
X-Tap4Book-Event appointment.created Type d'événement sans parsing JSON
X-TAP4BOOK-Signature sha256_signature Signature HMAC pour validation

Politique de Retry et Gestion des Duplicatas

Politique de Retry TAP4BOOK

TAP4BOOK implémente un système de retry automatique pour garantir la livraison des webhooks :

  • Nombre maximum de tentatives : 3 essais par webhook
  • Délais entre tentatives : 1 seconde, 5 secondes, 15 secondes
  • Timeout par tentative : 10 secondes
  • Même event_id : Toutes les tentatives utilisent le même identifiant d'événement

Conditions déclenchant un retry :

Condition d'Échec Code HTTP Retry ? Raison
Timeout de connexion - ✅ Oui Problème réseau temporaire
Erreur de connexion TCP - ✅ Oui Serveur indisponible temporairement
Erreur serveur 500-599 ✅ Oui Erreur côté récepteur, peut être temporaire
Succès 200-299 ❌ Non Webhook traité avec succès
Erreur client 400-499 ❌ Non Erreur permanente (URL incorrecte, authentification, etc.)
Important : Gestion des Duplicatas

Votre endpoint DOIT implémenter la déduplication basée sur l'event_id :

// Exemple d'implémentation de déduplication
function processWebhook($webhookData) {
    $eventId = $webhookData['event_id'];
    
    // Vérifier si déjà traité
    if (isEventAlreadyProcessed($eventId)) {
        // Retourner succès sans retraiter
        http_response_code(200);
        echo json_encode(['status' => 'already_processed', 'event_id' => $eventId]);
        return;
    }
    
    // Marquer comme en cours de traitement
    markEventAsProcessing($eventId);
    
    try {
        // Traiter le webhook
        handleWebhookEvent($webhookData);
        
        // Marquer comme traité avec succès
        markEventAsProcessed($eventId);
        
        http_response_code(200);
        echo json_encode(['status' => 'processed', 'event_id' => $eventId]);
        
    } catch (Exception $e) {
        // En cas d'erreur, ne pas marquer comme traité
        markEventAsError($eventId, $e->getMessage());
        
        http_response_code(500);
        echo json_encode(['error' => 'Processing failed', 'event_id' => $eventId]);
    }
}

Bonnes Pratiques pour Éviter les Retries

Optimisation de votre Endpoint
  • Répondre rapidement : Retournez HTTP 200 immédiatement, traitez en arrière-plan si nécessaire
  • Timeout court : Limitez le traitement à moins de 8 secondes
  • Validation précoce : Vérifiez la signature et le format avant tout traitement
  • Gestion d'erreur : Retournez 4xx pour les erreurs permanentes (URL incorrecte, format invalide)
  • Logging : Loggez tous les événements pour debugging
  • Monitoring : Surveillez les performances de votre endpoint

Exemple d'Endpoint Optimisé

{"error":"Method not allowed"}