# Comment récupérer les leads d'un signal Rodz par l'API, sans webhook ?

> Source: https://www.rodz.io/fr/guides/api/recuperer-les-leads-d-un-signal/
> Mis à jour le 2026-10-12 · Peter Cools · Recette API · Changements de poste · API REST

## En bref

Appelez GET /api/v1/signals/{signal_id}/leads avec le paramètre since, paginez avec limit et offset tant que hasMore vaut true, puis écrivez chaque lead dans votre CRM en le dédupliquant sur contact.lead_id. Lire les leads est gratuit : un lead est facturé une seule fois, quand il est produit. Un script planifié toutes les heures suffit, sans serveur exposé à internet.

## Chiffres clés

- Lire les leads est **gratuit** : un lead est facturé une seule fois, au moment où le signal le produit.
- Une page contient de **1 à 100 leads** (`limit`, 25 par défaut), du plus récent au plus ancien.
- La limite de débit est de **100 requêtes par seconde** par clé API ; au-delà, l'API répond `429` avec `Retry-After: 1`.
- Chaque lead a exactement la forme du webhook du signal : objets `signal`, `contact` et `company`, champs en snake_case.

Coût: Gratuit à la lecture (le lead est facturé à sa production)

## Avant de commencer

- Une clé API Rodz, créée dans **Intégrations → API** du tableau de bord (format `USR_...`).
- Un signal actif, par exemple un signal Nominations, et son identifiant.
- Python 3.9+ avec la bibliothèque `requests` (`pip install requests`), ou simplement `curl`.
- Un endroit où écrire les leads : CRM, base de données, tableur.

## Étapes

### 1. Trouvez l'identifiant de votre signal

`GET /api/v1/signals` liste les signaux du compte avec leur `id`, leur `status`, leur `signalTypeId` et leurs compteurs `leadsToday` et `leadsTotal`. Filtrez avec `status=active` ou `signalTypeId=job_changes`.

`Lister les signaux actifs`

```bash
curl -G "https://api.rodz.io/api/v1/signals" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  --data-urlencode "status=active"
```

### 2. Lisez les leads produits depuis une date

`GET /api/v1/signals/{signal_id}/leads` renvoie les leads du plus récent au plus ancien. `since` (horodatage ISO-8601) ne garde que les leads produits à partir de cet instant. La réponse donne `total`, `limit`, `offset` et `hasMore`.

Les leads écartés par vos filtres ne sont pas renvoyés : vous ne lisez que ce qui a été livré.

`Une page de leads`

```bash
curl -G "https://api.rodz.io/api/v1/signals/$SIGNAL_ID/leads" \
  -H "Authorization: Bearer $RODZ_API_KEY" \
  --data-urlencode "since=2026-10-11T00:00:00+00:00" \
  --data-urlencode "limit=100" \
  --data-urlencode "offset=0"
```

### 3. Paginez et respectez la limite de débit

Tant que `hasMore` vaut `true`, rappelez la même URL avec `offset` augmenté de `limit`. Sur une réponse `429`, attendez le nombre de secondes indiqué par l'en-tête `Retry-After`, puis renvoyez la même requête.

Le script ci-dessous fait les deux, et garde l'heure de la dernière exécution dans un petit fichier pour ne relire que les nouveaux leads.

`poll_rodz_leads.py`

```python
import json
import os
import pathlib
import time
from datetime import datetime, timedelta, timezone

import requests

API = "https://api.rodz.io/api/v1"
SIGNAL_ID = os.environ["RODZ_SIGNAL_ID"]
STATE = pathlib.Path("rodz_last_run.json")

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['RODZ_API_KEY']}"


def get(path: str, params: dict) -> dict:
    """GET with a retry on 429 (Retry-After is given in seconds)."""
    while True:
        resp = session.get(f"{API}{path}", params=params, timeout=30)
        if resp.status_code == 429:
            time.sleep(float(resp.headers.get("Retry-After", "1")))
            continue
        resp.raise_for_status()
        return resp.json()


def leads_since(since: str):
    """Yield every lead produced at or after `since`, page by page."""
    offset = 0
    while True:
        page = get(
            f"/signals/{SIGNAL_ID}/leads",
            {"since": since, "limit": 100, "offset": offset},
        )
        yield from page["leads"]
        if not page["hasMore"]:
            return
        offset += page["limit"]


def upsert_into_crm(lead: dict) -> None:
    """Replace with your CRM call. Key on contact.lead_id so a re-read is harmless."""
    contact = lead.get("contact") or {}  # "" or null on company-only leads
    company = lead.get("company") or {}
    key = contact.get("lead_id") or f"{lead['signal']['id']}:{company.get('website', '')}"
    print(key, contact.get("first_name"), contact.get("last_name"), company.get("name"))


def main() -> None:
    # First run: the last 24 hours. Later runs: from the start of the previous run.
    default = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
    since = json.loads(STATE.read_text())["since"] if STATE.exists() else default
    started = datetime.now(timezone.utc).isoformat()  # taken BEFORE reading
    count = 0
    for lead in leads_since(since):
        upsert_into_crm(lead)
        count += 1
    STATE.write_text(json.dumps({"since": started}))
    print(f"{count} lead(s) since {since}")


if __name__ == "__main__":
    main()
```

### 4. Planifiez le script

Lancez-le toutes les heures (cron, tâche planifiée, job CI) avec `RODZ_API_KEY` et `RODZ_SIGNAL_ID` en variables d'environnement. L'heure de départ est enregistrée *avant* la lecture : un lead produit pendant l'exécution sera relu la fois suivante, et la clé `lead_id` rend cette relecture sans effet dans votre CRM.

`crontab : toutes les heures, à la minute 5`

```bash
5 * * * * cd /opt/rodz && RODZ_API_KEY=USR_xxx RODZ_SIGNAL_ID=a1b2c3d4-... python3 poll_rodz_leads.py >> poll.log 2>&1
```

## Exemple de payload

Une page renvoyée par `GET /api/v1/signals/{signal_id}/leads` (un lead Nominations, données fictives).

```json
{
  "signalId": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
  "total": 42,
  "limit": 100,
  "offset": 0,
  "hasMore": false,
  "leads": [
    {
      "signal": {
        "id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
        "name": "Nouveaux dirigeants commerciaux SaaS",
        "type": "job_changes",
        "last_signal": "Job Change",
        "last_signal_date": "2026-10-11",
        "previous_title": "Head of Sales",
        "previous_company": "Northwind Analytics",
        "new_title": "Directeur commercial",
        "new_company": "Tech Corp",
        "new_job_started_on": "09/2026",
        "new_job_start_month": "2026-09",
        "change_type": "company"
      },
      "contact": {
        "lead_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
        "created_at": "2026-10-11T09:12:00+00:00",
        "first_name": "Julien",
        "last_name": "Martin",
        "email": "julien.martin@techcorp.example",
        "phone": "",
        "title": "Directeur commercial",
        "location": "Paris, Île-de-France, France",
        "linkedin_profile_url": "https://www.linkedin.com/in/julien-martin-example",
        "matched_persona": "Sales Director"
      },
      "company": {
        "name": "Tech Corp",
        "website": "https://techcorp.example",
        "linkedin_url": "https://www.linkedin.com/company/techcorp-example",
        "industry": "Software Development",
        "employee_range": "201-500",
        "hq_country_code": "FR"
      }
    }
  ]
}
```

## Quand préférer la lecture par l'API

| Besoin                              | Webhook                             | Lecture par l'API                      |
| ----------------------------------- | ----------------------------------- | -------------------------------------- |
| Recevoir le lead à la seconde       | Oui                                 | Non (au rythme de votre planification) |
| Serveur joignable depuis internet   | Obligatoire                         | Inutile                                |
| Rattraper des leads après une panne | `POST /signals/{id}/replay-webhook` | Oui, avec `since`                      |
| Coût                                | Inclus dans le prix du lead         | Gratuit                                |

Les deux approches se combinent : un webhook pour le temps réel, et le script de lecture une fois par jour comme filet de sécurité.

## Lire plusieurs signaux

Bouclez sur les identifiants renvoyés par `GET /api/v1/signals?status=active` et gardez une date `since` par signal. Pour ne voir que les signaux créés par une intégration donnée (et pas ceux de la production quand vous testez), ajoutez `createdByThisKey=true`.

## Dépannage

- **`404 NOT_FOUND`** : l'identifiant du signal est faux, ou le signal appartient à un autre compte que celui de la clé.
- **`401 UNAUTHORIZED`** : la clé est absente ou révoquée ; l'en-tête doit être `Authorization: Bearer USR_...`.
- **Aucun lead** : vérifiez que le signal est actif et que `since` n'est pas dans le futur. L'entonnoir du signal (`GET /api/v1/signals/{signal_id}/funnel`) montre quel filtre écarte les candidats.

## Questions fréquentes

### Webhook ou lecture par l'API : que choisir ?

Le webhook livre chaque lead au moment où il est produit, mais demande un serveur joignable depuis internet. La lecture par l'API se contente d'un script planifié et sert aussi à rattraper un CRM après une panne. Les deux renvoient exactement le même format.

### La lecture des leads consomme-t-elle des crédits ?

Non. Un lead est facturé une seule fois, quand le signal le produit. Le relire par l'API, autant de fois que nécessaire, est gratuit.

### Comment éviter les doublons dans mon CRM ?

Utilisez `contact.lead_id` comme clé d'insertion ou de mise à jour. Comme `since` inclut les leads produits à partir de l'instant donné, deux exécutions peuvent se recouvrir : la clé rend ce recouvrement sans effet.

### Que se passe-t-il si je dépasse la limite de débit ?

L'API répond `429 Too Many Requests` avec un en-tête `Retry-After`. Attendez ce nombre de secondes et renvoyez la requête. À 100 requêtes par seconde par clé, un script de lecture horaire n'atteint pas cette limite.

### Pourquoi `contact` est-il vide sur certains leads ?

Certains signaux livrent des entreprises sans personne (une levée de fonds sans ciblage de persona, par exemple). `contact` est alors vide : prévoyez une clé de secours basée sur l'entreprise, comme dans le script.

## Sources

- [Référence de l’API Rodz : List a signal's leads, List signals, limites de débit](https://api.rodz.io/docs)
- [Spécification OpenAPI de l’API Rodz](https://api.rodz.io/openapi.json)
- [RFC 9110 : l'en-tête Retry-After](https://www.rfc-editor.org/rfc/rfc9110#field.retry-after)

## Guides associés

- https://www.rodz.io/fr/guides/api/recepteur-webhook-hmac/
- https://www.rodz.io/fr/guides/api/creer-et-router-un-signal-par-api/
- https://www.rodz.io/fr/guides/cas-usage/changements-de-poste-vers-hubspot/
