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 |
Fixed limit (required for enterprise keys). |
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.
# Regular key: the limit is read from your tier and refreshed automatically.
Renidly("rnd-...", config=RenidlyConfig(auto_rate_limit=True))
# Enterprise key: the limit is fixed — supply it.
Renidly("enterprise-...", 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.
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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file renidly-0.2.0.tar.gz.
File metadata
- Download URL: renidly-0.2.0.tar.gz
- Upload date:
- Size: 42.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
235cb9c7b06b84e0e816252840d800297b19c0832fbdfca6cafa2a3ee94e5a6a
|
|
| MD5 |
22d4d9b2f88f51737ff6c469b4a57f8d
|
|
| BLAKE2b-256 |
c2bb92e49a67079c2cb138a4b9e5c76548be57dc7a041e27c0be61d21de963eb
|
Provenance
The following attestation bundles were made for renidly-0.2.0.tar.gz:
Publisher:
release.yml on renidly/renidly-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
renidly-0.2.0.tar.gz -
Subject digest:
235cb9c7b06b84e0e816252840d800297b19c0832fbdfca6cafa2a3ee94e5a6a - Sigstore transparency entry: 2263354943
- Sigstore integration time:
-
Permalink:
renidly/renidly-python@5b8fe155568023f0b3941297d79670aad9f2898c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/renidly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5b8fe155568023f0b3941297d79670aad9f2898c -
Trigger Event:
push
-
Statement type:
File details
Details for the file renidly-0.2.0-py3-none-any.whl.
File metadata
- Download URL: renidly-0.2.0-py3-none-any.whl
- Upload date:
- Size: 40.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6168071894b44db6488a8a0f1426d2039b1227e99901f6894767bc6435b2064e
|
|
| MD5 |
8d6743be45aa6134b6e65aaf0065bd6b
|
|
| BLAKE2b-256 |
d1026b86f083518750bbcc96674bc58cf71a568ac83e653823d6905bc0c26484
|
Provenance
The following attestation bundles were made for renidly-0.2.0-py3-none-any.whl:
Publisher:
release.yml on renidly/renidly-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
renidly-0.2.0-py3-none-any.whl -
Subject digest:
6168071894b44db6488a8a0f1426d2039b1227e99901f6894767bc6435b2064e - Sigstore transparency entry: 2263355048
- Sigstore integration time:
-
Permalink:
renidly/renidly-python@5b8fe155568023f0b3941297d79670aad9f2898c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/renidly
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5b8fe155568023f0b3941297d79670aad9f2898c -
Trigger Event:
push
-
Statement type: