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
É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.
É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 basefunction 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);
É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 hashlibimport hmacimport jsonimport osimport timefrom fastapi import BackgroundTasks, FastAPI, Request, ResponseSECRET = os.environ["RODZ_WEBHOOK_SECRET"].encode("utf-8")TOLERANCE_SECONDS = 300seen: set[str] = set() # en production : Redis ou votre baseapp = 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)
É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.
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.
Horodatage Unix (secondes) utilisé dans la signature
Refuser les messages trop anciens
X-Webhook-Id
Identifiant stable de la livraison, identique à chaque retentative
Dédoublonner
Les codes de réponse et leur effet
Votre réponse
Effet côté Rodz
2xx
Livraison réussie
429
Rodz ralentit et réessaie un peu plus tard
Timeout ou 5xx
Retentatives 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
Vérifiez toujours la signature, même derrière un pare-feu : l’URL d’un webhook finit par circuler.
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.
Journalisez signal.id et contact.lead_id : ce sont les identifiants à donner au support.
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.