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
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.
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.
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 jsonimport osimport pathlibimport timefrom datetime import datetime, timedelta, timezoneimport requestsAPI = "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()
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.
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.