Skip to main content
API recipe API REST Developer integration

How do you pull a Rodz signal’s leads through the API, without a webhook?

Peter Cools · Updated on · 4 min read · View as Markdown

Short answer

Call GET /api/v1/signals/{signal_id}/leads with the since parameter, page with limit and offset while hasMore is true, then write each lead to your CRM, deduplicated on contact.lead_id. Reading leads is free: a lead is charged once, when it is produced. A script scheduled every hour is enough, with no server exposed to the internet.

Key facts

  • Reading leads is free: a lead is charged once, when the signal produces it.
  • A page holds 1 to 100 leads (limit, default 25), newest first.
  • The rate limit is 100 requests per second per API key; above it the API answers 429 with Retry-After: 1.
  • Each lead has exactly the shape of the signal’s webhook: signal, contact and company objects, snake_case fields.
Cost : Free to read (the lead is charged when produced) Setup time : 20 min

Before you start

  • A Rodz API key, created under Integrations → API in the dashboard (format USR_...).
  • An active signal, for example a Job Changes signal, and its id.
  • Python 3.9+ with the requests library (pip install requests), or just curl.
  • Somewhere to write the leads: CRM, database, spreadsheet.

Steps

  1. Step 1. Find your signal’s id

    GET /api/v1/signals lists the account’s signals with their id, status, signalTypeId and their leadsToday and leadsTotal counters. Filter with status=active or signalTypeId=job_changes.

    List active signals

    curl -G "https://api.rodz.io/api/v1/signals" \
      -H "Authorization: Bearer $RODZ_API_KEY" \
      --data-urlencode "status=active"
  2. Step 2. Read the leads produced since a date

    GET /api/v1/signals/{signal_id}/leads returns leads newest first. since (ISO-8601 timestamp) keeps only the leads produced at or after that instant. The response gives total, limit, offset and hasMore.

    Leads your filters removed are not returned: you only read what was delivered.

    One page of leads

    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. Step 3. Paginate and respect the rate limit

    While hasMore is true, call the same URL again with offset increased by limit. On a 429 response, wait the number of seconds given by the Retry-After header, then send the same request again.

    The script below does both, and keeps the time of the last run in a small file so it only reads new leads.

    poll_rodz_leads.py

    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. Step 4. Schedule the script

    Run it every hour (cron, scheduled task, CI job) with RODZ_API_KEY and RODZ_SIGNAL_ID as environment variables. The start time is saved before reading: a lead produced during the run is read again next time, and the lead_id key makes that re-read harmless in your CRM.

    crontab: every hour, at minute 5

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

Example payload

One page returned by `GET /api/v1/signals/{signal_id}/leads` (one Job Changes lead, fictitious data).

{
  "signalId": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
  "total": 42,
  "limit": 100,
  "offset": 0,
  "hasMore": false,
  "leads": [
    {
      "signal": {
        "id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
        "name": "New sales leaders in 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": "VP Sales",
        "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": "VP Sales",
        "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"
      }
    }
  ]
}

When to prefer API polling

NeedWebhookAPI polling
Get the lead within secondsYesNo (at your schedule’s pace)
Server reachable from the internetRequiredNot needed
Catch up on leads after an outagePOST /signals/{id}/replay-webhookYes, with since
CostIncluded in the lead priceFree

The two combine well: a webhook for real time, and the polling script once a day as a safety net.

Reading several signals

Loop over the ids returned by GET /api/v1/signals?status=active and keep one since date per signal. To see only the signals created by a given integration (and not production ones while you test), add createdByThisKey=true.

Troubleshooting

  • 404 NOT_FOUND: the signal id is wrong, or the signal belongs to another account than the key’s.
  • 401 UNAUTHORIZED: the key is missing or revoked; the header must be Authorization: Bearer USR_....
  • No leads: check that the signal is active and that since is not in the future. The signal’s funnel (GET /api/v1/signals/{signal_id}/funnel) shows which filter removes the candidates.

Frequently asked questions

Webhook or API polling: which one?

The webhook delivers each lead the moment it is produced, but needs a server reachable from the internet. Polling only needs a scheduled script, and it also back-fills a CRM after an outage. Both return exactly the same format.

Does reading leads use credits?

No. A lead is charged once, when the signal produces it. Reading it through the API, as often as you need, is free.

How do I avoid duplicates in my CRM?

Use contact.lead_id as the upsert key. Because since includes leads produced from the given instant onward, two runs can overlap: the key makes the overlap harmless.

What happens if I exceed the rate limit?

The API answers 429 Too Many Requests with a Retry-After header. Wait that many seconds and send the request again. At 100 requests per second per key, an hourly polling script does not come close.

Why is `contact` empty on some leads?

Some signals deliver companies without a person (a funding round without persona targeting, for example). contact is then empty: keep a fallback key based on the company, as in the script.

Sources

Try it on your own market

Create an account: 100 free credits to test, no credit card.