Des contacts arrivaient à moitié vides dans Lemlist, notre outil d’outreach. J’ai construit deux workflows n8n qui les complètent avec Apollo : un en tâche de fond sur toute la base, un formulaire à la demande pour les commerciaux. Le plus long a été tout ce qui a cassé autour.
Le problème
Deux cas reviennent sans cesse :
- Cas 1. Un commercial ajoute un prospect depuis LinkedIn avec l’extension Chrome Lemlist. On a un nom et une URL LinkedIn, rien d’autre : ni email, ni téléphone, ni poste, ni société.
- Cas 2. Quelqu’un répond dans l’Inbox Lemlist : « il faut contacter untel@sasociete.org ». On a un email brut, sans nom ni poste.
Apollo était déjà connecté pour combler ce genre de trou.
Scanner les campagnes : une fausse piste
L’approche évidente (lister les leads des campagnes, repérer les champs vides, passer à Apollo) a cassé à chaque étape :
GET /campaigns/:id/export/leadsrenvoie juste la ligne d’en-têtes CSV : c’est le déclencheur d’un export asynchrone, pas un listing JSON.- Lesvrais endpoints existent, mais
GET /campaigns/{campaignId}/leads/ne renvoie que{ _id, contactId, state }. Il faut ensuite unGET /leads?id=par lead. Avec un rate limit de 20 requêtes par 2 secondes, ça ne passe pas à l’échelle. - Surtout, les trois contacts du cas 1 étaient introuvables dans les 32 campagnes du compte. Leur campagne d’origine avait été supprimée, mais Lemlist conserve la fiche Contact indépendamment. Un scan par campagne ne les aurait jamais trouvés.
Ce que j’ai construit
Lemlist distingue le Lead (un contact dans une campagne) et le Contact (la fiche centrale, unique). La bonne source, c’est la base Contacts : GET /contacts paginé (500 par page) pour lire, POST /contacts en upsert pour écrire. Cet upsert accepte email et téléphone, contrairement au PATCH des leads, limité à quelques champs standards.
Le workflow automatique tourne toutes les 15 minutes en heures ouvrées :
flowchart TD
A["Recuperation paginee\n/contacts (500 par page)"] --> B["Filtre incompletude\nemail, tel, poste ou societe manquant"]
B --> C["Filtre deja traite\nData Table n8n"]
C --> D["Enrichissement Apollo\nperson.enrich"]
D --> E["Upsert Lemlist\nPOST /contacts"]
E --> F["Journalisation"]
Impossible de marquer un contact « déjà traité » avec un champ personnalisé Lemlist : l’API refuse les champs non déclarés dans le référentiel de l’équipe. L’état vit donc dans une Data Table n8n (contactId, processedAt, outcome). Le workflow devient idempotent et ne rejoue jamais Apollo sur un échec.
Pour le cas 2, un formulaire n8n en deux pages : la première pour nom, email, LinkedIn, société ; la seconde affiche la liste des campagnes, générée en JSON par un nœud Code au moment de l’affichage. La liste est toujours à jour, sans liste figée ni sous-workflow de synchronisation.
Aucun champ n’est obligatoire individuellement. La vraie règle est « email OU LinkedIn OU (prénom ET nom) », ce qu’un champ requis ne sait pas exprimer : elle est vérifiée dans le code.
Le grand run
Avant d’écrire quoi que ce soit, j’ai lancé la partie lecture seule : 1059 contacts, dont 168 incomplets. Test d’abord sur les trois contacts du cas 1 : deux ont récupéré email, poste et localisation, le troisième seulement le poste (email « unavailable » chez Apollo).
Sur le reste, deux exécutions se sont arrêtées sur une erreur 429.
Rate limit mangé deux fois
Il a fallu trois réglages cumulés : un espacement entre appels (batchInterval), un retry sur le nœud HTTP (5 tentatives, 5 secondes), et onError: continueRegularOutput pour qu’un échec isolé n’arrête pas tout. Ensuite, les 168 contacts sont passés.
Ce qui a cassé
Une correction annoncée qui ne s’appliquait jamais
En éditant le formulaire par API, une modification partielle d’un paramètre imbriqué créait une clé parameters dupliquée à l’intérieur des paramètres existants. La relecture montrait la correction, mais le nœud lisait toujours l’ancienne valeur. Correctif : toujours remplacer l’objet complet. Le même piège est revenu sur la variable grammaticale.
- Des champs jamais repris.
companyNamen’était pas extrait d’Apollo (repli ajouté surapollo.organization.name). Et le formulaire ne reprenait pas le prénom et le nom trouvés par Apollo : un contact créé avec un email seul restait sans nom. - Le poste en anglais. Apollo renvoie le titre exact du profil LinkedIn, sans traduction. Ces personnes l’avaient écrit en anglais. Rien à corriger : je préfère rester fidèle à la source.
- Le sender manquant. Un lead créé par API arrive en « To launch », sans expéditeur. C’est le comportement normal de Lemlist : tout lead attend une revue manuelle avant d’être lancé, et l’expéditeur est assigné à ce moment.
À retenir
La logique d’enrichissement tient en trois nœuds. Le vrai travail a été de comprendre où vit la donnée (Contact plutôt que Lead) et de vérifier chaque hypothèse sur un cas réel avant le traitement en masse. Quand « ça a l’air corrigé », je relis la configuration réellement active avant de conclure.
Pour aller plus loin : Construire une machine à leads SDR et Détection IA de signaux d’affaires.