Renidly Python SDK
The official Python SDK for the Renidly B2B professional data APIs — resolve, search, enrich, and verify professional identities (people, organizations, institutions, skills, professional activity, job opportunities, and business email) through one clean, typed client.
from renidly import Renidly
renidly = Renidly("rnd-...")
person = renidly.data.people.retrieve(handle="ryanroslansky")
company = renidly.data.companies.retrieve(slug="stripe")
email = renidly.emails.verify("sundar@google.com")
print(person.headline, company.name, email.deliverable)
- One client, four products —
data,live,emails,account, all off the same key. - Fully typed — method names, parameters, and returns autocomplete in your IDE (ships
py.typed). - Batteries included — automatic retries, transparent pagination, batch jobs, a typed error hierarchy, and an optional self-tuning rate limiter.
- Sync and async — identical surface; just
await.
Table of contents
- Install
- Quickstart
- Authentication
- The four products
- Pagination
- Batch jobs
- Errors
- Configuration
- Automatic rate limiting
- Async
- Response objects
- Credits & response metadata
- Advanced
- Requirements & support
Install
pip install renidly
Requires Python 3.9+.
Quickstart
from renidly import Renidly
renidly = Renidly("rnd-...") # or Renidly() and set RENIDLY_API_KEY
# Retrieve a single record (returns None if nothing resolved)
person = renidly.data.people.retrieve(id="prsn_...")
if person:
print(person.first_name, person.headline)
# Search with any filters — they autocomplete in your editor
for p in renidly.data.people.search(title="cto", current_only=True).auto_paging_iter():
print(p.headline)
# Verify an email
v = renidly.emails.verify("sundar@google.com")
print(v.deliverable, v.reason)
That's the whole feel: renidly.<product>.<resource>.<action>(...).
Authentication
Pass your key positionally, via config, or through the environment — whichever you prefer.
Renidly("rnd-...") # positional
Renidly() # reads RENIDLY_API_KEY
Renidly(config=RenidlyConfig(api_key="rnd-...")) # inside the config object
Per-request override (e.g. multi-tenant apps):
renidly.data.people.search(title="cto", options={"api_key": "rnd-tenant-key"})
Grab your key from Workspace → API Keys.
The four products
data — clean, queryable records
Deduplicated professional records addressable by stable opaque IDs (prsn_, org_, inst_, skl_) or rich filters.
# People
renidly.data.people.retrieve(id="prsn_...") # or handle="..."
renidly.data.people.search(title="cto", skills="python", geo_country_code="US")
renidly.data.people.enrich_batch(handles=[...], ids=[...], live=True) # bulk (see Batch jobs)
# Companies
renidly.data.companies.retrieve(slug="google") # or id="org_..."
renidly.data.companies.search(name="stripe", staff_count_min=100)
renidly.data.companies.employees("google", title="engineer", current_only=True)
renidly.data.companies.enrich_batch(ids=[...]) # bulk (see Batch jobs)
# Institutions
renidly.data.institutions.retrieve("stanford") # by normalized name
renidly.data.institutions.search("stanford")
renidly.data.institutions.alumni("stanford", degree="MBA")
# Skills
renidly.data.skills.retrieve("skl_...")
renidly.data.skills.search("python")
# Job changes — trigger-based prospecting
renidly.data.job_changes.search(event_type="joined", days_ago=30)
live — freshest snapshot on demand
Resolve a single subject or run a discovery search.
# People — resolve a public handle to a stable id once, then reuse it
eid = renidly.live.people.resolve_handle("williamhgates").entityId
renidly.live.people.enrich(eid) # or handle="..."
renidly.live.people.employment_history(eid)
renidly.live.people.endorsements(eid)
renidly.live.people.lookalikes(eid)
renidly.live.people.interests(eid)
# Organizations
oid = renidly.live.organizations.resolve_slug("google").id
renidly.live.organizations.enrich(oid)
renidly.live.organizations.headcount(oid)
renidly.live.organizations.similar(oid)
renidly.live.organizations.affiliated(oid)
renidly.live.organizations.activities(oid)
renidly.live.organizations.opportunities("1441,1035") # comma-separated org ids
# Opportunities (job postings)
renidly.live.opportunities.retrieve("4019200001")
renidly.live.opportunities.similar("4019200001")
renidly.live.opportunities.related_views("4019200001")
renidly.live.opportunities.hiring_team("4019200001")
renidly.live.opportunities.by_person(eid)
# Activity
renidly.live.activities.feed(eid)
renidly.live.activities.retrieve(activity_id)
renidly.live.activities.reactions(activity_id)
renidly.live.activities.replies(activity_id, sort_by="date_posted")
renidly.live.activities.replies_by_author(eid)
# Discover (search)
renidly.live.discover.people(keyword="cto", count=25)
renidly.live.discover.organizations(keyword="fintech", headcountRange="51-200")
renidly.live.discover.opportunities(keyword="python", workplaceTypes="remote")
emails — verify, find, and resolve
renidly.emails.verify("sundar@google.com")
renidly.emails.find(first_name="Patrick", last_name="Collison", domain="stripe.com")
renidly.emails.find_by_url("https://example.com/in/someone") # from a professional profile URL
renidly.emails.reverse("john@acme.com") # who is behind this business email
renidly.emails.prospects("acme.com", kind="verified_only") # known contacts for a domain
# Bulk (see Batch jobs)
renidly.emails.verify_batch(["a@x.com", "b@y.com"])
renidly.emails.find_batch([{"first_name": "A", "last_name": "B", "domain": "acme.com"}])
account — balance, tier, and pricing
renidly.account.balance().balance
renidly.account.tier().current_tier.limit_per_minute
renidly.account.enterprise_balance() # for an Enterprise workspace
renidly.account.tiers() # public tier ladder (no key needed)
renidly.account.route_costs() # per-endpoint credit costs (no key needed)
Pagination
Every search/list method returns a list you can use directly and page through transparently.
page = renidly.data.people.search(title="cto", limit=25)
len(page) # items on this page
page[0] # index it
for p in page: ... # iterate this page
page.has_more # is there more?
# ...or walk 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)
# each page is a separate billed request — see its cost/balance on .meta
print(page.meta.credit_consumed, page.meta.remaining_balance)
Batch jobs
Process up to 1000 items in one async job. Submit returns a handle instantly.
job = renidly.data.people.enrich_batch(handles=["ryanroslansky", "williamhgates"], live=True)
# block until done and collect everything
result = job.wait(on_progress=lambda n: print("resolved", n))
print(result.status, result.resolved, "/", result.total)
for row in result.results:
print(row.matched_input, "->", row.headline)
print("not found:", result.not_found)
# ...or stream results as they resolve
for row in renidly.emails.verify_batch(["a@x.com", "b@y.com"]).stream():
print(row.email, row.deliverable)
Available on data.people.enrich_batch, data.companies.enrich_batch, emails.verify_batch, emails.find_batch.
Errors
Every failure raises a specific subclass of RenidlyError, and the message tells you exactly what went wrong.
from renidly import (
RenidlyError, AuthenticationError, InvalidRequestError,
InsufficientCreditsError, NotFoundError, RateLimitError,
PermissionDeniedError, ServiceUnavailableError,
)
try:
renidly.emails.find(first_name="A", last_name="B", domain="bad")
except InvalidRequestError as e:
print(e.message) # "Validation failed"
print(e.field_errors) # {"domain": "must be a bare hostname"}
except RateLimitError as e:
print(e.tier, e.limit, e.retry_after)
except InsufficientCreditsError:
...
except RenidlyError as e: # catch-all
print(e.status_code, e.error_code, e.message, e.errors)
The string form includes the detail, so an uncaught error is self-explanatory:
InvalidRequestError: Validation failed — domain: must be a bare hostname (VALIDATION_ERROR, HTTP 400)
| Exception | When |
|---|---|
AuthenticationError |
missing / invalid key |
PermissionDeniedError |
key valid but not allowed here |
InvalidRequestError |
bad input (see .field_errors) |
InsufficientCreditsError |
not enough credits |
NotFoundError |
job not found / expired |
RateLimitError |
per-minute limit hit (.tier, .limit, .retry_after) |
ServiceUnavailableError |
temporary — retry shortly |
APIConnectionError |
network / timeout |
Not-found lookups: a single
retrieve(...)that resolves nothing returnsNoneby default (not an exception). Setraise_on_not_found=Trueto raise instead.
Configuration
All options live on one object, RenidlyConfig:
from renidly import Renidly, RenidlyConfig
renidly = Renidly("rnd-...", config=RenidlyConfig(
timeout=30,
max_retries=3, # auto-retry on 429 / 503 / connection errors (backoff + jitter)
unwrap_data_obj=True, # return the data model (False -> the full envelope)
raise_on_not_found=False, # single lookups return None when empty (True -> raise)
raise_on_api_error=True, # map failures to typed exceptions
auto_rate_limit=False, # see below
))
| Option | Default | Meaning |
|---|---|---|
api_key |
RENIDLY_API_KEY env |
Your key (positional arg overrides this). |
timeout |
30 |
Per-request timeout (seconds). |
max_retries |
2 |
Retries on transient failures. |
backoff_factor |
0.5 |
Base seconds for exponential backoff. |
base_url |
https://renidly.com |
Override the API host. |
proxy |
None |
HTTP(S) proxy URL. |
default_headers |
{} |
Extra headers on every request. |
unwrap_data_obj |
True |
Return data vs the full envelope. |
raise_on_not_found |
False |
None vs NotFoundError on empty lookups. |
raise_on_api_error |
True |
Raise vs return None on API errors. |
auto_rate_limit |
False |
Self-throttle to your tier's limit. |
rate_limit_per_minute |
None |
Override the limit read from your account. |
rate_limit_safety |
1.0 |
Fraction of the limit to target (e.g. 0.9). |
Automatic rate limiting
Turn it on and the SDK keeps you under your per-minute limit automatically — no limiter to build.
# Any key (tiered or enterprise): the limit is read from your account.
Renidly("rnd-...", config=RenidlyConfig(auto_rate_limit=True))
# Optional: override it with a fixed limit.
Renidly("rnd-...", config=RenidlyConfig(auto_rate_limit=True, rate_limit_per_minute=550))
It uses a sliding 60-second window so you never exceed the limit, and re-reads your tier after a 429.
If the limit can't be determined, the SDK emits a RuntimeWarning and does not throttle client-side
(server 429s are still retried) until a later refresh succeeds.
Async
Same surface, same names — just await (and use it as a context manager to auto-close the connection pool).
import asyncio
from renidly import AsyncRenidly
async def main():
async with AsyncRenidly("rnd-...") as renidly:
company = await renidly.data.companies.retrieve(slug="stripe")
print(company.name)
async for p in renidly.data.people.search(title="cto").auto_paging_iter():
print(p.headline)
asyncio.run(main())
Response objects
Responses are dynamic and drill-able — access any field (nested included) as an attribute, no schema classes required.
t = renidly.account.tier()
t.current_tier.name # nested attribute access, arbitrarily deep
t.model_dump() # convert to a plain dict anytime
# HTTP metadata is attached to every object under .meta (see next section)
t.meta.status_code
t.meta.request_id
Prefer the raw envelope? Set unwrap_data_obj=False and every call returns APIResponse(success=..., data=..., message=...) — which also carries .meta.
Credits & response metadata
Every result carries a .meta object describing the HTTP call that produced it — including how many credits it cost and your balance afterward. It's kept separate from the response data, so person.headline is your data and person.meta.credit_consumed is billing info.
person = renidly.data.people.retrieve(id="prsn_06d0d44d…")
person.meta.credit_consumed # -> 1.0 credits charged for THIS request
person.meta.remaining_balance # -> 19813.0 balance after the charge
person.meta.status_code # -> 200
person.meta.request_id # server request id (if provided)
person.meta.headers # raw response headers (dict)
person.meta.body # parsed JSON envelope
person.meta.raw_body # raw response text
person.meta.raw_http # the underlying httpx.Response (everything else)
.meta is on every result — single objects, list pages, and each item in a page:
page = renidly.data.people.search(title="cto")
page.meta.credit_consumed # cost of fetching this page
page[0].meta.remaining_balance # same page → same balance
Notes:
credit_consumed/remaining_balanceareNonefor endpoints that aren't credit-billed (e.g.account.*) or when a request wasn't charged (errors, cached hits, zero-result billing).- Result-billed endpoints report the real dynamic amount — e.g.
emails.prospects("acme.com", kind="full")returning 18 emails showsmeta.credit_consumed == 18. - Cached responses are served free:
meta.credit_consumed == 0with the balance unchanged. - During
auto_paging_iter(), each page is a separate billed request, so each item reflects its own page'smeta(walk the items to see the balance step down per page). .last_responseremains as a deprecated alias for.meta.
Advanced
Per-request options override config for a single call:
renidly.data.skills.search("python", options={"timeout": 5, "api_key": "rnd-other"})
Escape hatch — call any endpoint directly:
env = renidly.raw_request("GET", "/people/search", service="data", params={"title": "cto"})
print(env.success, env.data)
Bring your own HTTP client (connection pools, custom timeouts, mounts):
import httpx
Renidly("rnd-...", http_client=httpx.Client(limits=httpx.Limits(max_connections=50)))
Requirements & support
Questions or issues? Open one on GitHub.
Contributing
Contributions are welcome and appreciated — bug reports, docs, tests, and features alike. See CONTRIBUTING.md to get set up in a couple of minutes, and please review our Code of Conduct. Found a security issue? See SECURITY.md.
License
MIT © Renidly
Metadata
Release files for renidly 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| renidly-0.2.1.tar.gz | 43.8 kB | With provenance | Details for renidly-0.2.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| renidly-0.2.1-py3-none-any.whl | Python 3 | none | any | Details for renidly-0.2.1-py3-none-any.whl |
Total release size: 84.8 kB
Release files / renidly-0.2.1.tar.gz
| Download URL | renidly-0.2.1.tar.gz |
|---|---|
| Size | 43.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bba07063005859d2984bf6859f129b8b23bc184fa8ed339bc8d02a671783d8fd
|
|
BLAKE2b-256 checksum How to use checksums |
820728d95f4a21e25dfd01a113cd6224840e6b6cb9f0ac49a233a6b3f8d3df64
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / renidly-0.2.1-py3-none-any.whl
| Download URL | renidly-0.2.1-py3-none-any.whl |
|---|---|
| Size | 41.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c2fc2e59eb5d904d008055dd08fa8c53092bb0a155e70ff84f1bb5f028d686c
|
|
BLAKE2b-256 checksum How to use checksums |
20f1ee69a6480c6f7f11deb18a4d9c61a43012c99e90e4e150f4a32ea05b6426
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logSecurity for renidly 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for renidly-0.2.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.