29 webhooks, signés et filtrables.
Chaque événement coché part vers votre adresse dès qu’il arrive, signé pour que vous sachiez qu’il vient de FantomHunt, et relancé si votre serveur ne répond pas.
Webhooks
FantomHunt envoie un POST JSON à votre URL à chaque événement coché. Jusqu’à 10 webhooks par compte, chacun avec ses événements (29, rangés par famille) et ses filtres, dans Réglages → API & Intégrations.
La livraison
- 1Votre URL doit être en https, sur le port 443, et pointer vers une adresse publique. Elle est revérifiée à chaque envoi ; les redirections ne sont jamais suivies.
- 2Répondez un code 2xx en moins de 10 secondes, puis traitez l’événement de votre côté. Tout autre code, une redirection ou un délai dépassé compte comme un échec.
- 3Un échec est relancé en recul progressif : 30 secondes, 2 minutes, 10 minutes, 30 minutes puis 2 heures. Après la dernière relance, la livraison est en échec définitif.
- 4Après trois échecs définitifs d’affilée, le webhook passe en pause : les livraisons en attente sont abandonnées, vous êtes prévenu par email et Réglages l’affiche en rouge. Réactivez-le quand votre URL répond de nouveau.
- 5Réglages montre les 50 dernières livraisons de chaque webhook (statut, code HTTP, erreur) ; le journal est conservé 14 jours.
Un événement fait dans l’application part en quelques secondes ; un changement fait par une campagne ou par l’extension part en moins d’une minute. L’ordre d’arrivée n’est pas garanti, et une livraison peut exceptionnellement arriver deux fois (si notre serveur redémarre pendant l’envoi) : traitez vos événements de façon idempotente, par exemple en vous appuyant sur l’identifiant de la fiche et sur cree_le.
Les événements
Prospects
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| prospect.ajoute | Une fiche entre, quelle que soit la source : extraction, import, ajout par son lien LinkedIn (une fois la fiche lue), nouveau contact WhatsApp ou email. Un contact déjà rattaché à un prospect LinkedIn n’en est pas un : sa nouvelle coordonnée passe par coordonnee.trouvee. | canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
| prospect.extrait | Un prospect entre par une extraction ou un import de l’extension. | prospect |
| statut.change | Le statut d’une fiche change réellement, quel que soit le chemin : à la main, par une campagne, par l’extension ou par une action groupée. | statut, statut_precedent, canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
| coordonnee.trouvee | Un email ou un téléphone est trouvé pour un prospect. | type (email ou telephone), valeur, prospect |
| poste.change | Un changement de poste est détecté sur un prospect suivi. | changement (type : promotion ou nouveau_poste, ancien_poste, ancienne_entreprise, nouveau_poste, nouvelle_entreprise, detecte_le), prospect |
Échanges
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| message.envoye | Un message part : LinkedIn quand l’extension l’a réellement envoyé, WhatsApp et email à l’envoi. Un historique resynchronisé ne déclenche rien. | message (texte, objet pour un email, envoye_le), canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
| invitation.envoyee | L’extension a envoyé l’invitation LinkedIn. | invitation (note, envoyee_le), prospect |
| invitation.acceptee | Le prospect accepte l’invitation LinkedIn. | prospect |
| reponse.recue | Une vraie réponse arrive. Les réponses automatiques (absence, congés) sont filtrées, ainsi que les emails dont l’expéditeur n’a pas pu être authentifié. | canal, et prospect (LinkedIn) ou contact (WhatsApp, email) ; plus message (texte, objet, recu_le) si le webhook joint le texte des réponses |
| reponse_ia.a_valider | L’IA propose une réponse LinkedIn qui attend votre validation. | suggestion (texte, campagne ou null), prospect |
Rendez-vous
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| rdv.pris | Un rendez-vous est posé sur une fiche qui n’en avait pas à venir. | rdv_le, canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
| rdv.deplace | Un rendez-vous à venir change de date. | rdv_le, ancien_rdv_le, canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
| rdv.debriefe | Le débrief du rendez-vous est enregistré. | rdv_le, canal, et prospect (LinkedIn) ou contact (WhatsApp, email) |
Campagnes et recherches
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| campagne.prospect_inscrit | Un prospect entre dans une campagne. | campagne (id, nom), prospect |
| campagne.prospect_sorti | Un prospect quitte une campagne. | campagne (id, nom), raison (termine, a_repondu, ou la raison de l’arrêt), prospect |
| recherche.terminee | Une recherche de prospects se termine. | recherche (id, cible, pays, volume, statut, prospects_trouves, secteurs, offre_id, dates) |
| recherche.echouee | Une recherche s’arrête sur une erreur. | recherche, avec erreur |
Tâches et affaires
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| tache.creee | Une tâche est créée. | tache (id, titre, echeance_le, faite_le, en_retard, canal, personne et ses identifiants) |
| tache.echue | L’échéance d’une tâche non faite est passée. Une seule fois par tâche, dans les minutes qui suivent. | tache |
| tache.faite | Une tâche est marquée faite. | tache |
| affaire.creee | Une affaire est créée. | affaire (id, nom, statut, montant_centimes, dates, colonne, personnes) |
| affaire.gagnee | Une affaire passe en gagnée. | affaire |
| affaire.perdue | Une affaire passe en perdue. | affaire, avec motif_de_perte |
Compte
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| canal.deconnecte | LinkedIn se déconnecte, ou une boîte email est à reconnecter. | canal (linkedin ou email), etat, etat_precedent ; raison (LinkedIn) ou adresse (email) |
| canal.reconnecte | Le canal refonctionne. | canal, etat, etat_precedent ; adresse pour un email |
| prospection.en_pause | Vous mettez la prospection en pause. | en_pause_depuis |
| prospection.reprise | La prospection reprend. | etait_en_pause_depuis |
| linkedin.freinee | FantomHunt ralentit LinkedIn après plusieurs échecs d’affilée, pour protéger votre compte. | jusqua, refus_d_affilee, raison |
| credits.bas | Vos crédits IA passent sous 10 % de votre forfait mensuel. | credits_ia (restants, par_mois) |
Test
| Événement | Quand il part | Ce que contient donnees |
|---|---|---|
| test.ping | Le bouton « Envoyer un test » de Réglages. | message (un texte de bienvenue, pas de fiche) |
Filtrer : « Seulement pour »
Chaque webhook peut se limiter à un ou plusieurs canaux (LinkedIn, WhatsApp, email), à une campagne, à une offre ou à une étiquette. Vide, un filtre laisse tout passer. Un filtre ne trie que les événements qui portent sa donnée : le canal sur tout événement qui en a un (une recherche compte pour LinkedIn), la campagne, l’offre et l’étiquette sur les événements qui concernent une personne, l’offre aussi sur les recherches. Les autres (crédits bas, affaire créée…) partent toujours, puisque vous les avez cochés. La campagne d’un contact WhatsApp ou email est celle de sa fiche LinkedIn liée. Un webhook se modifie (événements, filtres, texte des réponses) sans changer d’adresse ni de secret.
Le corps d’une livraison
Toujours evenement, cree_le (le moment de l’événement) et donnees. La fiche est la même que celle de l’API, figée au moment de l’envoi, à une différence près : les étiquettes y sont des noms. Une réponse reçue sur LinkedIn :
{
"evenement": "reponse.recue",
"cree_le": "2026-08-26T15:04:05.000Z",
"donnees": {
"canal": "linkedin",
"prospect": {
"id": "6f2b…", "nom": "Martin Dupont", "entreprise": "Acme",
"poste": "Directeur commercial", "url_linkedin": "https://www.linkedin.com/in/…",
"localisation": "Lyon", "email": "martin@acme.fr", "email_score": 95,
"telephone": "+33 6 12 34 56 78", "statut": "replied", "score": 82,
"source": "extension", "offre": { "id": "9c1d…", "nom": "Paie externalisée" },
"recherche_id": "4a7e…", "etiquettes": ["Chaud"],
"campagnes": [{ "id": "b3f0…", "nom": "Relance DAF" }],
"cree_le": "2026-08-20T09:12:00.000Z", "contacte_le": "2026-08-21T10:00:00.000Z",
"connecte_le": "2026-08-22T08:30:00.000Z", "repondu_le": "2026-08-26T15:04:05.000Z",
"rdv_le": null
}
}
}Un statut changé sur un contact WhatsApp :
{
"evenement": "statut.change",
"cree_le": "2026-09-24T10:12:44.000Z",
"donnees": {
"canal": "whatsapp", "statut": "rdv", "statut_precedent": "actif",
"contact": {
"id": "8a1c…", "nom": "Julie Leroy", "telephone": "+33 6 11 22 33 44",
"prospect_id": "6f2b…", "statut": "rdv",
"offre": { "id": "9c1d…", "nom": "Paie externalisée" },
"repondu_le": "2026-09-23T17:40:00.000Z", "rdv_le": "2026-09-26T09:00:00.000Z",
"derniere_activite_le": "2026-09-24T10:12:00.000Z", "cree_le": "2026-09-20T08:00:00.000Z",
"etiquettes": ["Chaud"], "campagnes": [{ "id": "b3f0…", "nom": "Relance DAF" }]
}
}
}Sur un webhook où la case « Joindre le texte des réponses reçues » est cochée (décochée par défaut), reponse.recue porte en plus le message : "message": { "objet": "Re: votre offre", "texte": "Oui, appelons-nous jeudi.", "recu_le": "…" } (objet pour un email seulement, texte coupé à 5 000 caractères). Chaque webhook reçoit sa propre version : cocher la case sur l’un ne change rien aux autres.
En-têtes de chaque livraison : Content-Type: application/json, User-Agent: FantomHunt-Webhooks/1.0 et X-Fantomhunt-Signature.
Vérifier la signature
Chaque livraison porte l'en-tête X-Fantomhunt-Signature: t=<horodatage>,v1=<signature>. Recalculez le HMAC-SHA256 de `${t}.${corps}` avec le secret affiché dans Réglages : s'il diffère, ou si l'horodatage est trop vieux, rejetez la livraison. Zapier et Make peuvent l'ignorer (leurs URL sont secrètes) ; un serveur à vous devrait toujours vérifier.
const crypto = require('crypto');
function verifier(enteteSignature, corpsBrut, secret) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(enteteSignature || '');
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // anti-rejeu : 5 min
const attendu = crypto.createHmac('sha256', secret)
.update(t + '.' + corpsBrut).digest('hex');
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(attendu));
}