Python SDK
One typed client for everything Renidly does — resolve, search, enrich, verify, and check your balance — with the production plumbing already written: retries, pagination, batch jobs, typed errors, and rate limiting. If you're integrating from Python, this is the fastest correct path.
pip install renidlyPython 3.9+. Two dependencies total — httpx and pydantic — nothing exotic in your lockfile. Source on GitHub, releases on PyPI, MIT licensed.
from renidly import Renidly
renidly = Renidly("rnd-...") # or Renidly() and set RENIDLY_API_KEY
person = renidly.data.people.retrieve(handle="ryanroslansky")
company = renidly.data.companies.retrieve(slug="stripe")
email = renidly.emails.verify("[email protected]")
print(person.headline, company.name, email.deliverable)Why not just call the API yourself?
You can — the REST surface is one header and one envelope, and it will always work with any HTTP client. But a correct production integration is never just the happy-path request. It's everything around it, and every team hand-rolling a client rebuilds the same stack of plumbing — then maintains it forever:
| With your own HTTP client, you write… | With the SDK… |
|---|---|
| Retry logic with backoff + jitter for rate limits, transient outages, and dropped connections | Built in and on by default — transient failures retry themselves. |
| Pagination loops: read the cursor, fetch, append, repeat, don’t double-fetch | One call: auto_paging_iter() walks every page lazily, one page in memory at a time. |
| Batch orchestration: submit, poll, advance cursors, collect, handle partial completion | job.wait() blocks to completion; job.stream() yields results as they resolve. |
| Error handling: parse the envelope, branch on codes, map to your own exceptions | A typed exception hierarchy — except RateLimitError as e: e.retry_after. |
| A rate limiter tuned to your tier — and retuned when your tier changes | auto_rate_limit=True: a sliding-window throttle that reads your tier and adapts. |
| A second copy of all of the above for your async code paths | AsyncRenidly — the identical surface, just await. |
| Guesswork: which fields exist, what they’re called, what comes back | Fully typed (ships py.typed) — methods, parameters, and returns autocomplete in your IDE. |
One client, four products
The whole surface reads as renidly.<product>.<resource>.<action>(...) — if you can say what you want, you can usually type it:
| Product | What it is | A taste |
|---|---|---|
data | Clean, deduplicated records — people, companies, institutions, skills, job changes — by stable ID or rich filters. | data.people.search(title="cto") |
live | The freshest snapshot of a single subject on demand — profiles, headcount, activity, opportunities, discovery. | live.organizations.headcount(oid) |
emails | Verify deliverability, find work emails, reverse-lookup an address, list known contacts for a domain. | emails.find(first_name=..., last_name=..., domain=...) |
account | Your balance, tier, rate limit, and per-endpoint costs — readable at runtime, so nothing is hardcoded. | account.balance().balance |
All four authenticate with the same key and bill from the same credit balance — one client object covers your whole integration.
The plumbing, in practice
Pagination disappears
# Walks EVERY page lazily — fetches as it goes, one page in memory at a time.
for person in renidly.data.people.search(title="cto").auto_paging_iter():
print(person.headline)Batch jobs are two lines
Up to 1,000 items per job. Submitting returns a handle instantly; you choose whether to block or stream:
job = renidly.emails.verify_batch(["[email protected]", "[email protected]"])
result = job.wait() # block until done, collect everything
# ...or stream verdicts the moment each one lands:
for row in job.stream():
print(row.email, row.deliverable)Errors tell you what to do
Every failure raises a specific subclass of RenidlyError carrying the machine-usable detail — no envelope parsing, no string matching:
from renidly import InvalidRequestError, RateLimitError, InsufficientCreditsError
try:
renidly.emails.find(first_name="A", last_name="B", domain="bad")
except InvalidRequestError as e:
print(e.field_errors) # {"domain": "must be a bare hostname"}
except RateLimitError as e:
time.sleep(e.retry_after) # the wait is handed to you
except InsufficientCreditsError:
... # top up, then retryA lookup that simply finds nothing isn't an error — single retrieve(...) calls return Noneby default, so "not found" stays a branch, not a try/except.
Rate limiting you don't have to build
from renidly import Renidly, RenidlyConfig
renidly = Renidly("rnd-...", config=RenidlyConfig(auto_rate_limit=True))The SDK reads your tier's per-minute limit, throttles itself under it with a sliding 60-second window, and re-reads the tier if a limit response ever slips through. Upgrade your tier and the throttle widens on its own — nothing to redeploy. See Rate Limits & Credits for how tiers work.
Async without a second codebase
from renidly import AsyncRenidly
async with AsyncRenidly("rnd-...") as renidly:
company = await renidly.data.companies.retrieve(slug="stripe")
async for p in renidly.data.people.search(title="cto").auto_paging_iter():
print(p.headline)It never boxes you in
Convenience shouldn't cost you control. Every default is a config switch on one object (RenidlyConfig): timeouts, retry counts, whether lookups raise or return None, whether you get the unwrapped model or the full response envelope. Per-request options={...} overrides any of it for a single call — useful for multi-tenant apps passing a different key per request. And if you need full control of connection pooling or proxies, hand the client your own configured httpx instance and the SDK uses it as-is.