Renidlyrenidly
SDKs

Python SDK

4 min read

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 renidly

Python 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 connectionsBuilt in and on by default — transient failures retry themselves.
Pagination loops: read the cursor, fetch, append, repeat, don’t double-fetchOne call: auto_paging_iter() walks every page lazily, one page in memory at a time.
Batch orchestration: submit, poll, advance cursors, collect, handle partial completionjob.wait() blocks to completion; job.stream() yields results as they resolve.
Error handling: parse the envelope, branch on codes, map to your own exceptionsA typed exception hierarchy — except RateLimitError as e: e.retry_after.
A rate limiter tuned to your tier — and retuned when your tier changesauto_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 pathsAsyncRenidly — the identical surface, just await.
Guesswork: which fields exist, what they’re called, what comes backFully 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:

ProductWhat it isA taste
dataClean, deduplicated records — people, companies, institutions, skills, job changes — by stable ID or rich filters.data.people.search(title="cto")
liveThe freshest snapshot of a single subject on demand — profiles, headcount, activity, opportunities, discovery.live.organizations.headcount(oid)
emailsVerify deliverability, find work emails, reverse-lookup an address, list known contacts for a domain.emails.find(first_name=..., last_name=..., domain=...)
accountYour 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 retry

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