ITSkillsCenter
E-commerce

Intégrer WhatsApp Business API à HubSpot ou Odoo en 2026

19 دقائق للقراءة

📍 Article principal de la série : WhatsApp Cloud API en 2026 : architecture, webhooks, templates et intégrations

Recevoir un message WhatsApp dans une boîte mobile isolée n’apporte presque rien à une équipe commerciale. Tant que la conversation reste cloisonnée dans le téléphone d’un vendeur, l’historique client est incomplet, l’attribution est impossible, et aucun workflow d’automatisation ne peut s’enclencher. Pousser chaque message entrant et sortant dans le CRM transforme WhatsApp en un canal de premier niveau au même titre que l’email : on retrouve la conversation depuis la fiche contact, on déclenche des séquences depuis un statut d’opportunité, et on mesure le taux de réponse par campagne. Ce tutoriel détaille comment relier la Cloud API à HubSpot puis à Odoo, deux CRM aux philosophies opposées mais aux objectifs proches.

Prérequis

Avant d’écrire la moindre ligne d’intégration, il faut s’assurer que la base WhatsApp fonctionne déjà. La Cloud API doit être active, c’est-à-dire qu’un numéro de téléphone est rattaché à une WABA (WhatsApp Business Account) vérifiée, qu’un Phone Number ID et un User Access Token (idéalement de longue durée ou un System User Token) sont disponibles, et qu’un webhook public répond aux GET de vérification de Meta. Les détails de cette mise en place sortent du périmètre de ce guide ; ils sont traités dans l’article principal de la série.

Côté CRM, il faut une instance HubSpot (un Hub gratuit suffit pour le développement, mais les Communications nécessitent au moins le palier Marketing Hub Pro pour être pleinement exploitées) avec les droits administrateur permettant de créer des Apps privées, ou une instance Odoo 17 ou 18 (Community ou Enterprise) avec un compte disposant des droits d’accès en lecture-écriture sur res.partner et mail.message. Un middleware Node.js, Python ou PHP exposé en HTTPS sert de pont entre Meta et le CRM ; n’importe quel runtime serverless ou conteneur léger fait l’affaire, à condition qu’il puisse répondre en moins de cinq secondes au webhook de Meta.

Architecture cible : webhook → middleware → CRM

Le schéma de circulation est simple à décrire mais important à respecter pour éviter les boucles de duplication. Lorsqu’un client envoie un message WhatsApp à votre numéro professionnel, Meta déclenche un POST vers votre webhook avec un objet contenant entry[].changes[].value.messages[]. Le middleware répond immédiatement par un statut 200 pour acquitter la réception, puis traite la charge utile en arrière-plan : il extrait le numéro de l’expéditeur depuis messages[0].from, recherche le contact correspondant dans le CRM, le crée s’il n’existe pas, puis enregistré le message comme un engagement ou une activité associée à ce contact. Pour les messages sortants déclenchés depuis le CRM, le flux est inversé : un workflow ou un bouton dans la fiche contact appelle votre middleware, qui lui-même appelle l’endpoint POST /PHONE_NUMBER_ID/messages de la Cloud API.

Cette séparation en trois couches à un mérite essentiel : elle isole la logique métier (mapping, enrichissement, déduplication) du protocole WhatsApp et du SDK CRM. Si demain vous changez de CRM, seule la couche d’écriture est à réécrire. Si Meta modifie un payload, seule la couche d’extraction bouge.

Choisir entre HubSpot et Odoo : comparatif rapide

Le choix entre les deux plateformes dépend moins de la richesse fonctionnelle de leurs API que du contexte d’usage. HubSpot est pensé pour les équipes marketing et vente qui veulent industrialiser des séquences sans écrire de code ; Odoo est une suite ERP modulaire qui aligne ventes, stocks et facturation sur la même base de données. Le tableau ci-dessous résume les différences qui pèsent dans une intégration WhatsApp.

Critère HubSpot Odoo 17 / 18
API contacts REST v3, JSON, OAuth ou Private App Token XML-RPC et JSON-RPC, authentification user/password ou clé API
Modèle de message Engagements (Notes, Calls, Communications) mail.message attaché à res.partner
Module WhatsApp natif Intégration officielle via Marketplace Module whatsapp Enterprise depuis Odoo 17
Workflows Builder visuel, déclenchement par propriété Automated Actions et règles serveur
Public cible Marketing et vente Vente, opérations, ERP intégré

Si votre besoin principal est de relier WhatsApp à un parcours commercial avec scoring et nurturing, HubSpot offre le chemin le plus court. Si WhatsApp doit aussi déclencher des bons de commande ou notifier l’expédition d’un colis depuis le module Stock, Odoo est plus naturel. Rien n’empêche par ailleurs de connecter les deux à votre middleware si l’organisation utilise déjà l’un et migre vers l’autre.

Étape 1 — Créer une App privée HubSpot et récupérer le Private App Token

HubSpot a remplacé les anciennes API Keys par les Private Apps. Une Private App est une intégration interne à un seul portail, qui expose un token Bearer limité aux scopes que vous lui accordez. Connectez-vous à votre portail, ouvrez le menu Paramètres, naviguez vers Intégrations puis Applications privées, et cliquez sur Créer une application privée. Donnez-lui un nom explicite comme « WhatsApp Bridge », puis ouvrez l’onglet Périmètres et cochez au minimum crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.companies.read ainsi que crm.objects.communications.read et crm.objects.communications.write. Validez la création, puis affichez le token : il commence par pat- et ne sera plus jamais affiché en clair par la suite, copiez-le dans votre gestionnaire de secrets.

Une fois le token en main, vérifiez qu’il fonctionne avec un appel minimal qui liste les premiers contacts. Cette commande sert de test de connectivité avant tout développement.

curl -X GET 'https://api.hubapi.com/crm/v3/objects/contacts?limit=1' \
  -H 'Authorization: Bearer pat-eu1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' \
  -H 'Content-Type: application/json'

La réponse attendue est un objet JSON contenant un tableau results avec un contact (ou un tableau vide si le portail est neuf) et un objet paging. Si vous recevez un code 401, le token est invalide ou les scopes ne couvrent pas la lecture des contacts. Un code 403 signale le plus souvent un mauvais data center : les portails créés en Europe utilisent api.hubapi.com mais certaines fonctions sont régionalisées, vérifiez l’URL de votre portail.

Étape 2 — Identifier le contact ou le créer depuis le numéro WhatsApp

Quand un message arrive, votre middleware reçoit le numéro de l’expéditeur sous la forme 221771234567, sans le signe plus, comme défini par Meta. La première opération consiste à chercher ce numéro dans HubSpot via l’endpoint Search, qui accepte des filtres sur n’importe quelle propriété. Stockez le numéro dans la propriété standard phone ou dans une propriété personnalisée whatsapp_number si vous voulez distinguer le numéro WhatsApp du numéro principal.

POST https://api.hubapi.com/crm/v3/objects/contacts/search
Authorization: Bearer pat-eu1-xxxx
Content-Type: application/json

{
  "filterGroups": [{
    "filters": [{
      "propertyName": "phone",
      "operator": "EQ",
      "value": "+221771234567"
    }]
  }],
  "properties": ["firstname", "lastname", "phone", "email"],
  "limit": 1
}

Le résultat contient soit un contact existant dans results[0], soit un tableau vide. Dans ce second cas, vous créez le contact en posant un nouvel objet sur l’endpoint POST /crm/v3/objects/contacts avec un payload properties contenant au minimum le téléphone et un nom provisoire (par exemple « Contact WhatsApp 771234567 »), que le commercial enrichira plus tard. Pensez à normaliser le numéro avec un préfixe international avant de le stocker, sinon les recherches futures échoueront sur des incohérences de format.

Étape 3 — Créer un engagement Note ou Communication via API HubSpot

HubSpot expose plusieurs types d’engagements pour matérialiser une interaction : Note, Call, Email, Meeting, Task, et depuis la refonte de l’inbox, Communications qui regroupe SMS, WhatsApp et LinkedIn. L’objet Communication est le plus adapté pour notre cas car il porte un champ hs_communication_channel_type qui accepte la valeur WHATSAPP et un hs_communication_logged_from pour distinguer les messages entrants des sortants. Pour rester compatible avec les portails Starter qui n’ont pas Communications activé, certaines équipes se rabattent sur des Notes typées avec un préfixe « WhatsApp : ».

POST https://api.hubapi.com/crm/v3/objects/communications
Authorization: Bearer pat-eu1-xxxx
Content-Type: application/json

{
  "properties": {
    "hs_communication_channel_type": "WHATSAPP",
    "hs_communication_body": "Bonjour, je voudrais commander la robe longue beige taille M.",
    "hs_communication_logged_from": "CRM",
    "hs_timestamp": "2026-04-30T09:14:22Z"
  },
  "associations": [{
    "to": { "id": "12345678901" },
    "types": [{
      "associationCategory": "HUBSPOT_DEFINED",
      "associationTypeId": 81
    }]
  }]
}

L’associationTypeId de valeur 81 lie la Communication au contact ; HubSpot publie la liste complète des types d’association dans sa documentation. Une création réussie renvoie un code 201 avec l’objet créé et son id : conservez-le, il vous servira plus tard pour mettre à jour le statut de lecture ou rattacher des pièces jointes. Si la création échoue avec un 400 sur la propriété hs_communication_channel_type, c’est que le type WHATSAPP n’est pas activé sur votre portail ; basculez alors sur des Notes en attendant.

Étape 4 — Pousser les messages entrants WhatsApp comme conversations CRM

L’enchaînement complet vit dans votre handler webhook. Le code ci-dessous illustre la logique en Node.js avec Express et le client officiel @hubspot/api-client. Il extrait le premier message du payload Meta, normalise le numéro, cherche ou crée le contact, puis crée la Communication. Les opérations critiques sont enveloppées dans un try-catch qui logge sans relancer, pour que le 200 soit toujours retourné à Meta même en cas d’erreur côté CRM.

app.post('/webhook/whatsapp', async (req, res) => {
  res.sendStatus(200);
  const change = req.body.entry?.[0]?.changes?.[0]?.value;
  const message = change?.messages?.[0];
  if (!message) return;

  const fromE164 = '+' + message.from;
  const body = message.text?.body || `[${message.type}]`;

  try {
    let contact = await findContactByPhone(fromE164);
    if (!contact) contact = await createContact(fromE164);
    await createCommunication(contact.id, body, message.timestamp);
  } catch (err) {
    console.error('CRM sync failed', { wa_id: message.id, err });
  }
});

Le signal de réussite à observer dans HubSpot est l’apparition d’une nouvelle ligne dans la timeline du contact, marquée d’une icône WhatsApp si vous avez utilisé Communications, ou d’une note préfixée si vous êtes resté sur l’objet Note. Une absence d’apparition malgré un 201 dans les logs vient presque toujours d’une erreur de typographie sur l’associationTypeId ou d’un contact créé dans un autre portail (oubli de switcher de token entre dev et prod).

Étape 5 — Déclencher des templates WhatsApp depuis un workflow HubSpot

Le sens inverse, du CRM vers WhatsApp, se construit avec les Workflows HubSpot et une action de type Webhook. Imaginez un scénario d’épicerie en ligne où le passage d’une opportunité au stade « Commande prête » doit envoyer un message « Votre commande est prête à être retirée » via un template approuvé. Dans le builder de workflow, ajoutez un déclencheur basé sur la propriété dealstage, puis une action Webhook qui POST vers votre middleware avec, dans le body, l’id du contact et le code du template à envoyer.

Le middleware reçoit cet appel, lit le numéro WhatsApp depuis l’API HubSpot, puis appelle la Cloud API avec le payload de template. Cette indirection a deux avantages : elle évite d’exposer le token Meta dans HubSpot et elle vous laisse insérer une logique anti-spam (ne pas envoyer plus de N templates par jour à un même numéro, par exemple).

app.post('/send/template', async (req, res) => {
  const { contactId, templateName, languageCode } = req.body;
  const contact = await hubspot.crm.contacts.basicApi.getById(contactId, ['phone']);
  const to = contact.properties.phone.replace('+', '');

  await fetch(`https://graph.facebook.com/v22.0/${PHONE_NUMBER_ID}/messages`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${META_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      to,
      type: 'template',
      template: { name: templateName, language: { code: languageCode } }
    })
  });
  res.sendStatus(202);
});

Une réponse 200 de la Cloud API ne garantit pas la livraison ; elle confirme seulement l’acceptation du message dans la file. Le statut réel (sent, delivered, read, failed) arrive plus tard via le même webhook qu’à l’étape 4, dans entry[].changes[].value.statuses[]. Pensez à propager ces statuts en mettant à jour la Communication créée précédemment, c’est ce qui donne aux commerciaux la lecture « lu » à côté du message.

Étape 6 — Activer le module WhatsApp natif d’Odoo 17/18 OU créer un connecteur custom

Depuis la version 17, Odoo Enterprise embarque un module officiel nommé simplement whatsapp qui gère l’inscription d’un Phone Number ID, le stockage des templates et l’envoi de messages depuis presque toutes les vues (devis, factures, contacts). En Enterprise, l’activation se fait depuis Apps en cherchant « WhatsApp », puis en renseignant l’App ID, le Phone Number ID et le token Meta dans les paramètres. Une fois configuré, un bouton WhatsApp apparaît sur les fiches contacts.

En Community, ce module n’est pas disponible et vous devrez écrire un connecteur custom. Le plus simple est de créer un module qui hérite de res.partner pour ajouter un champ whatsapp_number, et qui définit un modèle whatsapp.message lié à res.partner via une relation Many2one. Le middleware externe reste maître de la communication avec Meta ; Odoo ne fait que stocker et afficher.

Étape 7 — Authentification OAuth2 ou API key Odoo

Odoo n’expose pas un OAuth2 standard de la même façon que HubSpot. L’API externe historique repose sur XML-RPC ou JSON-RPC avec un trio base de données + utilisateur + mot de passe (ou clé API à usage de mot de passe). Depuis Odoo 14, vous pouvez générer une clé API par utilisateur depuis Préférences puis Sécurité du compte ; cette clé remplace le mot de passe dans tous les appels XML-RPC, ce qui évite de stocker un mot de passe en clair côté middleware.

Le snippet ci-dessous montre une authentification XML-RPC en Python qui récupère l’uid nécessaire à toutes les opérations suivantes. C’est le ticket d’entrée pour appeler n’importe quel modèle Odoo.

import xmlrpc.client

URL = 'https://votre-instance.odoo.com'
DB = 'votre-db'
USER = 'integration@example.com'
API_KEY = 'a1b2c3d4e5f6...'

common = xmlrpc.client.ServerProxy(f'{URL}/xmlrpc/2/common')
uid = common.authenticate(DB, USER, API_KEY, {})
models = xmlrpc.client.ServerProxy(f'{URL}/xmlrpc/2/object')
print('Authenticated as uid', uid)

Si l’uid est False ou que authenticate renvoie une erreur, vérifiez le nom exact de la base (visible en bas à gauche de l’écran de login Odoo) et le login utilisé : Odoo est sensible à la casse sur l’email. Une fois uid obtenu, vous pouvez exécuter execute_kw sur n’importe quel modèle accessible à cet utilisateur. Réservez un utilisateur dédié à l’intégration plutôt que de réutiliser un compte humain, cela facilite l’audit.

Étape 8 — Mapper messages WhatsApp vers res.partner et mail.message

La logique de mapping ressemble à celle de HubSpot. Quand un message arrive, on cherche un res.partner dont le champ phone, mobile ou whatsapp_number correspond au numéro de l’expéditeur, on crée le partenaire si absent, puis on enregistré le message. Pour bénéficier de l’affichage natif dans le chatter d’Odoo, le mieux est de créer un mail.message attaché au partenaire avec un message_type à comment et un subtype_id dédié.

partner_ids = models.execute_kw(DB, uid, API_KEY,
    'res.partner', 'search',
    [[['phone', '=', '+221771234567']]], {'limit': 1})

if not partner_ids:
    partner_id = models.execute_kw(DB, uid, API_KEY,
        'res.partner', 'create',
        [{'name': 'Contact WhatsApp 771234567', 'phone': '+221771234567'}])
else:
    partner_id = partner_ids[0]

models.execute_kw(DB, uid, API_KEY,
    'mail.message', 'create',
    [{
        'model': 'res.partner',
        'res_id': partner_id,
        'body': '<p>Bonjour, paiement Mixx by Yas effectué.</p>',
        'message_type': 'comment',
        'author_id': partner_id
    }])

Le résultat visible : ouvrez la fiche du partenaire dans Odoo, descendez sur le chatter, et le message apparaît avec l’horodatage et l’auteur. Si rien ne s’affiche malgré une création réussie, c’est en général que model et res_id ne pointent pas sur le bon enregistrement, ou que l’utilisateur d’intégration n’a pas les droits sur mail.message (ajoutez-le au groupe Discuss pour débloquer).

Étape 9 — Envoyer un message WhatsApp depuis un devis Odoo

Pour un atelier de couture qui envoie ses bons de commande de tissus pudiques par WhatsApp, le flux idéal est d’ajouter un bouton « Envoyer par WhatsApp » sur le devis (sale.order). Sur Enterprise avec le module natif, ce bouton existe d’office et déclenche un assistant qui choisit un template. Sur Community, vous l’ajoutez via une action serveur ou un module custom qui appelle votre middleware.

Une astuce utile sur Community : créer une Automated Action déclenchée par le passage du devis au statut « Confirmé », qui appelle un webhook externe via le module base_automation en couplage avec un script Python serveur restreint. Le script construit un payload contenant le numéro client et l’URL du PDF du devis, puis le poste sur votre middleware. Côté middleware, vous recevez la requête, fabriquez le payload Cloud API avec un template approuvé et l’URL en variable, et envoyez. La séparation rend le flux testable indépendamment du frontal Odoo.

Étape 10 — Idempotence : éviter les doublons (message_id WhatsApp)

Meta peut livrer le même message plusieurs fois si votre webhook a tardé à répondre 200. Sans précaution, vous obtiendrez plusieurs Communications HubSpot ou plusieurs mail.message Odoo identiques sur la même fiche, ce qui pollue l’historique et fausse les statistiques. La parade est d’utiliser le champ id du message WhatsApp (un identifiant unique commençant par wamid.) comme clé d’idempotence.

La méthode la plus robuste est une table dédiée dans le middleware (PostgreSQL, Redis avec TTL de 7 jours, ou même un fichier SQLite) qui enregistré chaque wamid traité. Avant d’écrire dans le CRM, le middleware fait un INSERT ... ON CONFLICT DO NOTHING sur cette table : si l’insertion ne crée pas de ligne, le message est un doublon et on saute l’écriture CRM. Cette logique se complète d’un nettoyage périodique pour ne pas voir la table grossir indéfiniment.

CREATE TABLE wa_processed (
  wamid      TEXT PRIMARY KEY,
  processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Dans le handler webhook :
INSERT INTO wa_processed (wamid) VALUES ($1)
ON CONFLICT (wamid) DO NOTHING
RETURNING wamid;

Le test pour valider la mécanique : reproduisez deux fois la même requête POST sur votre webhook avec le même payload, puis vérifiez côté CRM qu’une seule entrée a été créée. Si vous voyez deux entrées, l’idempotence n’est pas en place ou la table de déduplication n’a pas été interrogée avant l’écriture CRM.

Étape 11 — Vérification : test bidirectionnel

Avant la mise en production, exécutez un scénario de bout en bout. Depuis un téléphone, envoyez un message texte au numéro WhatsApp Business. Vérifiez en moins de cinq secondes l’apparition d’une nouvelle entrée timeline dans HubSpot ou d’un message dans le chatter Odoo, sur la fiche du contact (créé à la volée si nécessaire). Inversement, depuis le CRM, déclenchez un envoi de template ou un message libre dans la fenêtre de 24h, et vérifiez la réception sur le téléphone du destinataire ainsi que la mise à jour du statut de livraison une fois le message lu.

Conservez ces deux tests dans une suite de smoke tests automatisés que vous rejouez après chaque déploiement du middleware. Une régression silencieuse sur l’écriture CRM est l’une des pannes les plus douloureuses : les messages continuent d’arriver dans WhatsApp, mais l’historique CRM se vide, et personne ne s’en aperçoit avant une revue commerciale.

Erreurs fréquentes

Symptôme Cause probable Correction
HubSpot 401 sur tous les appels Token expiré ou scopes insuffisants Régénérer le Private App Token, vérifier les périmètres
Doublons de messages dans la timeline Idempotence absente, Meta retransmet Mettre en place table wa_processed sur wamid
Odoo authenticate renvoie False Mauvais nom de DB ou casse de l’email Vérifier la DB en bas du login, respecter la casse
Communication HubSpot non visible associationTypeId erroné Utiliser 81 (contact ↔ communication)
Template envoyé hors fenêtre 24h refusé Pas de session client active Utiliser exclusivement des templates approuvés
Webhook timeout côté Meta Traitement CRM synchrone > 5s Répondre 200 immédiatement, traiter en async

Tutoriels frères

Pour aller plus loin

Une fois la liaison stable, deux chantiers prolongent naturellement l’intégration. Le premier consiste à enrichir automatiquement les contacts depuis les métadonnées de profil WhatsApp (nom affiché, photo) que Meta expose dans le payload contacts du webhook ; cela donne des fiches CRM moins anonymes. Le second est l’ajout des médias : images, documents, audios reçus, qu’il faut télécharger via l’endpoint /v22.0/{media_id} de Meta avant de les attacher à la Communication HubSpot ou à un ir.attachment Odoo, sous peine de perdre les pièces jointes lorsque l’URL temporaire expire.

FAQ

Peut-on connecter WhatsApp à Odoo Community sans Enterprise ?

Oui, en passant par un middleware externe qui appelle l’API XML-RPC d’Odoo. Le module whatsapp natif est réservé à Enterprise depuis la version 17, mais rien n’empêche de stocker les messages dans mail.message avec un connecteur custom.

Le Private App Token HubSpot expire-t-il ?

Non, il reste valide tant qu’un administrateur ne le révoque pas ou ne supprime pas l’application privée. C’est l’avantage par rapport à un OAuth2 dont le refresh token doit être géré, mais cela impose une rotation manuelle régulière pour limiter la surface d’attaque.

Faut-il un middleware ou peut-on appeler le CRM directement depuis le webhook Meta ?

Le middleware n’est pas obligatoire mais fortement recommandé. Sans lui, vous mélangez logique Meta et logique CRM dans un même handler, ce qui complique la maintenance et expose le token CRM à tous les retries de Meta. Une fonction serverless très simple suffit comme middleware initial.

Comment gérer la fenêtre des 24 heures dans le workflow CRM ?

Stockez sur le contact l’horodatage du dernier message entrant, exposé comme propriété personnalisée. Vos workflows peuvent alors conditionner l’envoi d’un message libre à un écart inférieur à 24h, et basculer automatiquement sur un template approuvé au-delà.

Le numéro WhatsApp suffit-il à dédoublonner les contacts ?

Dans la majorité des cas oui, à condition de normaliser au format E.164 (avec préfixe pays). Méfiez-vous cependant des numéros partagés en famille ou en boutique, qui peuvent générer des conflits de fiches. Pour les marchés où Free Money se nomme désormais Mixx by Yas, n’utilisez pas l’opérateur comme critère secondaire car il change.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité