Aller au contenu principal
Guides techniques

Construire un pipeline multi-signaux avec l'API Rodz

Peter Cools · · Mis à jour le 3 mai 2026 · 15 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 dans un flux unifié. Plutôt que 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 de base est celle-ci : chaque signal d’affaires, pris individuellement, révèle une partie du contexte d’un prospect. Une levée de fonds indique qu’une entreprise a du budget. Une offre d’emploi sur un poste commercial montre qu’elle se structure pour vendre. Un changement de poste chez un décideur signale que la fenêtre de décision est ouverte. Croisés, ces signaux racontent une histoire complète du contexte dans lequel se trouve l’entreprise. C’est cette lecture combinée qui permet de prioriser les efforts commerciaux avec précision, bien au-delà de ce qu’un signal unique peut offrir.

Or sans architecture adaptée, ces signaux arrivent dans des formats différents, à des moments différents, avec des niveaux de détail variables. On finit 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, vérifiez que vous disposez 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 d’entrer 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é « non enrichi » et passe à l’étape de scoring avec les données disponibles. Cette résilience compte quand vous traitez des dizaines ou des centaines de signaux par jour.

Principes de conception

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 le guide des signaux financiers et le guide des signaux RH pour ajuster vos filtres.

Le point qui compte ici : configurez 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 simultanément.

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 sert à 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');
});

Point à retenir : 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 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, mettez en place 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é reste 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 permet de prioriser vos signaux en combinant plusieurs critères. Dans un pipeline multi-signaux, le scoring transforme un flux brut en 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 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

Partager :

Détectez vos prochains clients automatiquement

100 crédits offerts. Sans carte bancaire.

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