Aller au contenu principal
Recette API Webhook Intégration développeur

Comment recevoir les leads Rodz par webhook et vérifier leur signature HMAC ?

Peter Cools · Mis à jour le · 5 min de lecture · Version Markdown

En bref

Exposez une route POST en HTTPS, lisez le corps brut, recalculez le HMAC-SHA256 de « timestamp + point + corps » avec le secret de webhook de votre clé API et comparez-le en temps constant à l'en-tête X-Webhook-Signature. Rejetez les horodatages trop anciens, ignorez un X-Webhook-Id déjà traité, répondez 200 tout de suite et traitez le lead ensuite. Testez avec l'endpoint test-webhook.

Chiffres clés

  • La signature est un HMAC-SHA256 en hexadécimal de {X-Webhook-Timestamp}.{corps brut}, avec un horodatage Unix en secondes recalculé à chaque envoi.
  • Un timeout ou un 5xx déclenche des retentatives avec un délai croissant ; un 4xx est définitif ; un 429 demande à Rodz de ralentir puis de réessayer.
  • L'API accepte 100 requêtes par seconde par clé (fenêtre glissante d'une seconde).
  • Rodz identifie 400 000+ signaux par mois (moyenne de juillet à septembre 2026), livrés par webhook ou lisibles par l’API.
Mise en place : 30 min

Avant de commencer

  • Une clé API Rodz et son secret de webhook : Intégrations → API dans le tableau de bord (le secret peut y être régénéré).
  • Un signal lié à cette clé API (créé avec elle par l'API) : sans clé liée, les livraisons partent sans signature.
  • Une URL HTTPS publique pour votre serveur (en développement, un tunnel suffit).
  • Node.js 18+ avec Express 4, ou Python 3.10+ avec FastAPI.

Étapes

  1. Étape 1. Récupérez le secret de webhook

    Dans le tableau de bord Rodz, ouvrez Intégrations → API. Chaque clé API a un secret de webhook généré automatiquement. Copiez-le dans une variable d'environnement RODZ_WEBHOOK_SECRET, jamais dans le code.

  2. Étape 2. Écrivez le récepteur en Node.js

    Le point clé : lire le corps brut (express.raw), car la signature porte sur les octets exacts envoyés. Le récepteur vérifie la signature, refuse un horodatage de plus de 5 minutes, ignore une livraison déjà vue, répond 200 puis traite le lead.

    node/server.js

    // Récepteur de webhook Rodz (Node.js 18+, Express 4).
    const express = require('express');
    const crypto = require('crypto');
    
    const SECRET = process.env.RODZ_WEBHOOK_SECRET;
    const TOLERANCE_SECONDS = 300;
    const seen = new Set(); // en production : Redis ou votre base
    
    function isValid(rawBody, signature, timestamp) {
      if (!signature || !timestamp) return false;
      const expected = crypto
        .createHmac('sha256', SECRET)
        .update(Buffer.concat([Buffer.from(`${timestamp}.`, 'utf8'), rawBody]))
        .digest('hex');
      const a = Buffer.from(expected, 'utf8');
      const b = Buffer.from(signature, 'utf8');
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    
    async function handleLead(lead) {
      if (lead.signal && lead.signal.is_test) return console.log('payload de test reçu');
      // Votre logique : CRM, file de messages, base de données...
      console.log(lead.signal.type, lead.company && lead.company.name);
    }
    
    const app = express();
    app.post('/webhooks/rodz', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => {
      const signature = req.get('X-Webhook-Signature');
      const timestamp = req.get('X-Webhook-Timestamp');
      if (!isValid(req.body, signature, timestamp)) return res.status(401).send('invalid signature');
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
        return res.status(401).send('stale timestamp');
      }
    
      const deliveryId = req.get('X-Webhook-Id');
      if (deliveryId && seen.has(deliveryId)) return res.status(200).send('duplicate');
      if (deliveryId) seen.add(deliveryId);
    
      const lead = JSON.parse(req.body.toString('utf8'));
      res.status(200).send('ok'); // répondre d'abord
      setImmediate(() => handleLead(lead).catch(console.error)); // traiter ensuite
    });
    
    app.listen(process.env.PORT || 3000);
  3. Étape 3. Ou écrivez-le en Python

    Même logique avec FastAPI : await request.body() donne les octets bruts, hmac.compare_digest compare en temps constant, et une tâche de fond traite le lead après la réponse.

    python/app.py (uvicorn app:app)

    # Récepteur de webhook Rodz (Python 3.10+, FastAPI).
    import hashlib
    import hmac
    import json
    import os
    import time
    
    from fastapi import BackgroundTasks, FastAPI, Request, Response
    
    SECRET = os.environ["RODZ_WEBHOOK_SECRET"].encode("utf-8")
    TOLERANCE_SECONDS = 300
    seen: set[str] = set()  # en production : Redis ou votre base
    
    app = FastAPI()
    
    
    def is_valid(body: bytes, signature: str, timestamp: str) -> bool:
        if not signature or not timestamp:
            return False
        message = f"{timestamp}.".encode("utf-8") + body
        expected = hmac.new(SECRET, message, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, signature)
    
    
    def handle_lead(lead: dict) -> None:
        if lead.get("signal", {}).get("is_test"):
            print("payload de test reçu")
            return
        company = lead.get("company") or {}
        print(lead["signal"]["type"], company.get("name"))
    
    
    @app.post("/webhooks/rodz")
    async def rodz_webhook(request: Request, tasks: BackgroundTasks) -> Response:
        body = await request.body()
        signature = request.headers.get("X-Webhook-Signature", "")
        timestamp = request.headers.get("X-Webhook-Timestamp", "")
        if not is_valid(body, signature, timestamp):
            return Response("invalid signature", status_code=401)
        if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
            return Response("stale timestamp", status_code=401)
    
        delivery_id = request.headers.get("X-Webhook-Id")
        if delivery_id and delivery_id in seen:
            return Response("duplicate", status_code=200)
        if delivery_id:
            seen.add(delivery_id)
    
        tasks.add_task(handle_lead, json.loads(body))
        return Response("ok", status_code=200)
  4. Étape 4. Branchez le signal et envoyez un test

    Enregistrez votre URL comme webhookUrl du signal (à la création, ou par PATCH /api/v1/signals/{signal_id}). Puis appelez POST /api/v1/signals/{signal_id}/test-webhook : avec un corps {"url": "..."}, vous testez une autre adresse avant de l'enregistrer. La réponse donne success, le statusCode de votre serveur, le payloadSent (avec signal.is_test à true) et signed : false veut dire qu'aucune clé API n'est liée au signal.

    Tester le récepteur

    curl -X POST "https://api.rodz.io/api/v1/signals/$SIGNAL_ID/test-webhook" \
      -H "Authorization: Bearer $RODZ_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"url": "https://hooks.example.com/webhooks/rodz"}'
  5. Étape 5. Rattrapez une panne avec replay-webhook

    Si votre serveur a été indisponible au-delà des retentatives, POST /api/v1/signals/{signal_id}/replay-webhook renvoie tous les leads déjà produits par le signal, avec signal.is_replay à true. La réponse donne total, sent et failed. Votre dédoublonnage évite de créer deux fois le même lead. Pour un simple rattrapage sans webhook, GET /api/v1/signals/{signal_id}/leads?since=... lit les leads directement.

    Renvoyer les leads du signal

    curl -X POST "https://api.rodz.io/api/v1/signals/$SIGNAL_ID/replay-webhook" \
      -H "Authorization: Bearer $RODZ_API_KEY"

Exemple de payload

Extrait d'une requête telle que votre serveur la reçoit : en-têtes, puis corps JSON. Les champs de `signal` varient selon le type de signal ; `contact` vaut `""` pour un lead sans personne ; `signal.is_test` n'apparaît que sur un test.

POST /webhooks/rodz HTTP/1.1
Content-Type: application/json
X-Webhook-Id: 7f3c2a9e-1b4d-4c8e-9a01-5d6e7f8a9b0c
X-Webhook-Timestamp: 1760256000
X-Webhook-Signature: 3b1f0c...e9a4 (64 caractères hexadécimaux)

{
  "signal": {
    "id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
    "name": "Séries A et plus en France",
    "type": "fundraising",
    "last_signal": "Fundraising",
    "last_signal_date": "2026-10-12",
    "is_test": true,
    "funding_stage": "Series A",
    "funding_amount_usd": 5500000,
    "announcement_date": "2026-10-10"
  },
  "contact": "",
  "company": {
    "name": "Northwind Robotics",
    "website": "https://northwind-robotics.example",
    "linkedin_url": "https://www.linkedin.com/company/northwind-robotics-example",
    "hq_country_code": "FR",
    "hq_city": "Nantes"
  }
}

Les en-têtes d’une livraison

En-têteContenuUsage
X-Webhook-SignatureHMAC-SHA256 en hexadécimalAuthentifier la requête
X-Webhook-TimestampHorodatage Unix (secondes) utilisé dans la signatureRefuser les messages trop anciens
X-Webhook-IdIdentifiant stable de la livraison, identique à chaque retentativeDédoublonner

Les codes de réponse et leur effet

Votre réponseEffet côté Rodz
2xxLivraison réussie
429Rodz ralentit et réessaie un peu plus tard
Timeout ou 5xxRetentatives avec un délai croissant
Autre 4xxÉchec définitif, pas de retentative

Répondez en moins d’une seconde ou deux et déplacez le travail lourd (appel au CRM, enrichissement) dans une file ou une tâche de fond.

Bonnes pratiques

  1. Vérifiez toujours la signature, même derrière un pare-feu : l’URL d’un webhook finit par circuler.
  2. Stockez les X-Webhook-Id traités avec une durée de vie de quelques jours (Redis SET NX EX) plutôt qu’en mémoire.
  3. Journalisez signal.id et contact.lead_id : ce sont les identifiants à donner au support.
  4. Ignorez signal.is_test en production, et traitez signal.is_replay comme une livraison normale dédoublonnée.

Pour envoyer ces leads dans un outil sans écrire de serveur, voyez les recettes n8n et Make vers Salesforce.

Questions fréquentes

Que se passe-t-il si mon serveur répond une erreur ?

Un 2xx marque la livraison comme réussie. Un timeout ou un 5xx est retenté avec un délai croissant. Un 4xx (mauvaise URL, authentification, validation) est considéré comme définitif et n'est pas retenté, sauf 429, qui demande à Rodz de ralentir et de réessayer.

Pourquoi le même lead peut-il arriver deux fois ?

Parce que les retentatives garantissent une livraison au moins une fois : si votre réponse se perd, Rodz renvoie. L'en-tête X-Webhook-Id est identique à chaque tentative d'une même livraison ; dédoublonnez dessus.

Où trouver le secret de webhook ?

Dans Intégrations → API du tableau de bord : chaque clé API a son secret, que vous pouvez régénérer sur la même page. Après une régénération, mettez à jour la variable d’environnement de votre récepteur.

Pourquoi la vérification échoue-t-elle alors que le secret est bon ?

Presque toujours parce que la signature est recalculée sur un JSON relu et réécrit au lieu des octets bruts. Lisez le corps avant tout parseur JSON (express.raw, await request.body()). Vérifiez aussi que le test renvoie signed: true.

Faut-il un récepteur pour lire les leads ?

Non. GET /api/v1/signals/{signal_id}/leads renvoie les leads d'un signal dans le même format que le webhook, avec since, limit et offset. Lire les leads est gratuit : un lead est facturé une seule fois, quand il est produit.

Sources

Essayez sur votre marché

Créez un compte : 100 crédits offerts pour tester, sans carte bancaire.