Aller au contenu principal
Guides techniques

Construire un pipeline multi-signaux avec l'API Rodz

Peter Cools · · 22 min de lecture

En résumé : Un pipeline multi-signaux agrège plusieurs types de signaux d’affaires (levées de fonds, recrutements, changements de poste, publications LinkedIn, etc.) dans un seul flux de traitement. Ce guide vous accompagne étape par étape : configuration des signaux via l’API, réception centralisée par webhook, normalisation, enrichissement automatique, scoring Balance et routage vers vos canaux d’action (CRM, Slack, séquences email). Avec un exemple complet en Node.js.

Qu’est-ce qu’un pipeline multi-signaux ?

Un pipeline multi-signaux est une architecture de traitement de données qui combine plusieurs types de signaux d’affaires en un flux unifié. Au lieu de traiter chaque signal de manière isolée (une alerte pour les levées de fonds ici, une notification de recrutement là), vous centralisez l’ensemble dans un seul système capable d’ingérer, normaliser, enrichir, scorer et router chaque signal vers l’action commerciale appropriée.

L’idée est simple. Chaque type de signal, pris individuellement, raconte une partie de l’histoire d’un prospect. Une levée de fonds vous dit qu’une entreprise a du budget. Une offre d’emploi sur un poste de commercial vous dit qu’elle se structure pour vendre. Un changement de poste d’un décideur vous dit que la fenêtre de décision est ouverte. Pris ensemble, ces signaux racontent une histoire complète. Et c’est cette histoire complète qui vous permet de prioriser vos efforts commerciaux avec précision.

Le problème, c’est que sans architecture adaptée, ces signaux arrivent dans des formats différents, à des moments différents, avec des niveaux de détail variables. Vous finissez par jongler entre des onglets, des tableurs et des notifications dispersées. Le pipeline multi-signaux résout ce problème en imposant un traitement standardisé à tous les signaux, quel que soit leur type ou leur source.

Ce guide s’adresse aux développeurs et aux équipes ops qui ont déjà une connaissance de base de l’API Rodz. Si vous débutez, commencez par le guide de démarrage et d’authentification. Si vous n’avez jamais configuré de webhook, lisez d’abord le guide de configuration des webhooks.

Prérequis

Avant de vous lancer dans la construction de votre pipeline, assurez-vous de disposer des éléments suivants :

  1. Un compte Rodz actif avec accès API complet. Votre plan doit inclure les endpoints signaux, enrichissement et webhooks. Vérifiez votre plan sur app.rodz.io.
  2. Votre clé API Rodz. Récupérez-la depuis les paramètres de votre compte. Le guide de démarrage explique la procédure complète.
  3. Un serveur HTTPS accessible publiquement pour recevoir les webhooks. Les URLs en HTTP simple ne sont pas acceptées.
  4. Node.js 18 ou supérieur installé sur votre serveur. Les exemples de code de cet article utilisent Node.js avec Express.
  5. Un CRM configuré (HubSpot, Pipedrive ou équivalent) si vous souhaitez router les signaux vers votre CRM. Le guide de connexion HubSpot couvre cette intégration en détail.
  6. La référence API à portée de main. Consultez la documentation complète des endpoints pour les détails techniques de chaque appel.
  7. Notions de base en architecture événementielle. Vous devez comprendre le principe des files d’attente, du traitement asynchrone et de l’idempotence.

Architecture générale du pipeline

Avant de plonger dans le code, posons l’architecture. Un pipeline multi-signaux se décompose en six étapes séquentielles. Chaque étape a une responsabilité unique, ce qui facilite le débogage, la maintenance et l’évolution du système.

Le flux de données en six étapes

Voici le parcours complet d’un signal, de sa détection à l’action commerciale :

Signal détecté par Rodz

[1. INGESTION] → Webhook reçoit le payload brut

[2. NORMALISATION] → Transformation en modèle unifié

[3. ENRICHISSEMENT] → Appels aux endpoints /enrich/*

[4. SCORING] → Application du modèle Balance

[5. ROUTAGE] → Règles de distribution par score et type

[6. ACTION] → CRM, Slack, séquence email, tâche manuelle

Chaque étape est indépendante. Si l’enrichissement échoue pour un signal, le pipeline ne s’arrête pas. Le signal est marqué comme « non enrichi » et passe à l’étape de scoring avec les données disponibles. Cette résilience est essentielle quand vous traitez des dizaines ou des centaines de signaux par jour.

Principes de conception

Trois principes guident cette architecture :

Idempotence. Un même signal traité deux fois ne doit pas produire de doublon dans votre CRM ou déclencher deux notifications Slack. Chaque signal possède un identifiant unique (signal_id) que vous utilisez pour vérifier s’il a déjà été traité.

Traitement asynchrone. Votre endpoint webhook doit répondre avec un code 200 en moins de 5 secondes. Tout le traitement (enrichissement, scoring, routage) se fait après la réponse, dans un processus séparé. En production, utilisez une file d’attente (Redis, RabbitMQ, SQS) entre l’ingestion et le traitement.

Observabilité. Chaque étape du pipeline doit émettre des métriques et des logs structurés. Quand un signal arrive mais ne génère aucune action, vous devez pouvoir retracer son parcours pour comprendre pourquoi.

Étape 1 : Configurer les types de signaux via l’API

La première étape consiste à activer les types de signaux que vous souhaitez recevoir. L’API Rodz propose plusieurs familles de signaux, chacune documentée dans son propre guide. Pour un pipeline multi-signaux complet, vous allez en combiner plusieurs.

Voici un exemple qui configure quatre types de signaux simultanément :

# Signal financier : levées de fonds
curl -X POST https://api.rodz.io/v1/signals/configure \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "signal_type": "fundraising",
    "filters": {
      "min_amount": 1000000,
      "countries": ["FR", "BE", "CH"],
      "industries": ["saas", "fintech", "cybersecurity"]
    },
    "enabled": true
  }'

# Signal RH : offres d'emploi
curl -X POST https://api.rodz.io/v1/signals/configure \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "signal_type": "job_posting",
    "filters": {
      "job_titles": ["Head of Sales", "VP Sales", "Directeur Commercial"],
      "countries": ["FR"]
    },
    "enabled": true
  }'

# Signal de changement de poste
curl -X POST https://api.rodz.io/v1/signals/configure \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "signal_type": "job_change",
    "filters": {
      "seniority_levels": ["c-level", "vp", "director"],
      "countries": ["FR", "BE"]
    },
    "enabled": true
  }'

# Signal de contenu social : publications LinkedIn
curl -X POST https://api.rodz.io/v1/signals/configure \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "signal_type": "social_content",
    "filters": {
      "keywords": ["recrutement", "croissance", "levée de fonds", "nouveau bureau"],
      "min_engagement": 50
    },
    "enabled": true
  }'

Chaque type de signal est documenté en détail dans les articles de la série API. Consultez notamment le guide des signaux financiers et le guide des signaux RH pour ajuster vos filtres.

L’important ici est de configurer chaque type de signal avec des filtres suffisamment précis pour éviter le bruit. Un pipeline qui reçoit 500 signaux par jour dont 400 sont non pertinents n’est pas un bon pipeline. Commencez avec des filtres serrés et élargissez progressivement.

Étape 2 : Centraliser la réception avec un seul webhook

Plutôt que de créer un webhook par type de signal, configurez un seul endpoint qui reçoit tout. Cela simplifie votre infrastructure et vous permet de traiter les corrélations entre signaux (par exemple, une entreprise qui recrute ET qui vient de lever des fonds).

Enregistrez votre webhook via l’API :

curl -X POST https://api.rodz.io/v1/webhooks \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://votre-serveur.com/webhooks/rodz",
    "events": [
      "signal.fundraising",
      "signal.job_posting",
      "signal.job_change",
      "signal.social_content"
    ],
    "secret": "votre_secret_hmac_256"
  }'

Le champ events liste tous les types de signaux que votre webhook doit recevoir. Le champ secret est utilisé pour la vérification HMAC des payloads entrants. Le guide des webhooks détaille le mécanisme de vérification de signature.

Voici le squelette de votre endpoint de réception :

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

const WEBHOOK_SECRET = process.env.RODZ_WEBHOOK_SECRET;

function verifySignature(req) {
  const signature = req.headers['x-rodz-signature'];
  const hash = crypto.createHmac('sha256', WEBHOOK_SECRET).update(JSON.stringify(req.body)).digest('hex');
  return signature === `sha256=${hash}`;
}

app.post('/webhooks/rodz', async (req, res) => {
  // Vérifier la signature HMAC
  if (!verifySignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Répondre immédiatement pour respecter le timeout
  res.status(200).json({ received: true });

  // Traitement asynchrone du signal
  processSignal(req.body).catch((err) => {
    console.error('Erreur de traitement :', err);
  });
});

app.listen(3000, () => {
  console.log('Pipeline multi-signaux actif sur le port 3000');
});

Le point important : la réponse 200 est envoyée avant le traitement. Si votre endpoint met trop de temps à répondre, Rodz considère la livraison comme échouée et relance la requête. Cela peut créer des doublons si vous ne gérez pas l’idempotence correctement.

Étape 3 : Normaliser les signaux dans un modèle unifié

Chaque type de signal arrive avec sa propre structure de données. Un signal de levée de fonds contient un montant et un type de tour. Un signal de recrutement contient un intitulé de poste et une localisation. Un changement de poste contient l’ancien et le nouveau rôle. Pour les traiter de manière uniforme en aval, vous devez les normaliser.

Voici un modèle de données unifié :

/**
 * Modèle unifié pour tous les signaux du pipeline
 */
const normalizedSignal = {
  // Identité du signal
  id: '', // signal_id original de Rodz
  type: '', // fundraising, job_posting, job_change, social_content
  timestamp: '', // ISO 8601
  receivedAt: '', // moment de réception dans votre pipeline

  // Entreprise concernée
  company: {
    rodzId: '', // identifiant Rodz de l'entreprise
    name: '',
    domain: '',
    siren: '',
    industry: '',
    country: '',
    size: '',
  },

  // Données spécifiques au signal (normalisées)
  details: {
    headline: '', // résumé lisible du signal
    rawData: {}, // payload original pour référence
  },

  // Champs ajoutés par le pipeline
  enrichment: null, // rempli à l'étape 3
  score: null, // rempli à l'étape 4
  routed: false, // mis à jour à l'étape 5
  actions: [], // liste des actions déclenchées
};

Et voici la fonction de normalisation :

function normalizeSignal(payload) {
  const base = {
    id: payload.signal_id,
    type: payload.signal_type,
    timestamp: payload.timestamp,
    receivedAt: new Date().toISOString(),
    company: {
      rodzId: payload.company.rodz_id,
      name: payload.company.name,
      domain: payload.company.domain || '',
      siren: payload.company.siren || '',
      industry: payload.company.industry || '',
      country: payload.company.country || '',
      size: payload.company.employee_count || '',
    },
    details: {
      headline: buildHeadline(payload),
      rawData: payload,
    },
    enrichment: null,
    score: null,
    routed: false,
    actions: [],
  };

  return base;
}

function buildHeadline(payload) {
  switch (payload.signal_type) {
    case 'fundraising':
      return `${payload.company.name} a levé ${payload.data.amount}€ (${payload.data.round_type})`;
    case 'job_posting':
      return `${payload.company.name} recrute un(e) ${payload.data.job_title}`;
    case 'job_change':
      return `${payload.data.person_name} est devenu(e) ${payload.data.new_title} chez ${payload.company.name}`;
    case 'social_content':
      return `${payload.company.name} a publié sur LinkedIn (${payload.data.engagement_count} interactions)`;
    default:
      return `Signal ${payload.signal_type} pour ${payload.company.name}`;
  }
}

Cette normalisation vous garantit que toutes les étapes suivantes (enrichissement, scoring, routage) travaillent avec le même format, quel que soit le type de signal en entrée. Le champ rawData conserve le payload original si vous avez besoin d’accéder à des données spécifiques plus tard.

Étape 4 : Enrichir automatiquement les signaux

Une fois le signal normalisé, vous pouvez appeler les endpoints d’enrichissement de l’API Rodz pour compléter les données. L’enrichissement se fait en deux temps : l’enrichissement de l’entreprise (données firmographiques) et l’enrichissement du contact (trouver le bon interlocuteur).

const axios = require('axios');

const API_BASE = 'https://api.rodz.io/v1';
const API_KEY = process.env.RODZ_API_KEY;

const apiClient = axios.create({
  baseURL: API_BASE,
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    'Content-Type': 'application/json',
  },
});

async function enrichSignal(signal) {
  const enrichment = { company: null, contacts: [] };

  // 1. Enrichissement firmographique de l'entreprise
  try {
    const companyRes = await apiClient.post('/enrich/company', {
      domain: signal.company.domain,
      siren: signal.company.siren,
    });
    enrichment.company = companyRes.data;

    // Mettre à jour les données de l'entreprise avec les infos enrichies
    signal.company.industry = companyRes.data.industry || signal.company.industry;
    signal.company.size = companyRes.data.employee_count || signal.company.size;
  } catch (err) {
    console.warn(`Enrichissement entreprise échoué pour ${signal.company.name}:`, err.message);
  }

  // 2. Trouver le décideur pertinent
  try {
    const contactRes = await apiClient.post('/enrich/contact', {
      company_domain: signal.company.domain,
      job_titles: getTargetTitles(signal.type),
    });

    if (contactRes.data.contacts && contactRes.data.contacts.length > 0) {
      enrichment.contacts = contactRes.data.contacts;

      // 3. Trouver l'email du premier contact
      const primaryContact = contactRes.data.contacts[0];
      try {
        const emailRes = await apiClient.post('/enrich/find-email', {
          first_name: primaryContact.first_name,
          last_name: primaryContact.last_name,
          company_domain: signal.company.domain,
        });
        enrichment.contacts[0].email = emailRes.data.email;
        enrichment.contacts[0].email_verified = emailRes.data.verified;
      } catch (err) {
        console.warn(`Recherche email échouée pour ${primaryContact.first_name} ${primaryContact.last_name}`);
      }
    }
  } catch (err) {
    console.warn(`Enrichissement contact échoué pour ${signal.company.domain}:`, err.message);
  }

  signal.enrichment = enrichment;
  return signal;
}

function getTargetTitles(signalType) {
  const titleMap = {
    fundraising: ['CEO', 'CTO', 'VP Sales', 'Head of Growth'],
    job_posting: ['VP Sales', 'Head of Sales', 'Directeur Commercial'],
    job_change: ['CEO', 'COO', 'VP Sales'],
    social_content: ['CEO', 'CMO', 'Head of Marketing'],
  };
  return titleMap[signalType] || ['CEO', 'COO'];
}

Pour les détails techniques des endpoints d’enrichissement, consultez le guide d’enrichissement de contacts et le guide d’enrichissement d’entreprises.

Quelques points d’attention sur l’enrichissement dans un pipeline :

  • Respectez les limites de débit. L’API Rodz applique un rate limiting sur les endpoints d’enrichissement. Si vous recevez un code 429, implémentez un mécanisme de retry avec backoff exponentiel. La référence API détaille les limites par endpoint.
  • Mettez en cache les résultats. Si vous recevez trois signaux pour la même entreprise dans la même journée, ne faites pas trois appels d’enrichissement identiques. Utilisez un cache (Redis, mémoire) avec un TTL de 24 heures.
  • Ne bloquez pas le pipeline sur un échec d’enrichissement. Si un appel échoue, le signal continue son parcours. Un signal non enrichi mais scoré est toujours plus utile qu’un signal bloqué dans une file d’attente.

Étape 5 : Appliquer le scoring Balance

Le modèle de scoring Balance de Rodz vous permet de prioriser vos signaux en combinant plusieurs critères. Dans un pipeline multi-signaux, le scoring est ce qui transforme un flux brut en une liste ordonnée par priorité.

async function scoreSignal(signal) {
  try {
    const scoreRes = await apiClient.post('/signals/score', {
      signal_id: signal.id,
      company_id: signal.company.rodzId,
      signal_type: signal.type,
      enrichment_data: signal.enrichment,
    });

    signal.score = {
      total: scoreRes.data.score,
      breakdown: scoreRes.data.breakdown,
      tier: scoreRes.data.tier, // "hot", "warm", "cold"
    };
  } catch (err) {
    console.warn(`Scoring échoué pour le signal ${signal.id}:`, err.message);

    // Score par défaut basé sur le type de signal
    signal.score = {
      total: getDefaultScore(signal.type),
      breakdown: { default: true },
      tier: 'warm',
    };
  }

  return signal;
}

function getDefaultScore(signalType) {
  const defaults = {
    fundraising: 75,
    job_change: 70,
    job_posting: 50,
    social_content: 30,
  };
  return defaults[signalType] || 40;
}

Le scoring Balance prend en compte non seulement le type de signal, mais aussi les données d’enrichissement (taille de l’entreprise, secteur, localisation) et l’historique des interactions. Un signal de levée de fonds pour une entreprise SaaS de 50 personnes dans votre ICP aura un score bien supérieur à celui d’une entreprise hors cible.

Le champ tier (hot, warm, cold) simplifie les règles de routage à l’étape suivante. Vous n’avez pas besoin de définir des seuils numériques manuellement. Le modèle Balance classe chaque signal dans un niveau de priorité directement exploitable.

Étape 6 : Router vers les bons canaux

Le routage est l’étape qui transforme un signal scoré en action concrète. Les règles de routage dépendent de deux paramètres : le score (tier) et le type de signal.

async function routeSignal(signal) {
  const routes = [];

  // Règle 1 : les signaux "hot" vont toujours dans le CRM et Slack
  if (signal.score.tier === 'hot') {
    routes.push(pushToCRM(signal), notifySlack(signal, '#signaux-hot'));

    // Si on a un email vérifié, déclencher une séquence
    if (signal.enrichment?.contacts?.[0]?.email_verified) {
      routes.push(addToEmailSequence(signal));
    }
  }

  // Règle 2 : les signaux "warm" vont dans le CRM uniquement
  if (signal.score.tier === 'warm') {
    routes.push(pushToCRM(signal));
    routes.push(notifySlack(signal, '#signaux-warm'));
  }

  // Règle 3 : les signaux "cold" sont archivés pour analyse
  if (signal.score.tier === 'cold') {
    routes.push(archiveSignal(signal));
  }

  // Règle spécifique : les changements de poste C-level déclenchent
  // toujours une notification, même avec un score faible
  if (signal.type === 'job_change' && signal.details.rawData?.data?.seniority === 'c-level') {
    routes.push(notifySlack(signal, '#mouvements-clevel'));
  }

  // Exécuter toutes les actions en parallèle
  const results = await Promise.allSettled(routes);

  signal.routed = true;
  signal.actions = results.map((r, i) => ({
    status: r.status,
    reason: r.status === 'rejected' ? r.reason?.message : undefined,
  }));

  return signal;
}

Voici des exemples d’implémentation pour les fonctions de routage :

async function pushToCRM(signal) {
  // Exemple avec HubSpot - voir le guide dédié pour les détails
  const hubspotPayload = {
    properties: {
      company: signal.company.name,
      signal_type: signal.type,
      signal_headline: signal.details.headline,
      score: signal.score.total,
      signal_date: signal.timestamp,
    },
  };

  // Appel à l'API HubSpot pour créer ou mettre à jour le deal
  await hubspotClient.crm.deals.basicApi.create({
    properties: hubspotPayload.properties,
  });
}

async function notifySlack(signal, channel) {
  const emoji = {
    fundraising: '💰',
    job_posting: '📋',
    job_change: '🔄',
    social_content: '📢',
  };

  await slackClient.chat.postMessage({
    channel,
    text: `${emoji[signal.type] || '📡'} *${signal.details.headline}*\nScore : ${signal.score.total}/100 (${signal.score.tier})\nSecteur : ${signal.company.industry}\n<https://app.rodz.io/signals/${signal.id}|Voir dans Rodz>`,
  });
}

async function addToEmailSequence(signal) {
  const contact = signal.enrichment.contacts[0];
  // Intégration avec votre outil de séquences (Lemlist, HubSpot, etc.)
  await sequenceClient.addContact({
    email: contact.email,
    firstName: contact.first_name,
    lastName: contact.last_name,
    company: signal.company.name,
    signalType: signal.type,
    signalHeadline: signal.details.headline,
  });
}

Pour l’intégration CRM complète, consultez le guide de connexion Rodz/HubSpot.

Assembler le pipeline complet

Voici la fonction processSignal qui orchestre les six étapes :

const processedSignals = new Set();

async function processSignal(payload) {
  // Déduplication : vérifier si le signal a déjà été traité
  if (processedSignals.has(payload.signal_id)) {
    console.log(`Signal ${payload.signal_id} déjà traité, ignoré.`);
    return;
  }
  processedSignals.add(payload.signal_id);

  console.log(`[PIPELINE] Début du traitement : ${payload.signal_id} (${payload.signal_type})`);

  // Étape 1 : Normalisation
  let signal = normalizeSignal(payload);
  console.log(`[NORMALISATION] ${signal.details.headline}`);

  // Étape 2 : Enrichissement
  signal = await enrichSignal(signal);
  const contactCount = signal.enrichment?.contacts?.length || 0;
  console.log(`[ENRICHISSEMENT] ${contactCount} contact(s) trouvé(s)`);

  // Étape 3 : Scoring
  signal = await scoreSignal(signal);
  console.log(`[SCORING] Score : ${signal.score.total}/100 (${signal.score.tier})`);

  // Étape 4 : Routage
  signal = await routeSignal(signal);
  console.log(`[ROUTAGE] ${signal.actions.length} action(s) déclenchée(s)`);

  // Persister le signal traité pour analyse
  await saveToDatabase(signal);
  console.log(`[PIPELINE] Traitement terminé : ${signal.id}`);

  return signal;
}

En production, remplacez le Set en mémoire par une base de données (Redis ou PostgreSQL) pour la déduplication. Un Set en mémoire ne survit pas à un redémarrage du serveur.

Stratégies de déduplication

Dans un pipeline multi-signaux, la déduplication est un sujet critique. Vous allez rencontrer deux cas de figure.

Doublons techniques

Le même signal est livré deux fois par le webhook (timeout réseau, retry automatique). La solution est simple : utilisez le signal_id comme clé de déduplication. Avant de traiter un signal, vérifiez s’il existe déjà dans votre base.

async function isDuplicate(signalId) {
  const exists = await db.query('SELECT 1 FROM processed_signals WHERE signal_id = $1', [signalId]);
  return exists.rows.length > 0;
}

Doublons métier

La même entreprise génère plusieurs signaux dans un intervalle court. Par exemple, une startup lève des fonds (signal financier) et publie un post LinkedIn pour l’annoncer (signal social). Ce ne sont pas des doublons techniques (les signal_id sont différents), mais du point de vue commercial, vous ne voulez pas contacter la même entreprise deux fois en 24 heures.

La solution consiste à grouper les signaux par entreprise avec une fenêtre de temps :

async function getRecentSignalsForCompany(companyId, windowHours = 24) {
  const result = await db.query(
    `SELECT * FROM processed_signals
     WHERE company_rodz_id = $1
     AND processed_at > NOW() - INTERVAL '${windowHours} hours'
     ORDER BY score_total DESC`,
    [companyId],
  );
  return result.rows;
}

async function shouldProcess(signal) {
  const recent = await getRecentSignalsForCompany(signal.company.rodzId);

  if (recent.length === 0) return true;

  // Si un signal de score supérieur existe déjà, ne pas router (mais sauvegarder)
  const highestExisting = recent[0].score_total;
  if (signal.score && signal.score.total <= highestExisting) {
    console.log(`Signal ${signal.id} : entreprise déjà traitée avec un score supérieur (${highestExisting})`);
    return false;
  }

  return true;
}

Cette approche vous permet de conserver tous les signaux en base (pour l’analyse), mais de ne router que le signal le plus pertinent pour chaque entreprise dans une fenêtre donnée. Si une entreprise cumule trois signaux en 24 heures, c’est probablement un compte très chaud, et le signal avec le score le plus élevé est celui qui doit déclencher l’action.

Monitoring et alertes

Un pipeline qui tourne sans supervision est un pipeline qui va tomber en silence. Voici les métriques essentielles à suivre :

Volume de signaux. Comptez le nombre de signaux reçus, traités et routés par heure. Une chute soudaine peut indiquer un problème de webhook. Un pic peut indiquer un événement marché à analyser.

Taux d’enrichissement. Quel pourcentage de signaux sont enrichis avec succès ? Si ce taux chute en dessous de 60 %, vérifiez vos quotas API et la qualité des données en entrée.

Latence de traitement. Mesurez le temps entre la réception du webhook et la fin du routage. Si la latence dépasse 30 secondes, votre pipeline est trop lent pour une prospection en temps réel.

Taux d’erreur par étape. Identifiez quelle étape du pipeline génère le plus d’erreurs. L’enrichissement est souvent le maillon le plus fragile (dépendance à une API externe).

Voici un exemple de module de monitoring :

const metrics = {
  received: 0,
  processed: 0,
  enriched: 0,
  scored: 0,
  routed: 0,
  errors: { enrichment: 0, scoring: 0, routing: 0 },
  lastSignalAt: null,
};

function recordMetric(step, success = true) {
  if (success) {
    metrics[step]++;
  } else {
    metrics.errors[step]++;
  }
  metrics.lastSignalAt = new Date().toISOString();
}

// Endpoint de health check
app.get('/health', (req, res) => {
  const minutesSinceLastSignal = metrics.lastSignalAt
    ? (Date.now() - new Date(metrics.lastSignalAt).getTime()) / 60000
    : null;

  const healthy = minutesSinceLastSignal === null || minutesSinceLastSignal < 120;

  res.status(healthy ? 200 : 503).json({
    status: healthy ? 'ok' : 'degraded',
    metrics,
    minutesSinceLastSignal: minutesSinceLastSignal?.toFixed(1),
  });
});

Configurez une alerte (PagerDuty, OpsGenie, ou un simple cron + Slack) qui vous prévient si le health check retourne un code 503 ou si aucun signal n’a été reçu depuis plus de 2 heures pendant les heures ouvrées.

Considérations de mise à l’échelle

Quand votre pipeline commence à traiter plus de 1 000 signaux par jour, quelques ajustements deviennent nécessaires.

Introduisez une file d’attente. Remplacez l’appel direct à processSignal par une mise en file d’attente (Redis avec Bull, RabbitMQ, ou AWS SQS). Cela découple la réception (qui doit rester rapide) du traitement (qui peut prendre du temps à cause de l’enrichissement).

Parallélisez le traitement. Avec une file d’attente, vous pouvez lancer plusieurs workers qui consomment les signaux en parallèle. Trois workers peuvent traiter trois signaux simultanément, ce qui divise la latence globale par trois.

Optimisez les appels d’enrichissement. Les endpoints d’enrichissement sont souvent le goulot d’étranglement. Implémentez un cache Redis avec un TTL de 24 heures pour les résultats d’enrichissement par domaine d’entreprise. Regroupez les appels par lots quand l’API le permet.

Partitionnez votre base de données. Si vous conservez l’historique de tous les signaux traités, la table de déduplication peut grossir rapidement. Partitionnez par mois et purgez les entrées de plus de 90 jours.

Surveillez vos quotas API. L’API Rodz applique des limites de débit par plan. Quand vous approchez des 80 % de votre quota, mettez en place un mécanisme de throttling qui ralentit le traitement au lieu de s’arrêter brutalement.

Questions fréquentes

Combien de types de signaux puis-je combiner dans un seul pipeline ?

Il n’y a pas de limite technique côté Rodz. Vous pouvez configurer autant de types de signaux que votre plan le permet et les recevoir tous sur le même webhook. En pratique, commencez avec 3 ou 4 types de signaux qui correspondent à votre stratégie commerciale, puis ajoutez-en progressivement. Un pipeline qui reçoit 10 types de signaux sans règles de routage précises pour chacun génère plus de bruit que de valeur.

Faut-il un webhook par type de signal ou un seul pour tout ?

Un seul webhook est préférable dans la grande majorité des cas. L’avantage principal est la corrélation : quand votre pipeline reçoit tout au même endroit, il peut détecter qu’une entreprise génère plusieurs signaux simultanément et ajuster le scoring en conséquence. Le seul cas où des webhooks séparés se justifient, c’est quand vous avez des équipes distinctes qui traitent des types de signaux différents (par exemple, l’équipe marketing traite les signaux sociaux et l’équipe sales traite les signaux financiers).

Comment gérer les pics de volume ?

Les pics de volume surviennent lors d’événements marché (annonce de résultats trimestriels, vague de levées de fonds). La file d’attente est votre meilleur allié. Elle absorbe le pic en entrée et vos workers le traitent à leur rythme. Si vous n’avez pas de file d’attente, implémentez un mécanisme de backpressure qui ralentit l’acquittement des webhooks quand la charge dépasse un seuil. Rodz réessaiera les livraisons non acquittées avec un backoff progressif.

Quelle base de données utiliser pour stocker les signaux traités ?

PostgreSQL est un excellent choix par défaut. Il gère bien les requêtes de déduplication, les recherches par entreprise et l’archivage. Pour la file d’attente et le cache d’enrichissement, Redis est plus adapté. Si vous traitez plus de 10 000 signaux par jour et que vous avez besoin d’analyses complexes, envisagez un data warehouse comme BigQuery ou Snowflake en complément.

Comment tester mon pipeline avant de passer en production ?

L’API Rodz propose un mode sandbox qui vous permet de recevoir des signaux de test sur votre webhook. Activez-le depuis votre tableau de bord. Vous pouvez aussi générer des payloads de test manuellement avec cURL et les envoyer à votre endpoint local. Pour le développement, un outil comme ngrok expose votre serveur local sur une URL HTTPS publique, ce qui vous évite de déployer à chaque modification. Consultez la documentation officielle de l’API pour les détails du mode sandbox.

Le pipeline fonctionne-t-il avec d’autres CRM que HubSpot ?

Absolument. Le routage vers le CRM est une des dernières étapes du pipeline et ne dépend pas du reste de l’architecture. Vous pouvez router vers Pipedrive, Salesforce, ou tout CRM qui propose une API. L’article sur la connexion HubSpot utilise HubSpot comme exemple, mais le principe est identique pour tout CRM. Adaptez la fonction pushToCRM avec le client API de votre CRM.

Comment mesurer le ROI de mon pipeline multi-signaux ?

Suivez trois indicateurs clés. Premièrement, le taux de conversion des signaux routés en réponses positives (rendez-vous obtenus, deals ouverts). Deuxièmement, le temps de réaction entre la détection du signal et le premier contact commercial. Troisièmement, le taux de pertinence perçu par les commerciaux. Si plus de 30 % des signaux routés sont jugés non pertinents par l’équipe, resserrez vos filtres ou ajustez vos règles de scoring.

Puis-je utiliser ce pipeline pour alimenter des campagnes marketing, pas seulement des actions commerciales ?

Oui, le routage est entièrement configurable. Vous pouvez ajouter des destinations marketing en plus des destinations commerciales : ajout dans une audience publicitaire (LinkedIn Ads, Google Ads), déclenchement d’un workflow de nurturing (HubSpot, ActiveCampaign), mise à jour d’un tableau de bord BI. Le principe reste le même : le score et le type de signal déterminent la destination. Un signal “cold” peut alimenter une campagne de notoriété tandis qu’un signal “hot” déclenche un appel commercial direct.

Pour aller plus loin

Ce guide vous a donné les fondations d’un pipeline multi-signaux fonctionnel. Pour approfondir chaque composant, consultez les ressources suivantes :

Le pipeline décrit ici est volontairement linéaire pour rester lisible. En production, vous ajouterez probablement des branches conditionnelles, des circuits de retry plus sophistiqués et des intégrations spécifiques à votre stack. L’essentiel est de garder les six étapes bien séparées. Chaque étape doit pouvoir évoluer, être testée et être déboguée indépendamment des autres.

Partager :

Générez votre stratégie outbound gratuitement

Notre IA analyse votre entreprise et crée un playbook complet : ICP, personas, templates d'emails, scripts d'appels.

Générer ma stratégie