Intégration Zoho CRM
Extension payante. Chaque soumission devient un lead dans Zoho CRM : connexion OAuth au centre de données de votre compte, association champ par champ, condition d’envoi, dédoublonnage et journal.
Ce document décrit ce que le module garantit et ce qu’il refuse. Le guide utilisateur en donne la version courte, dans la section « Verser les réponses dans Zoho CRM ».
La connexion est au site, l’association est au formulaire
C’est le partage qui commande tout le module.
La connexion — un compte Zoho, une région, un couple de jetons — se règle une fois, dans SolisForms → Zoho CRM. Elle vaut pour le site entier.
L’association — quel champ nourrit quel champ du CRM, à quelle condition, avec quelle source — se règle par formulaire, dans le constructeur.
Ni l’un ni l’autre n’est arbitraire. Placer l’association au niveau du site aurait obligé tous les formulaires à porter les mêmes champs : un formulaire de contact et un formulaire de devis n’ont pas les mêmes. Placer la connexion au niveau du formulaire aurait multiplié les endroits où un jeton de rafraîchissement peut fuir, par le nombre de formulaires.
Pourquoi les leads, et rien d’autre
Un formulaire public produit des pistes : quelqu’un se signale. Le module Lead de Zoho existe exactement pour cela, et sa conversion en Contact, Compte et Affaire est un geste commercial qui appartient au CRM.
Écrire directement dans Contact aurait demandé de savoir si la personne existe déjà, de décider quoi écraser, et de donner à un site WordPress public le droit de modifier le répertoire client. C’est une décision d’entreprise, pas de formulaire.
Le flux va donc dans un seul sens : du site vers le CRM. Le module ne lit rien pour préremplir et ne reçoit rien de Zoho.
Le centre de données n’est pas un détail
Zoho n’est pas un service : c’est huit services qui portent le même nom. Un
compte créé en Europe vit sur zoho.eu et n’existe pas sur zoho.com — les
jetons n’y valent rien, les enregistrements n’y sont pas, et l’API répond
« invalid token » sans jamais dire que le problème est géographique.
C’est la première chose qui se passe mal quand on branche Zoho, et la dernière à laquelle on pense. Le choix est donc demandé à l’écran, avant tout le reste.
Les huit couples d’hôtes sont écrits en toutes lettres dans le code. La
tentation était d’en composer l’adresse — 'accounts.zoho.' . $region —, ce qui
aurait donné à un champ de réglage le pouvoir de choisir l’hôte auquel le serveur
envoie le secret client et demande des jetons. Une région inconnue ne donne donc
pas une valeur approchante : elle ne donne rien.
Zoho renvoie lui-même le domaine d’API à chaque échange de jetons, et peut déplacer un compte d’un centre à l’autre. Ce déplacement est suivi — mais seulement vers un hôte de la liste.
Les jetons sont chiffrés, et voici ce que cela protège
Un jeton de rafraîchissement Zoho ne périme pas. Qui l’obtient peut, aussi longtemps qu’il n’est pas révoqué, émettre des jetons d’accès et lire ou écrire dans le CRM — depuis n’importe où, sans passer par le site. Ce n’est pas un identifiant de session : c’est une clé de la maison.
Il est donc chiffré au repos, en AES-256-GCM, avec une clé dérivée des sels du site.
Ce que cela protège : la divulgation de la seule base de données. Une injection SQL, une sauvegarde égarée, un export confié à un prestataire, un hébergement mutualisé où les bases se touchent. C’est le cas le plus fréquent, et de loin.
Ce que cela ne protège pas : la lecture du système de fichiers. La clé dérive
de wp-config.php ; qui lit ce fichier lit aussi la base. Le dire est plus utile
que de laisser croire à une protection absolue — et le chiffrement garde tout son
sens pour autant, les deux fuites n’étant pas le même évènement.
Trois conséquences pratiques :
- Une rotation des sels déconnecte. Les jetons deviennent illisibles, et la connexion est à refaire. C’est voulu : on fait tourner ses sels après une compromission, et c’est précisément là qu’il faut cesser d’employer des jetons qui ont pu fuir.
- Sans
openssl, rien n’est stocké. La connexion est refusée, avec le motif. Une dégradation silencieuse en matière de secret est pire que l’absence de la fonctionnalité. - GCM, pas CBC. Une valeur altérée en base ne se déchiffre pas en une autre valeur : le déchiffrement échoue.
Mots de passe d’application, portées minimales
Le module demande quatre portées, et pas une de plus :
| Portée | Pour |
|---|---|
ZohoCRM.modules.leads.CREATE | poser une fiche |
ZohoCRM.modules.leads.UPDATE | mettre à jour celle d’un contact déjà connu |
ZohoCRM.modules.leads.READ | laisser Zoho dédoublonner |
ZohoCRM.settings.fields.READ | proposer le tableau d’association |
Il aurait été plus simple de demander ZohoCRM.modules.ALL. Ce jeton aurait
permis de lire tout le CRM — contacts, affaires, comptes — depuis un site
WordPress public. Une portée excédentaire ne se remarque jamais : elle ne change
rien tant que rien ne tourne mal, et change tout le jour où quelque chose tourne
mal.
Une soumission, une fiche
Trois mécanismes se superposent, et chacun couvre ce que les autres laissent passer.
La clé unique sur la soumission. Une ligne par soumission dans
slf_zoho_leads : la base refuse la seconde.
La prise de ligne. UPDATE … WHERE state <> 'sent' : celui dont la requête
modifie la ligne envoie, les autres se retirent. Ce n’est pas une optimisation —
deux tâches planifiées pour la même soumission se chevauchent dès qu’un envoi
dépasse l’intervalle de reprise, c’est-à-dire précisément quand le CRM est lent.
Le contrôle ne peut pas se faire en PHP : lire l’état puis l’écrire laisse entre
les deux l’intervalle où l’autre processus lit la même valeur.
L’upsert. Reste un cas qu’aucun garde local ne couvre : la requête aboutit
chez Zoho, et la réponse se perd en route. Nous ne savons alors pas si la fiche
existe. POST /Leads/upsert avec dédoublonnage sur l’adresse électronique crée
la fiche si elle n’existe pas et la met à jour sinon : le rejeu devient une mise
à jour, pas une seconde fiche.
C’est aussi pourquoi le dédoublonnage est allumé par défaut, et pourquoi l’écran dit ce qu’on perd en l’éteignant.
Un bail, pour que rien ne reste coincé
Une ligne passée à « envoi en cours » dont le processus meurt — erreur fatale, mémoire épuisée, serveur redémarré — resterait dans cet état pour toujours, et la soumission ne partirait jamais. Au-delà de dix minutes, la ligne est reprise : perdre dix minutes vaut mieux que perdre la soumission.
Ce qui ne sort pas
- Les champs non associés. L’envoi est un tableau de correspondances : ce qui n’y figure pas n’a nulle part où aller.
- Les mots de passe, paiements, fichiers et signatures, même associés de force dans un réglage importé. La mise en forme les écarte en amont, pour toutes les sorties du plugin à la fois. Une adresse de téléversement est, de surcroît, un accès permanent au fichier : la coller dans une fiche de CRM la recopierait chez un tiers hors de toute expiration.
- Les valeurs vides. Zoho distingue « champ absent » de « champ vide », et un upsert qui envoie un champ vide efface la valeur existante. Sans ce filtrage, une seconde soumission sans numéro de téléphone supprimerait celui que la première avait posé.
Les échecs, et lesquels se rejouent
Zoho répond 202 Multi-Status quand une partie d’un lot a échoué — y compris
pour un lot d’un seul élément. Le sort de la fiche est donc dans le corps, jamais
dans le code HTTP. S’y fier aurait compté comme envoyées des fiches que Zoho a
refusées.
| Motif | Rejoué | Pourquoi |
|---|---|---|
MANDATORY_NOT_FOUND, INVALID_DATA, DUPLICATE_DATA | non | le même appel obtiendra le même refus |
OAUTH_SCOPE_MISMATCH, NOT_ALLOWED | non | il manque un droit, pas une occasion |
429, 5xx, réseau coupé | oui | 1, 5 puis 30 minutes |
401 | une fois, tout de suite | le jeton est renouvelé, puis l’appel rejoué |
invalid_client, invalid_grant | non | l’autorisation est perdue, l’écran le dit |
Last_Name est vérifié avant l’appel : Zoho refuse toute fiche sans lui, avec
un message qui ne nomme pas le champ manquant. Le motif écrit dans le journal est
le seul qui soit lisible par qui doit le corriger.
Renvoyer
Trois tentatives échouées laissent une soumission au sol. La cause est souvent extérieure et corrigeable : une portée oubliée, un champ obligatoire non associé, un CRM en maintenance. Le détail de la soumission porte donc un bouton de renvoi.
Il ne duplique pas : l’envoi se fait par upsert, et un second passage met à jour la même fiche — tant que l’adresse électronique est associée.
Déconnecter
Le bouton Déconnecter révoque le jeton chez Zoho avant d’effacer ici. Effacer sans révoquer ne retire rien à l’autorisation : le jeton resterait valable, et une sauvegarde de la base restituerait un accès en état de marche.
Si Zoho est injoignable, la déconnexion locale se fait quand même — un site qui ne peut plus joindre Zoho doit pouvoir se débarrasser de ses jetons — et l’écran dit que l’autorisation reste à retirer à la main depuis le compte Zoho.
L’effacement est total : jetons, secret, identifiant, région. Déconnecter ne garde pas « les réglages, au cas où » ; le secret client est un secret, et qui déconnecte veut qu’il parte.
Diagnostic
L’écran SolisForms → Zoho CRM porte le décompte des envois par état et les derniers échecs avec leur motif. Un module d’envoi sans écran de diagnostic se juge aux soumissions qui manquent dans le CRM — c’est-à-dire trop tard, et par quelqu’un d’autre.
Le détail de chaque soumission porte le même état, et un lien vers la fiche dans l’interface de Zoho.
Schéma
Table slf_zoho_leads : une ligne par soumission à envoyer, avec son état, son
identifiant de fiche, le nombre de tentatives et le motif du dernier échec.
Aucune valeur de soumission n’y est recopiée.
La connexion, elle, vit dans une option — il n’y en a qu’une par site, et une table d’une ligne aurait coûté une migration pour n’apporter aucune requête.
Ce que la V1 ne fait pas
Ni Contact, ni Affaire, ni Compte, ni Blueprint, ni synchronisation inverse, ni lecture du CRM. Le plan le bornait ainsi, et chacune de ces extensions demande une décision qui n’est pas technique : qui a le droit d’écraser quoi dans le fichier client.
