Skip to main content

Renidly Python SDK

PyPI version Python versions CI License: MIT Ruff Typed

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 productsdata, 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

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 returns None by default (not an exception). Set raise_on_not_found=True to 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_balance are None for 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 shows meta.credit_consumed == 18.
  • Cached responses are served free: meta.credit_consumed == 0 with the balance unchanged.
  • During auto_paging_iter(), each page is a separate billed request, so each item reflects its own page's meta (walk the items to see the balance step down per page).
  • .last_response remains 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

renidly-0.2.0.tar.gz (42.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

renidly-0.2.0-py3-none-any.whl (40.4 kB view details)

Uploaded Python 3

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

Hashes for renidly-0.2.0.tar.gz
Algorithm Hash digest
SHA256 235cb9c7b06b84e0e816252840d800297b19c0832fbdfca6cafa2a3ee94e5a6a
MD5 22d4d9b2f88f51737ff6c469b4a57f8d
BLAKE2b-256 c2bb92e49a67079c2cb138a4b9e5c76548be57dc7a041e27c0be61d21de963eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for renidly-0.2.0.tar.gz:

Publisher: release.yml on renidly/renidly-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for renidly-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6168071894b44db6488a8a0f1426d2039b1227e99901f6894767bc6435b2064e
MD5 8d6743be45aa6134b6e65aaf0065bd6b
BLAKE2b-256 d1026b86f083518750bbcc96674bc58cf71a568ac83e653823d6905bc0c26484

See more details on using hashes here.

Provenance

The following attestation bundles were made for renidly-0.2.0-py3-none-any.whl:

Publisher: release.yml on renidly/renidly-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page