# Envoyer des données par webhook

Cette page explique **exactement quoi envoyer** à DURUM.ai pour que l'app fonctionne à son plein potentiel. Que vous utilisiez GHL, Typeform, Calendly, Stripe, Zoho, Pipedrive ou un outil custom, les règles sont les mêmes.

---

## Les 3 choses absolument essentielles

Sans ces 3 éléments, votre événement ne sera pas traité correctement.

### 1. Identifier votre compte (`client_key`)

Chaque webhook doit être associé à votre compte. Il y a deux façons de le faire :

**Option A : Dans l'URL avec `bid` (recommandé)** :
```
https://app.durum.ai/api/webhook/lead?bid=VOTRE_BUSINESS_ID
```
C'est le format que l'app vous donne dans Réglages -> Intégrations. Le `bid` est un identifiant unique impossible à deviner, donc plus sûr qu'une clé lisible. Les 8 endpoints standard l'acceptent.

**Option B : Dans l'URL avec `client_key`** :
```
https://app.durum.ai/api/webhook/lead?client_key=VOTRE_CLE
```
Toujours supporté, aucune dépréciation.

**Option C : Dans le payload JSON** :
```json
{ "client_key": "VOTRE_CLE", "email": "..." }
```

> **Danger: Sans client_key**
>
> L'événement est mis en **quarantaine**. Il ne sera pas perdu, mais il devra être classé manuellement par l'agence. Vos KPIs ne seront pas à jour en temps réel.

### 2. Identifier le contact (email ou téléphone)

DURUM.ai doit pouvoir identifier qui est le prospect. Envoyez **au minimum un** de ces champs :

| Champ | Exemple | Priorité |
|-------|---------|----------|
| `email` | `jean.tremblay@email.com` | Idéal : sert aussi à la déduplication et à l'attribution |
| `phone` | `+15145551234` | Bon backup : format E.164 ou 10 chiffres |
| `name` | `Jean Tremblay` | Utile mais insuffisant seul |

> **Warning: Sans email ni téléphone**
>
> Pour `booking`, `no-show` et `sale` (envoi JSON générique), l'événement est **refusé** (réponse `rejected: missing_contact_info`) : un rendez-vous ou une vente impossible à rattacher à un contact ne sert à rien et fausserait vos KPIs. Pour `lead` et `application`, l'événement sera enregistré, mais le système ne pourra pas :
> - Détecter les doublons (risque de compter 2 fois le même lead)
> - Relier le lead à ses futures actions (booking, vente)
> - Enrichir les données avec l'historique du contact

### 3. Envoyer au bon endpoint

Chaque type d'événement a son propre endpoint. C'est ce qui détermine le type dans DURUM.ai.

| Quand | Endpoint | Ce qui est créé |
|-------|----------|-----------------|
| Un prospect montre de l'intérêt | `/api/webhook/lead` | Lead |
| Un prospect remplit un formulaire détaillé | `/api/webhook/application` | Application |
| Un rendez-vous est réservé | `/api/webhook/booking` | Booking |
| Un prospect ne se présente pas | `/api/webhook/no-show` | No-show |
| Une vente est conclue | `/api/webhook/sale` | Vente |
| Un paiement est reçu | `/api/webhook/payment` | Paiement |
| Un remboursement est émis | `/api/webhook/refund` | Remboursement |
| Un visiteur ajoute au panier | `/api/webhook/add-to-cart` | Ajout au panier |
| Un événement à vous (hors des 8 standard) | `/api/webhook/event` | Événement custom (nommé) |

---

## Événements custom (vos propres événements)

En plus des 8 événements standard, vous pouvez envoyer **n'importe quel événement, sous le nom que vous voulez** (« Démo complétée », « Deal déplacé en colonne X », « Webinaire regardé à 80% »...).

Le nom de l'événement se met **dans l'URL**, vous n'envoyez que les données du contact dans le corps :

```
POST https://app.durum.ai/api/webhook/event?client_key=VOTRE_CLE&event_name=demo_completee&source=custom
Content-Type: application/json

{
  "email": "client@exemple.com",
  "phone": "+15145550199",
  "value": 250,
  "occurred_at": "2026-07-07T14:00:00Z"
}
```

Champs reconnus : `email`, `phone`, `value`, `occurred_at`, `contact_id`, `calendar_name`, plus les [13 champs d'attribution](#les-13-champs-d-attribution-publicitaire) si vous voulez rattacher l'événement à une publicité. Tout autre champ est conservé dans le payload brut.

> **Tip: Où créer un événement custom dans l'app**
>
> Réglages -> **Événements** -> « Ajouter un événement custom » : nommez-le, obtenez son URL de webhook dédiée + le cadre JSON. Il apparaît ensuite dans vos **Conversions** (page Conversions) et vos mappings d'intégration (ex. « ce calendrier Calendly = cet événement »).

> **Warning: Un événement != trois événements**
>
> Si 3 intégrations envoient toutes des leads, c'est **un seul** événement `Lead` alimenté par 3 sources, pas 3 événements. Pour distinguer par source, utilisez un **filtre** au niveau de la conversion, jamais un événement séparé. Un événement custom = seulement un signal qui n'existe pas dans les 8 standard.

---

## Les 13 champs d'attribution publicitaire

Ces champs permettent à DURUM.ai de **relier chaque lead, booking et vente à la publicité exacte qui l'a généré**. C'est ce qui alimente le tableau Marketing, le calcul du CPA, ROAS, ROI, et toute l'attribution dans l'app.

> **Danger: Sans ces champs, l'app perd sa valeur principale**
>
> Un lead sans UTMs arrive dans DURUM.ai mais il est **impossible de savoir de quelle publicité il vient**. Vos KPIs marketing (CPA, ROAS, ROI) seront faussés. Vous ne saurez pas quel ad performe et lequel gaspille votre budget.

### Les 5 UTMs (noms des éléments publicitaires)

Ce sont les noms lisibles de vos campagnes, publicités et adsets. DURUM.ai les utilise pour **matcher les events avec vos publicités Meta**.

| # | Champ | Alias acceptés | Ce que ça fait dans l'app | Exemple | Priorité |
|---|-------|---------------|--------------------------|---------|----------|
| 1 | `utm_campaign` | `utmcampaign`, `UTM_Campaign`, `campaign_name` | Lie l'event à la **campagne**. Utilisé dans le tableau Marketing, le filtre par campagne, et le calcul du spend par campagne. | `bootcamp-mars-2026` | **Non négociable** |
| 2 | `utm_content` | `utmcontent`, `UTM_Content`, `ad_name`, `utm_ad_name` | Lie l'event à la **publicité (ad)**. C'est LE champ qui connecte un lead à un ad dans Meta. Sans lui, pas d'attribution par ad. | `ad-temoignage-tx1-ix2` | **Non négociable** |
| 3 | `utm_source` | `utmsource`, `UTM_Source` | Identifie la **source** du trafic. Permet de distinguer Meta vs Google vs organique dans les rapports. | `meta`, `google`, `tiktok`, `organic` | Important |
| 4 | `utm_medium` | `utmmedium`, `UTM_Medium` | Identifie le **type de trafic**. Distingue le paid du organique, de l'email, du social. | `paid`, `cpc`, `email`, `social`, `organic` | Important |
| 5 | `utm_term` | `utmterm`, `UTM_Term`, `utm_adset_name`, `adset_name` | Lie l'event à l'**adset**. Permet l'analyse de performance par audience/adset. | `adset-lookalike-1pct`, `adset-retarget-30j` | Recommandé |

### Les 3 IDs Meta (identifiants numériques)

Ce sont les IDs numériques de Meta Ads. Ils permettent un **match exact** au lieu de matcher par nom (plus fiable si vous renommez une campagne).

| # | Champ | Alias acceptés | Ce que ça fait dans l'app | Exemple | Priorité |
|---|-------|---------------|--------------------------|---------|----------|
| 6 | `utm_campaign_id` | `campaign_id` | Match exact avec la **campagne Meta** par ID au lieu du nom | `120210987654321` | Recommandé |
| 7 | `utm_ad_id` | `ad_id` | Match exact avec la **publicité Meta** par ID au lieu du nom | `120210123456789` | Recommandé |
| 8 | `utm_adset_id` | `adset_id` | Match exact avec l'**adset Meta** par ID au lieu du nom | `120210111222333` | Recommandé |

> **Tip: Noms vs IDs : pourquoi les deux?**
>
> Les **noms** (`utm_campaign`, `utm_content`, `utm_term`) sont lisibles et utiles dans les tableaux. Les **IDs** (`utm_campaign_id`, `utm_ad_id`, `utm_adset_id`) sont fiables même si vous renommez une campagne. Idéalement, envoyez les deux. Si vous ne pouvez envoyer qu'un seul, envoyez les noms.

### Les 2 champs de tracking avancé

Pour l'attribution first-party et les outils tiers comme Hyros.

| # | Champ | Ce que ça fait dans l'app | Exemple | Priorité |
|---|-------|--------------------------|---------|----------|
| 9 | `fbc_id` | **Facebook Click ID** : identifiant unique du clic publicitaire Meta. Permet l'attribution même si les cookies sont bloqués. Capturé par le paramètre `fbclid` dans l'URL. | `fb.1.1612345678.abc123def456` | Optionnel (bonus) |
| 10 | `h_ad_id` | **Hyros Ad ID** : si vous utilisez Hyros pour le tracking, cet ID permet le lien direct. | `120210123456789` | Optionnel (si Hyros) |

### Les 3 champs complémentaires

| # | Champ | Alias acceptés | Ce que ça fait dans l'app | Exemple | Priorité |
|---|-------|---------------|--------------------------|---------|----------|
| 11 | `product_key` | `product`, `funnel_key` | Identifie le **produit ou service**. Alimente le funnel par produit, les revenus par produit, et les commissions. | `bootcamp_ai` | Important |
| 12 | `assigned_user_name` | `assigned_to`, `Deal_Owner`, `rep_name` | Identifie le **rep de vente** assigné. Alimente le tableau Ventes Reps et les commissions par rep. | `Marie Dupont` | Recommandé |
| 13 | `amount` | `value`, `total`, `Amount`, `Grand_Total` | **Montant en dollars** de la vente ou du paiement. Sans ce champ, le ROI, ROAS et les revenus sont à $0. | `2500` | **Non négociable** (ventes/paiements) |

> **Info: Produits reconnus automatiquement**
>
> `bootcamp_ai`, `scaler_vos_ventes`, `propulsion`, `club_prive`, `mastermind_ai`, `formation_croisiere`, `accelerateur`, `acquisition_entreprises`. Tout autre nom est accepté et converti en slug (ex : `Mon Produit` devient `mon_produit`).

### Comment configurer les UTMs dans Meta Ads

Quand vous créez une publicité Meta, configurez les paramètres URL au niveau de la publicité (Ad) :

**URL Parameters** (dans le champ "URL Parameters" de l'ad) :

```txt [Copier dans Meta Ads]
utm_source=meta&utm_medium=paid&utm_campaign={campaign.name}&utm_content={ad.name}&utm_term={adset.name}&utm_campaign_id={campaign.id}&utm_ad_id={ad.id}&utm_adset_id={adset.id}
```

Dans Meta Ads, les variables dynamiques entre accolades (`{campaign.name}`, `{ad.name}`, etc.) sont remplacées automatiquement par les vraies valeurs. Vos outils (GHL, Typeform, Calendly) capturent ensuite ces paramètres.

### Où placer les UTMs selon votre outil

| Outil | Où les mettre | Détail |
|-------|--------------|--------|
| **GHL** | Custom fields du contact | `utm_source`, `utm_campaign`, `utm_content`, `utm_term`, `utm_ad_id`, `utm_campaign_id`, `utm_adset_id` dans le tableau `customFields` |
| **Typeform** | Hidden fields du formulaire | Injectés via l'URL du formulaire (`#utm_source=meta&utm_campaign=...`) |
| **Calendly** | Paramètres UTM de la page | Calendly capture automatiquement les UTMs de la page où le lien est affiché |
| **Stripe** | Metadata du checkout | `client_key`, `product_key`, `utm_campaign`, `utm_content` dans l'objet `metadata` |
| **Zoho CRM** | Champs du deal | `UTM_Source`, `UTM_Campaign`, `UTM_Content`, `UTM_Term`, `utm_ad_id`, `utm_campaign_id` |
| **Zoho Books** | Custom fields (`cf_*`) | `cf_product_key`, `cf_source`, `cf_referent` |
| **Générique / Make / Zapier** | Champs directs dans le JSON | Tous les champs au premier niveau du payload |

### Exemple complet avec TOUS les champs d'attribution

```json
{
  "email": "jean.tremblay@email.com",
  "phone": "+15145551234",
  "name": "Jean Tremblay",
  "product_key": "bootcamp_ai",
  "amount": 2500,
  "assigned_user_name": "Marie Dupont",

  "utm_source": "meta",
  "utm_medium": "paid",
  "utm_campaign": "bootcamp-mars-2026",
  "utm_content": "ad-temoignage-tx1-ix2",
  "utm_term": "adset-lookalike-1pct",

  "utm_campaign_id": "120210987654321",
  "utm_ad_id": "120210123456789",
  "utm_adset_id": "120210111222333",

  "fbc_id": "fb.1.1612345678.abc123def456",
  "h_ad_id": "120210123456789"
}
```

---

## Les réponses aux questions de vos formulaires

Chaque client pose des questions différentes dans ses formulaires (GHL, Typeform, Calendly). **Toutes les réponses sont capturées et stockées**, même si elles ne correspondent pas à des champs standard. Rien n'est perdu.

### Ce qui est stocké

Toutes les réponses arrivent dans le champ `raw_payload` de l'événement (format JSONB). C'est un stockage complet du payload original : chaque question, chaque réponse, chaque champ custom.

En plus du stockage brut, certains champs sont **extraits automatiquement** vers des colonnes dédiées (email, téléphone, nom, UTMs). Les autres restent dans `raw_payload` et sont accessibles pour des analyses, des exports ou des filtres avancés.

### Typeform : toutes les réponses du formulaire

Chaque réponse est capturée avec la **question posée** (le titre du champ) et la **réponse donnée**, quel que soit le type.

Exemples de questions custom que vos clients posent dans leurs formulaires Typeform :

| Question dans le formulaire | Type | Exemple de réponse | Stocké dans |
|----------------------------|------|-------------------|-------------|
| Quel est votre chiffre d'affaires annuel? | `number` | `500000` | `raw_payload` |
| Quel est votre secteur d'activité? | `choice` | `E-commerce` | `raw_payload` |
| Quelles plateformes publicitaires utilisez-vous? | `choices` | `["Facebook Ads", "Google Ads"]` | `raw_payload` |
| Quel est votre objectif principal? | `text` | `Doubler mes ventes en 6 mois` | `raw_payload` |
| Combien d'employés avez-vous? | `number` | `15` | `raw_payload` |
| Avez-vous un site web? | `url` | `https://mon-entreprise.com` | `raw_payload` |
| Date souhaitée pour commencer? | `date` | `2026-04-15` | `raw_payload` |
| Acceptez-vous d'être contacté? | `boolean` | `true` | `raw_payload` |
| Votre courriel | `email` | `jean@email.com` | `raw_payload` + `contact_email` |
| Votre téléphone | `phone_number` | `+15145551234` | `raw_payload` + `contact_phone` |
| Votre nom complet | `text` (si le titre contient "nom") | `Jean Tremblay` | `raw_payload` + `contact_name` |

**Structure dans le payload** : chaque réponse est stockée comme ceci :

```json
{
  "field_ref": {
    "label": "Quel est votre chiffre d'affaires annuel?",
    "type": "number",
    "value": 500000
  }
}
```

Vous pouvez ajouter autant de questions que vous voulez dans votre Typeform. Elles seront toutes là.

### GHL : tous les custom fields du contact

GHL envoie les custom fields dans un tableau `customFields`. **Tout le tableau est lu et stocké.**

Les champs UTM et produit sont extraits vers des colonnes dédiées (voir section précédente). **Tous les autres custom fields sont conservés dans `raw_payload`.**

Exemples de custom fields que vos clients configurent dans GHL :

| Custom Field GHL | Description | Exemple |
|-----------------|-------------|---------|
| `ville` | Ville du prospect | `Montreal` |
| `entreprise` | Nom de l'entreprise | `Tremblay Inc.` |
| `chiffre_affaires` | CA annuel | `500000` |
| `nb_employes` | Taille de l'équipe | `15` |
| `budget_mensuel` | Budget marketing | `5000` |
| `secteur_activite` | Secteur | `E-commerce` |
| `objectif_principal` | Objectif | `Augmenter mes ventes de 50%` |
| `comment_avez_vous_entendu` | Source déclarée | `Publicite Facebook` |
| `qualification_score` | Score attribué par le rep | `8` |
| `revenue_attendu` | Revenue estimé de la vente | `2500` |
| `type_entreprise` | B2B, B2C, D2C | `B2C` |
| `experience_pub` | Expérience publicitaire | `Debutant` |
| `outil_crm_actuel` | CRM utilisé | `HubSpot` |
| `delai_decision` | Urgence | `Ce mois-ci` |

**Comment les envoyer** :

```json
{
  "contact": {
    "email": "jean@email.com",
    "customFields": [
      { "field_name": "ville", "value": "Montreal" },
      { "field_name": "entreprise", "value": "Tremblay Inc." },
      { "field_name": "chiffre_affaires", "value": "500000" },
      { "field_name": "nb_employes", "value": "15" },
      { "field_name": "objectif_principal", "value": "Augmenter mes ventes" },
      { "field_name": "budget_mensuel", "value": "5000" },
      { "field_name": "qualification_score", "value": "8" }
    ]
  }
}
```

> **Info: Pas de limite**
>
> Vous pouvez ajouter autant de custom fields que nécessaire. Il n'y a pas de liste prédéterminée : tout ce que vous envoyez dans `customFields` est stocké et accessible.

### Calendly : les questions de réservation

Calendly permet de poser des questions custom quand quelqu'un réserve un rendez-vous. Elles arrivent dans le tableau `questions_and_answers`.

Exemples de questions configurées dans Calendly :

| Question Calendly | Réponse type |
|------------------|-------------|
| Quel est le nom de votre entreprise? | `Tremblay Inc.` |
| Quel est votre chiffre d'affaires annuel? | `500 000$` |
| Quel est votre objectif principal pour cet appel? | `Valider si le bootcamp est pour moi` |
| Comment avez-vous entendu parler de nous? | `Publicite Facebook` |
| Nombre d'employés? | `15` |
| Avez-vous déjà fait de la publicité en ligne? | `Oui, Facebook Ads depuis 6 mois` |
| Quel est votre budget mensuel en publicité? | `3000-5000$` |

**Structure dans le payload Calendly** :

```json
{
  "questions_and_answers": [
    {
      "question": "Quel est le nom de votre entreprise?",
      "answer": "Tremblay Inc."
    },
    {
      "question": "Quel est votre chiffre d'affaires annuel?",
      "answer": "500 000$"
    },
    {
      "question": "Objectif principal pour cet appel?",
      "answer": "Valider si le bootcamp est pour moi"
    }
  ]
}
```

### Zoho Books : custom fields et line items

Zoho Books envoie les custom fields (préfixés `cf_`) et les line items (produits facturés).

| Custom Field Zoho | Description |
|-------------------|-------------|
| `cf_product_key` | Identifiant du produit |
| `cf_source` | Source de la vente (`DURUM`, `ORGANIC`) |
| `cf_referent` | Vendeur/référent |
| `cf_af_contact_id` | ID GHL du contact (via ActiveFunnel) |
| Tout autre `cf_*` | Stocké dans `raw_payload` |

Les **line items** (produits facturés) sont aussi capturés :

```json
{
  "line_items": [
    {
      "name": "Bootcamp IA - Session Mars 2026",
      "rate": 2500,
      "quantity": 1,
      "item_total": 2500
    },
    {
      "name": "Materiel supplementaire",
      "rate": 200,
      "quantity": 1,
      "item_total": 200
    }
  ]
}
```

Le système identifie automatiquement le **produit principal** (item avec le plus gros montant, en excluant les frais bancaires comme `Stripe Processing Fees` ou `Compte Stripe`).

### Générique / Make / Zapier : champs libres

Si vous construisez le JSON vous-même, ajoutez vos champs custom directement dans le payload. Ils seront tous stockés :

```json
{
  "email": "jean@email.com",
  "client_key": "avego",
  "utm_campaign": "bootcamp-mars-2026",

  "entreprise": "Tremblay Inc.",
  "chiffre_affaires": 500000,
  "nb_employes": 15,
  "secteur": "E-commerce",
  "objectif": "Augmenter mes ventes de 50%",
  "budget_pub": 5000,
  "source_declaree": "Publicite Facebook",
  "questions_custom": {
    "experience_pub": "Facebook Ads depuis 6 mois",
    "outil_actuel": "HubSpot",
    "delai": "Ce mois-ci"
  }
}
```

Tout est conservé dans `raw_payload`. Les champs standard (`email`, `utm_campaign`, etc.) sont en plus extraits vers des colonnes dédiées.

---

## Par type d'événement : quoi envoyer

### Lead ou Application

Un prospect montre de l'intérêt ou remplit un formulaire.

```
POST /api/webhook/lead?client_key=VOTRE_CLE
Content-Type: application/json
```

```json
{
  "email": "jean.tremblay@email.com",
  "phone": "+15145551234",
  "name": "Jean Tremblay",
  "product_key": "bootcamp_ai",
  "utm_source": "meta",
  "utm_campaign": "bootcamp-mars-2026",
  "utm_content": "ad-temoignage-tx1"
}
```

| Champ | Statut | Pourquoi |
|-------|--------|----------|
| `email` | Essentiel | Identifie le contact et permet la déduplication |
| `utm_campaign` | Essentiel | Attribution à la campagne |
| `utm_content` | Essentiel | Attribution à la publicité |
| `phone` | Recommandé | Backup d'identification + enrichissement |
| `name` | Recommandé | Affichage dans les tableaux |
| `product_key` | Recommandé | Classification par produit/funnel |
| `utm_source` | Recommandé | Source du trafic |
| `utm_ad_id` | Optionnel | Lien direct avec Meta |

### Booking (rendez-vous)

Un prospect réserve un rendez-vous.

```
POST /api/webhook/booking?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "name": "Jean Tremblay",
  "calendar_name": "Appel Decouverte",
  "booking_date": "2026-04-05",
  "booking_time": "14:00",
  "assigned_user_name": "Marie Dupont",
  "utm_campaign": "bootcamp-mars-2026",
  "utm_content": "ad-temoignage-tx1"
}
```

| Champ | Statut | Pourquoi |
|-------|--------|----------|
| `email` | Essentiel | Relie le booking au lead original |
| `booking_date` | Essentiel | Distingue "date de création" vs "date du RDV" |
| `booking_time` | Recommandé | Analyse des créneaux no-show |
| `calendar_name` | Recommandé | Type de rendez-vous dans les tableaux |
| `assigned_user_name` | Recommandé | Associe au rep pour le suivi de performance |
| UTMs | Important | Si pas déjà sur le lead, c'est la dernière chance de les capturer |

> **Info: Booking date vs event date**
>
> DURUM.ai distingue la **date de création du booking** (quand le prospect a réservé) de la **date du rendez-vous** (quand le meeting aura lieu). C'est critique pour calculer correctement les taux de no-show et le pipeline.

### No-show

Un prospect ne se présente pas à son rendez-vous.

```
POST /api/webhook/no-show?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "name": "Jean Tremblay",
  "calendar_name": "Appel Decouverte",
  "booking_date": "2026-04-05",
  "booking_time": "14:00"
}
```

### Vente

Un deal est conclu (gagné ou perdu).

```
POST /api/webhook/sale?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "name": "Jean Tremblay",
  "amount": 2500,
  "product_key": "bootcamp_ai",
  "status": "won",
  "assigned_user_name": "Marie Dupont",
  "utm_campaign": "bootcamp-mars-2026",
  "utm_content": "ad-temoignage-tx1"
}
```

| Champ | Statut | Pourquoi |
|-------|--------|----------|
| `email` | Essentiel | Relie la vente au lead et au booking |
| `amount` | Essentiel | Calcul du revenu, ROI, ticket moyen |
| `status` | Important | `won` = vente, `lost` = perdue. Par défaut : vente. |
| `product_key` | Recommandé | Revenu par produit, commissions |
| `assigned_user_name` | Recommandé | Performance par rep |
| UTMs | Important | Attribution de la vente à la publicité |

> **Danger: Sans montant**
>
> La vente apparaît dans le funnel mais avec `$0`. Le ROI, le ROAS et les revenus seront faussés.

### Paiement

Un paiement est reçu (facture payée).

```
POST /api/webhook/payment?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "name": "Jean Tremblay",
  "amount": 2500,
  "product_key": "bootcamp_ai",
  "invoice_number": "INV-001234",
  "payment_status": "paid"
}
```

| Champ | Statut | Pourquoi |
|-------|--------|----------|
| `email` | Essentiel | Relie le paiement au client |
| `amount` | Essentiel | Montant encaissé |
| `invoice_number` | Recommandé | Déduplication (évite de compter 2 fois si le webhook est renvoyé) |
| `payment_status` | Recommandé | `paid` = encaissé, `overdue` = en retard |
| `product_key` | Recommandé | Revenu par produit |

### Remboursement

```
POST /api/webhook/refund?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "amount": 2500
}
```

### Ajout au panier

Un panier abandonné, c'est un ajout au panier qui n'est pas suivi d'une vente. Envoyez donc l'ajout au panier ici, et la vente sur `/api/webhook/sale` comme d'habitude : DURUM.ai fait la différence, et peut pousser la conversion `AddToCart` aux régies (Meta, TikTok, Google, Snapchat, LinkedIn) depuis Réglages > Conversions.

```
POST /api/webhook/add-to-cart?client_key=VOTRE_CLE
```

```json
{
  "email": "jean.tremblay@email.com",
  "value": 97,
  "currency": "CAD",
  "product_key": "formation-impots",
  "product_name": "Formation Impôts",
  "utm_source": "facebook",
  "utm_campaign": "Impots-Sept",
  "utm_content": "Ad-Temoignage-12"
}
```

| Champ | Statut | Pourquoi |
|-------|--------|----------|
| `email` ou `phone` | Requis (sauf pixel, voir ci-dessous) | Identifier la personne et relier au lead, à la vente |
| `value` + `currency` | Recommandé | Valeur du panier, envoyée telle quelle à la régie |
| `product_key` (ou `content_ids`, `sku`) | Recommandé | Produit ajouté (`content_ids` côté Meta) |
| `product_name` (ou `content_name`) | Optionnel | Nom lisible du produit |
| `utm_*`, `fbc_id`, `gclid` | Recommandé | Attribution à la publicité (sinon, DURUM.ai reprend celle du lead) |

**Depuis une page avec le pixel DURUM.ai installé**, aucun webhook à brancher : appelez `durum.track('AddToCart', { value: 97, currency: 'CAD', content_ids: ['formation-impots'] })` au clic sur le bouton, ou posez `data-durum-event="AddToCart"` (avec `data-durum-value`, `data-durum-currency`, `data-durum-content-id`) sur le bouton. Le pixel envoie l'événement au serveur avec le visiteur, ses UTM et ses identifiants de clic ; l'email est retrouvé si ce visiteur est déjà connu (formulaire rempli plus tôt). La conversion part aux régies depuis le serveur, une seule fois, jamais depuis le navigateur.

---

## Récapitulatif : hiérarchie d'importance

| Niveau | Champs | Impact si absent |
|--------|--------|-----------------|
| **Non négociable** | `client_key` (URL ou payload) | Événement en quarantaine, pas dans vos KPIs |
| **Non négociable** | `email` ou `phone` | Pas de déduplication, pas de lien entre les étapes du funnel |
| **Non négociable** | `utm_campaign` + `utm_content` | **Pas d'attribution.** Impossible de savoir quel ad a généré le lead/vente. CPA, ROAS et ROI non calculables. |
| **Non négociable** | `amount` (ventes et paiements) | Revenus à $0, ROI et ROAS faussés |
| **Important** | `utm_source` + `utm_medium` | Impossible de distinguer Meta vs Google vs organique |
| **Important** | `product_key` | Pas de funnel par produit, pas de commissions par produit |
| **Recommandé** | `utm_term` | Pas d'analyse par adset/audience |
| **Recommandé** | `utm_campaign_id` + `utm_ad_id` + `utm_adset_id` | Match par nom au lieu d'ID exact (fragile si vous renommez) |
| **Recommandé** | `name`, `assigned_user_name` | Moins de détails dans les tableaux, pas de perf par rep |
| **Bonus** | `fbc_id`, `h_ad_id` | Attribution avancée first-party et Hyros |

---

## Ce que DURUM.ai fait automatiquement

Vous n'avez pas besoin d'envoyer toutes les données dans chaque webhook. Le système fait un travail important en arrière-plan.

### Enrichissement UTM automatique

Si un webhook arrive **sans UTMs** (par exemple un paiement Zoho Books), DURUM.ai cherche automatiquement les UTMs dans cet ordre :

1. **Custom fields GHL** : si le contact existe dans GHL avec des champs UTM configurés, ils sont récupérés
2. **Tracker navigateur** : si le prospect a visité votre site avec le tracker DURUM.ai installé, ses UTMs ont été capturés automatiquement (premier clic)
3. **Events précédents** : si le même email a déjà un lead ou un booking avec des UTMs, ils sont hérités

Concrètement : si un prospect remplit un formulaire avec les bons UTMs, puis réserve un booking, puis paie via Stripe, **même si Stripe n'envoie aucun UTM**, la vente sera attribuée à la bonne publicité grâce au lead original.

> **Tip**
>
> C'est pourquoi les UTMs sur le **premier point de contact** (lead ou application) sont si importants. Même si les étapes suivantes n'en ont pas, l'attribution fonctionnera.

### Déduplication intelligente

DURUM.ai empêche automatiquement les doublons. Vous n'avez pas à vous en soucier si un webhook est envoyé 2 fois.

| Source | Méthode de dedup | Ce qui est vérifié |
|--------|-----------------|-------------------|
| Typeform | Token de soumission | Chaque soumission a un token unique |
| Calendly | URI de l'invité | Chaque invité a une URI unique |
| Zoho Books | Numéro de facture | Même `invoice_number` = même événement |
| GHL / Générique | Email + type + fenêtre temps | Même email + même type dans les 24h (leads) ou 2 min (autres) |

### Rep assigné automatiquement

Si vous n'envoyez pas le nom du rep (`assigned_user_name`), le système le cherche automatiquement :
1. Dans le contact GHL (champ `assigned_to`)
2. Dans les events précédents du même contact
3. Via le ID utilisateur GHL mappé à un rep dans DURUM.ai

### Client identifié automatiquement

Si le `client_key` n'est pas dans l'URL, le système tente de l'identifier via :
- L'email du contact (s'il existe déjà dans un compte)
- Le numéro de téléphone
- L'ID de contact GHL
- Le numéro de facture
- L'ID client Zoho ou Stripe Connect

---

## Alias acceptés par champ

DURUM.ai accepte plusieurs noms pour le même champ. Utilisez celui qui correspond à votre outil.

| Ce que DURUM.ai stocke | Noms acceptés |
|----------------------|---------------|
| `contact_email` | `email`, `contact_email`, `customer_email` |
| `contact_phone` | `phone`, `contact_phone`, `customer_phone` |
| `contact_name` | `name`, `contact_name`, `customer_name` |
| `client_key` | `client_key`, `clientKey` |
| `product_key` | `product_key`, `product`, `funnel_key` |
| `utm_source` | `utm_source`, `utmsource`, `UTM_Source` |
| `utm_campaign` | `utm_campaign`, `utmcampaign`, `UTM_Campaign` |
| `utm_content` | `utm_content`, `utmcontent`, `UTM_Content` |
| `utm_term` | `utm_term`, `utmterm`, `UTM_Term`, `utm_adset_name` |
| `meta_ad_id` | `utm_ad_id`, `ad_id` |
| `meta_campaign_id` | `utm_campaign_id`, `campaign_id` |
| `meta_adset_id` | `utm_adset_id`, `adset_id` |
| `value` (montant) | `amount`, `value`, `total`, `Amount`, `Grand_Total` |
| `assigned_user_name` | `assigned_user_name`, `assigned_to`, `Deal_Owner`, `rep_name` |

---

## Spécificités par plateforme

Chaque outil envoie les données dans un format différent. Voici où placer les champs importants selon votre outil.

### GHL (GoHighLevel)

Les UTMs et le produit vont dans les **custom fields du contact** :

```json
{
  "contact": {
    "email": "jean@email.com",
    "customFields": [
      { "field_name": "utm_campaign", "value": "bootcamp-mars-2026" },
      { "field_name": "utm_content", "value": "ad-temoignage-tx1" },
      { "field_name": "product_key", "value": "bootcamp_ai" }
    ]
  }
}
```

L'URL doit inclure `?client_key=VOTRE_CLE&source=ghl`.

Vous pouvez ajouter n'importe quel custom field supplémentaire (`ville`, `entreprise`, `chiffre_affaires`, etc.). Ils seront tous conservés dans les données brutes.

> **Details: Voir le payload GHL complet (lead)**
>
> ```json
> {
>   "contact": {
>     "id": "abc123",
>     "firstName": "Jean",
>     "lastName": "Tremblay",
>     "email": "jean.tremblay@email.com",
>     "phone": "+15145551234",
>     "dateAdded": "2026-03-30T14:30:00.000Z",
>     "tags": ["lead", "bootcamp-ai"],
>     "assignedTo": "rep-user-id",
>     "customFields": [
>       { "field_name": "utm_source", "value": "meta" },
>       { "field_name": "utm_campaign", "value": "bootcamp-mars-2026" },
>       { "field_name": "utm_content", "value": "ad-temoignage-tx1" },
>       { "field_name": "utm_term", "value": "adset-lookalike-1pct" },
>       { "field_name": "utm_ad_id", "value": "120210123456789" },
>       { "field_name": "utm_campaign_id", "value": "120210987654321" },
>       { "field_name": "product_key", "value": "bootcamp_ai" },
>       { "field_name": "ville", "value": "Montreal" },
>       { "field_name": "entreprise", "value": "Tremblay Inc." }
>     ]
>   },
>   "locationId": "ghl-location-abc123",
>   "form": {
>     "id": "form-xyz-789",
>     "name": "Formulaire Bootcamp IA"
>   }
> }
> ```

> **Details: Voir le payload GHL complet (booking)**
>
> ```json
> {
>   "id": "appt-abc123",
>   "contactId": "contact-xyz789",
>   "contact": {
>     "email": "jean.tremblay@email.com",
>     "name": "Jean Tremblay",
>     "customFields": [
>       { "field_name": "utm_campaign", "value": "bootcamp-mars-2026" }
>     ]
>   },
>   "calendarName": "Appel Decouverte",
>   "startTime": "2026-04-05T14:00:00.000Z",
>   "appointmentStatus": "scheduled",
>   "assignedUserId": "user-rep-123",
>   "locationId": "ghl-location-abc123"
> }
> ```

> **Details: Voir le payload GHL complet (vente)**
>
> ```json
> {
>   "opportunity": {
>     "id": "opp-abc123",
>     "status": "won",
>     "monetaryValue": 2500
>   },
>   "contact": {
>     "id": "contact-xyz789",
>     "email": "jean.tremblay@email.com",
>     "name": "Jean Tremblay",
>     "customFields": [
>       { "field_name": "product_key", "value": "bootcamp_ai" }
>     ]
>   },
>   "locationId": "ghl-location-abc123"
> }
> ```

### Typeform

Les UTMs et le client_key vont dans les **hidden fields** du formulaire :

```
https://form.typeform.com/to/XXXXX#client_key=VOTRE_CLE&utm_source=meta&utm_campaign=bootcamp-mars-2026&utm_content=ad-temoignage-tx1&product_key=bootcamp_ai
```

Les réponses du formulaire (email, téléphone, nom, questions custom) sont extraites automatiquement depuis le tableau `answers`.

> **Details: Voir le payload Typeform complet**
>
> ```json
> {
>   "form_response": {
>     "form_id": "sJ5lztlq",
>     "token": "unique-submission-token",
>     "definition": { "title": "Formulaire Bootcamp IA" },
>     "hidden": {
>       "client_key": "VOTRE_CLE",
>       "utm_source": "meta",
>       "utm_campaign": "bootcamp-mars-2026",
>       "utm_content": "ad-temoignage-tx1",
>       "utm_ad_id": "120210123456789",
>       "product_key": "bootcamp_ai"
>     },
>     "answers": [
>       { "type": "email", "email": "jean@email.com", "field": { "title": "Votre courriel" } },
>       { "type": "phone_number", "phone_number": "+15145551234", "field": { "title": "Telephone" } },
>       { "type": "text", "text": "Jean Tremblay", "field": { "title": "Votre nom" } },
>       { "type": "choice", "choice": { "label": "E-commerce" }, "field": { "title": "Secteur" } },
>       { "type": "number", "number": 500000, "field": { "title": "Chiffre d'affaires" } }
>     ]
>   }
> }
> ```

### Calendly

Les UTMs sont capturés automatiquement dans l'objet `tracking` si l'URL de la page de réservation contient les paramètres UTM. Les questions personnalisées sont dans `questions_and_answers`.

L'URL doit inclure `?client_key=VOTRE_CLE`.

> **Details: Voir le payload Calendly complet**
>
> ```json
> {
>   "event": "invitee.created",
>   "payload": {
>     "event_type": { "name": "Appel Decouverte" },
>     "invitee": {
>       "email": "jean.tremblay@email.com",
>       "name": "Jean Tremblay",
>       "text_reminder_number": "+15145551234",
>       "uri": "https://api.calendly.com/scheduled_events/xxx/invitees/yyy"
>     },
>     "scheduled_event": {
>       "start_time": "2026-04-05T14:00:00.000Z",
>       "event_memberships": [{ "user_name": "Marie Dupont" }]
>     },
>     "tracking": {
>       "utm_source": "meta",
>       "utm_campaign": "bootcamp-mars-2026",
>       "utm_content": "ad-temoignage-tx1"
>     },
>     "questions_and_answers": [
>       { "question": "Votre entreprise?", "answer": "Tremblay Inc." },
>       { "question": "Chiffre d'affaires?", "answer": "500 000$" }
>     ]
>   }
> }
> ```

### Stripe

Le `client_key` et le `product_key` vont dans les **metadata** du checkout session ou de la facture. Les montants sont en **cents** (divisés automatiquement par 100).

Pas besoin de `client_key` dans l'URL : Stripe est détecté par sa signature.

> **Details: Voir le payload Stripe complet**
>
> ```json
> {
>   "type": "checkout.session.completed",
>   "data": {
>     "object": {
>       "customer_email": "jean.tremblay@email.com",
>       "amount_total": 250000,
>       "payment_status": "paid",
>       "customer_details": {
>         "name": "Jean Tremblay",
>         "email": "jean.tremblay@email.com"
>       },
>       "metadata": {
>         "client_key": "VOTRE_CLE",
>         "product_key": "bootcamp_ai",
>         "utm_campaign": "bootcamp-mars-2026"
>       }
>     }
>   }
> }
> ```

### Zoho CRM

Les UTMs sont dans des champs du deal (`UTM_Source`, `UTM_Campaign`, `UTM_Content`). Le `client_key` doit être dans l'URL.

> **Details: Voir le payload Zoho CRM complet**
>
> ```json
> {
>   "data": [{
>     "Deal_Name": "Jean Tremblay - Bootcamp AI",
>     "Stage": "Closed Won",
>     "Amount": 2500,
>     "Email": "jean.tremblay@email.com",
>     "Contact_Name": "Jean Tremblay",
>     "Owner": { "name": "Marie Dupont" },
>     "Product": "bootcamp_ai",
>     "UTM_Source": "meta",
>     "UTM_Campaign": "bootcamp-mars-2026",
>     "UTM_Content": "ad-temoignage-tx1"
>   }]
> }
> ```

### Zoho Books

Le `client_key` **doit** être dans l'URL (Zoho Books ne l'envoie pas dans le payload). Les custom fields Zoho sont préfixés par `cf_`.

> **Details: Voir le payload Zoho Books complet**
>
> ```json
> {
>   "payment": {
>     "amount": 2500,
>     "payment_status": "paid",
>     "customer_name": "Jean Tremblay",
>     "customer_email": "jean.tremblay@email.com",
>     "invoices": [{ "invoice_number": "INV-001234", "amount_applied": 2500 }],
>     "custom_fields": [
>       { "api_name": "cf_client_key", "value": "VOTRE_CLE" },
>       { "api_name": "cf_product_key", "value": "bootcamp_ai" },
>       { "api_name": "cf_source", "value": "DURUM" },
>       { "api_name": "cf_referent", "value": "Marie Dupont" }
>     ]
>   }
> }
> ```

### Pipedrive

L'URL doit inclure `?client_key=VOTRE_CLE&source=pipedrive`. Les emails et téléphones sont des tableaux avec `primary: true`.

> **Details: Voir le payload Pipedrive complet (deal gagné)**
>
> ```json
> {
>   "meta": { "action": "updated", "object": "deal", "company_id": 12345 },
>   "current": {
>     "title": "Jean Tremblay - Bootcamp IA",
>     "status": "won",
>     "value": 2500,
>     "person_id": {
>       "name": "Jean Tremblay",
>       "email": [{ "value": "jean@email.com", "primary": true }],
>       "phone": [{ "value": "+15145551234", "primary": true }]
>     },
>     "user_id": { "name": "Marie Dupont" },
>     "won_time": "2026-03-30T15:00:00Z"
>   },
>   "previous": { "status": "open" }
> }
> ```

---

## Tester votre webhook

Copiez-collez cette commande dans votre terminal pour envoyer un lead de test :

```bash
curl -X POST "https://app.durum.ai/api/webhook/lead?client_key=VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test-webhook@votredomaine.com",
    "name": "Test Webhook",
    "utm_source": "test",
    "utm_campaign": "test-webhook",
    "utm_content": "test-ad"
  }'
```

Puis vérifiez dans DURUM.ai > **Logs Data** que l'événement apparaît.

---

## Dépannage rapide

| Symptôme | Cause | Solution |
|----------|-------|----------|
| Événement absent de l'app | `client_key` manquant ou invalide | Ajoutez `?client_key=VOTRE_CLE` à l'URL |
| Statut `quarantined` | Formulaire ou client non reconnu | Normal pour un nouveau formulaire. L'agence le catégorise. |
| Réponse `deduped: true` | Événement déjà reçu | Normal. Le doublon a été détecté. |
| UTMs vides dans le dashboard | Pas d'UTMs dans le webhook | Configurez les hidden fields (Typeform), custom fields (GHL) ou tracking (Calendly) |
| Montant à $0 sur une vente | Champ `amount` absent | Ajoutez le montant dans le payload |
| Réponse 401 | Authentification échouée | Pour GHL : ajoutez `&source=ghl`. Pour Stripe : vérifiez la signature. |
| Réponse 429 | Trop de requêtes | Maximum 60 par minute par IP |
| Réponse 202 | Serveur temporairement occupé | L'événement est en file d'attente et sera traité sous quelques minutes |

---

---

Source: https://docs.durum.ai/integrations/payloads-webhook
