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

> Source: https://www.rodz.io/fr/guides/api/recepteur-webhook-hmac/
> Mis à jour le 2026-10-12 · Peter Cools · Recette API · Tous les signaux · Webhook

## 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.

## 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. 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. É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`

```javascript
// 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. 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)`

```python
# 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. 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`

```bash
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. 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`

```bash
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.

```http
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ête               | Contenu                                                            | Usage                             |
| --------------------- | ------------------------------------------------------------------ | --------------------------------- |
| `X-Webhook-Signature` | HMAC-SHA256 en hexadécimal                                         | Authentifier la requête           |
| `X-Webhook-Timestamp` | 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

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](/fr/guides/cas-usage/levees-de-fonds-vers-n8n/) et [Make vers Salesforce](/fr/guides/cas-usage/offres-emploi-vers-salesforce-avec-make/).

## 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

- [Référence de l'API Rodz : webhooks, signature, retentatives, test-webhook et replay-webhook](https://api.rodz.io/docs)
- [RFC 2104 : HMAC, authentification de messages par hachage à clé](https://www.rfc-editor.org/rfc/rfc2104)
- [Express : express.raw()](https://expressjs.com/en/api.html)

## Guides associés

- https://www.rodz.io/fr/guides/api/recuperer-les-leads-d-un-signal/
- https://www.rodz.io/fr/guides/api/creer-et-router-un-signal-par-api/
- https://www.rodz.io/fr/guides/cas-usage/levees-de-fonds-vers-n8n/
