renidly
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
Products
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Use cases
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Docs
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
renidly
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
Products
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Use cases
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Docs
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
renidly
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
Products
Data API
Query the clean B2B graph: people, companies, schools, job changes.
Live API
The freshest profile, company, or job posting, resolved on demand.
Email API
Verify, find, and resolve any work email.
Use cases
Recruiting & HR platforms
Verify candidates in seconds with quality scores and live profile data.
Sales intelligence
Enrich CRM accounts with verified firmographics built for lead scoring.
Market research & analytics
Track 60K+ active job postings, posting velocity, and geographic trends.
Business development
Map stakeholders, org structure, and warm-intro paths to any decision maker.
Outbound & cold email
Find and verify every prospect’s work email before a sequence ever sends.
Inbound lead enrichment
Reverse-lookup each signup into a person and company the moment it lands.
Docs
Documentation
Getting started, core concepts, guides.
Quickstart
First resolution in 5 minutes.
API Reference
Interactive endpoint playground.
Pricing
Sign inStart Free
Blog/Guides
Guides

Profile Lookup API: The Complete Guide to Enrich Professional Data

Nov 2, 202630 min read
Share

Contents

  • Part 1: Understanding the category
  • Lookup, search, enrichment and verification
  • Where profile lookup gets used
  • The three-product shape
  • Part 2: What a profile record contains
  • The full field inventory
  • Identifiers, in depth
  • Part 3: The five input paths
  • Path 1: by stable ID
  • Path 2: by public handle
  • Path 3: from a work email address
  • Path 4: from a name and a company domain
  • Path 5: from a profile URL
  • Choosing a path
  • Part 4: Dataset reads versus live resolution
  • Two modes, same key
  • Choosing per workload
  • Forcing live resolution inside a batch
  • The Live API's deeper person surface
  • Part 5: Responses, errors and the semantics that matter
  • The envelope
  • Not-found is a billed answer
  • What is never charged
  • The full error taxonomy
  • Part 6: Running lookups at scale
  • Batch, not loops
  • Streaming instead of waiting
  • Jobs larger than 1000
  • Rate limiting
  • Pagination, all three models
  • Part 7: Cost, measured rather than estimated
  • The arithmetic
  • Reading cost per call
  • Pricing before you integrate
  • Three habits that control spend
  • Part 8: Designing the storage layer
  • Schema principles
  • Write rules
  • Idempotency
  • Refresh by field, not by calendar
  • Detect change instead of re-resolving
  • Part 9: Operational concerns
  • Never send enriched data to the browser
  • Multi-tenant and per-request overrides
  • Testing without spending credits
  • Monitoring
  • The escape hatch
  • Part 10: Mistakes, evaluation and the honest limits
  • Nine mistakes that show up in production
  • How to evaluate a provider on your own data
  • What profile lookup cannot do
  • Build or buy
  • Frequently asked questions
  • The one-line version

A profile lookup API takes an identifier for a working professional and returns their structured record: current role, employer, full career history, education, skills, certifications and location.

The identifier can be one of several things. A profile handle. An opaque stable ID you stored earlier. A work email address. A name plus a company domain. A profile URL. Different entry points, one resolved record.

That is the simple version. The useful version is everything underneath it: which input path to use when, how dataset reads differ from live resolution, what happens when a lookup finds nothing, how billing actually works, how to run a hundred thousand lookups without breaking your pipeline, and which design decisions quietly destroy a dataset six months after you make them.

This guide covers all of it, with working code in Python and Node throughout.


Part 1: Understanding the category

Lookup, search, enrichment and verification

Four words that get used interchangeably and mean four different operations. Getting them straight saves you from buying the wrong thing.

Lookup resolves a known identifier into one record. You know who you want and you hold something that points at them. One input, one output, deterministic.

Search finds people matching criteria. You do not know who you want; you know what they look like. Criteria in, a paginated set out.

Enrichment is the umbrella term covering both, usually including companies as well as people.

Verification confirms that a specific attribute is currently true, most commonly whether an email address can receive mail. It answers a narrower question than lookup and the two are frequently needed together.

This guide is about lookup. For criteria-based querying see the people search API guide; for the category overview see the B2B data enrichment API guide.

Where profile lookup gets used

Five patterns cover almost every real integration.

Inbound enrichment. Someone submits a work email on a signup or demo form. You resolve it before they finish the flow, so routing, scoring and field-skipping all happen automatically. The architecture, including why you must never block form submission on a third-party call, is in email enrichment at signup.

CRM hydration and refresh. Existing records with gaps, resolved in bulk, then refreshed on a schedule. The governance side of this is in CRM enrichment patterns.

Candidate and talent workflows. Recruiting platforms resolving candidates to confirm current employment before outreach. Covered in verified candidate data.

Sales research automation. A rep opens an account and the system resolves the relevant people in real time, so the brief is current rather than cached from whenever the record was created.

Product features. Any application where a user pastes a profile link or an email and expects structured data back. Here latency is user-visible and the design constraints are different.

The three-product shape

Renidly splits professional data across three products that share one API key, one response envelope and one credit balance, which means mixing them is a per-call decision rather than a second integration.

ProductBase pathUse for
Data API/api/data/v1Clean, deduplicated, queryable records by stable ID or rich filters; includes async batch
Live API/api/v2Freshest available snapshot of one subject, resolved on demand
Email API/api/emails/v1Verify deliverability, find addresses, reverse-resolve the person behind an address

Authentication is one header on every request across all three:

text
1X-renidly-apikey: <your-api-key>

Account and credit routes are the one exception: they live under /api/panel, use the header name X-AUTHAPI-Key with the same key value, and require a trailing slash on their /k/ routes.


Part 2: What a profile record contains

The full field inventory

A complete record from /people/profile carries considerably more than most integrations use.

GroupFields
IdentifiersOpaque prsn_ ID, public handle, profile URL
NameFull name, first name, last name, localized name variants
HeadlineHeadline, summary
Current positionTitle, organization name, organization ID, organization slug, organization URL, start date
Position historyEvery past position with organization, title, dates and description
EducationInstitutions, degrees, fields of study, dates
CapabilitySkills, certifications, certification authorities, languages
Work artifactsProjects, publications
GeographyCity, country, structured location
Social signalsFollower count, connection count
Flagsis_creator, is_premium, is_open_to_work, is_hiring

Three observations that matter when you design around this.

Position history is append-only. Past positions do not change. Once you have resolved someone's history to 2019, that portion of the record is permanently correct and never needs refreshing. Only the current position is volatile. This single fact should shape your refresh strategy, and most teams miss it and re-pull entire records.

Organization references carry identifiers. Each position includes the organization's ID and slug, not just its name. That means a profile lookup also gives you the key for company lookups, so you can resolve an employer without a separate name-matching step.

Flags are cheap signals. is_open_to_work and is_hiring are booleans sitting in a record you already paid for. For recruiting and for trigger-based sales they are free signal that most integrations ignore.

Identifiers, in depth

This is the most important technical section in the guide, because the decision you make here is the one that breaks pipelines a year later.

Renidly uses opaque, prefixed, stable identifiers:

PrefixSubject
prsn_Person
org_Organization
inst_Educational institution
skl_Skill

Example: prsn_06d0d44dogo2m, org_5ehe8408s7tme.

They are deliberately meaningless. You cannot parse them, derive anything from them, enumerate them or construct one. The golden rule across the entire platform is that identifiers and cursors are opaque: pass them back exactly as received, never build or mutate them.

Why this matters more than it sounds. The alternative is to key your records on a public handle or slug. Both are human-readable, both can change, and when they change nothing in your system errors. Your next refresh returns a not-found, and if your integration treats not-found as "no data available," you write null over a populated record. You discover it during an audit, by which point an unknown number of good records have been overwritten.

The correct pattern, always:

python
1from renidly import Renidly
2
3renidly = Renidly()  # reads RENIDLY_API_KEY from the environment
4
5# First contact: resolve whatever identifier you were given
6person = renidly.data.people.retrieve(handle="ryanroslansky")
7
8if person:
9    store_person_id(internal_user_id, person.id)   # persist prsn_…, not the handle

If your stack is Node, the same call and the same discipline:

ts
1import { Renidly } from "renidly";
2
3const renidly = new Renidly();
4
5const person = await renidly.data.people.retrieve({ handle: "ryanroslansky" });
6
7if (person) {
8  await storePersonId(internalUserId, person.id);
9}

Every refresh after that uses the ID. The handle becomes display text that you update when it changes, not a key you depend on.

The same rule applies to the Live API, which uses its own token types: people have an entityId, organizations have a numeric id, opportunities have an opportunityEntityId. Resolve a handle or slug to its stable token once, cache it, and reuse it. The background reasoning on identity and why names fail as keys is in what B2B identity resolution actually is.


Part 3: The five input paths

Path 1: by stable ID

The fastest and most reliable path, and the one you should be on for every repeat lookup.

python
1person = renidly.data.people.retrieve(id="prsn_06d0d44dogo2m")
ts
1const person = await renidly.data.people.retrieve({ id: "prsn_06d0d44dogo2m" });

No ambiguity, no matching, no name collisions. If you have the ID, use it.

Path 2: by public handle

The entry point when someone gives you a profile link or handle.

python
1person = renidly.data.people.retrieve(handle="ryanroslansky")
ts
1const person = await renidly.data.people.retrieve({ handle: "ryanroslansky" });

Either id or handle is required and the ID takes precedence if both are supplied. Treat this as a one-time resolution: convert to the ID and store it.

Path 3: from a work email address

The highest-leverage path in B2B, because an email is the one identifier you get free from almost every inbound touchpoint.

python
1result = renidly.emails.reverse("[email protected]")
2
3if result.found and result.confidence in ("high", "medium"):
4    person  = result.person
5    company = result.current_company
ts
1const result = await renidly.emails.reverse("[email protected]");
2
3if (result.found && ["high", "medium"].includes(result.confidence)) {
4  const { person, currentCompany } = result;
5}

Three behaviors to design around.

Confidence is four-valued, not boolean: high, medium, low, none. Route on it rather than treating every match as equal. A high match can write straight to a record; a low match belongs in a review queue.

Person and company are independently nullable. A resolution that identifies the company but not the individual is still useful for routing and scoring. Handle partial results rather than discarding them.

Unsupported addresses are rejected for free. Public mailbox providers, role accounts, disposable addresses and relay services return 422 with a validation error before any work runs, and nothing is charged. That means you do not need a blocklist to control spend; send the list and let the API classify. The full model is in reverse email lookup.

Path 4: from a name and a company domain

The weakest input because names are not unique, but frequently the only one available.

python
1found = renidly.emails.find(
2    first_name="Jane",
3    last_name="Doe",
4    domain="northwind-logistics.com",   # bare hostname, no scheme, no @
5)
ts
1const found = await renidly.emails.find({
2  firstName: "Jane",
3  lastName: "Doe",
4  domain: "northwind-logistics.com",
5});

The domain parameter is strict: a bare hostname containing a dot. Passing https://northwind-logistics.com or [email protected] returns a 400 validation error naming the offending field in errors. User-entered company fields are full of full URLs, so normalize before the call rather than handling the error after. The comparison of all the methods for this path is in how to find a work email address.

Path 5: from a profile URL

When you hold a link rather than a handle.

python
1found = renidly.emails.find_by_url("https://example.com/in/jane-doe")
ts
1const found = await renidly.emails.findByUrl("https://example.com/in/jane-doe");

Returns the address plus the resolved first name, last name, domain and company, which is useful when you need both the identity and a contactable address in one step.

Choosing a path

You holdUseReliability
prsn_ IDdata.people.retrieve(id=…)Exact
Public handledata.people.retrieve(handle=…)Exact, then store the ID
Work emailemails.reverse(…)High, with graded confidence
Profile URLemails.find_by_url(…)High
Name + domainemails.find(…)Moderate, names collide
Criteria onlydata.people.search(…)Not a lookup; see people search

Part 4: Dataset reads versus live resolution

This is the architectural decision that most affects cost and accuracy, and almost nobody evaluates it deliberately.

Two modes, same key

Dataset read queries a clean, deduplicated, queryable set of records. Fast, consistent, and the right default for bulk work, analytics and anything processing thousands of records where small staleness is acceptable. This is the Data API.

Live resolution resolves one subject on demand and returns the freshest available snapshot. The right choice when a human is waiting, when the answer feeds an expensive decision, or when you specifically need current state.

python
1# Bulk: dataset read, tuned for throughput
2for row in renidly.data.people.search(
3    organization_slugs="northwind-logistics",
4    current_only=True,
5).auto_paging_iter():
6    upsert(row)
7
8# Interactive: a rep just opened this account
9snapshot = renidly.live.people.enrich(handle="jane-doe")
ts
1for await (const row of renidly.data.people.search({
2  organization_slugs: "northwind-logistics",
3  current_only: true,
4})) {
5  await upsert(row);
6}
7
8const snapshot = await renidly.live.people.enrich({ handle: "jane-doe" });

One naming detail that catches everyone on day one: SDK method names follow the language convention, so snake_case in Python and camelCase in Node. But filter parameters keep the names the API defines. Data API filters stay snake_case in both languages, and Live API filters stay camelCase in both. So it is data.people.search({ current_only: true }) in Node, not currentOnly. This is deliberate and consistent once you know it.

Choosing per workload

SituationModeWhy
Hydrating 100,000 cold recordsDatasetCost, throughput, staleness acceptable
Rep opens a key accountLiveAccuracy matters, volume is low
Nightly refresh of a long tailDatasetBulk economics
Pre-outreach check on 50 targetsLiveA wrong employer costs the conversation
Analytics over a market segmentDatasetConsistency beats freshness
User pastes a link in your productLiveUser-visible, expects current

A pattern worth adopting: split the run. Standard dataset reads for the bulk, live resolution for the subset that matters. Your target account list gets the fresh treatment; the long tail does not.

Forcing live resolution inside a batch

Batch enrichment accepts a flag that forces the freshest per-item resolution rather than the standard dataset read:

python
1job = renidly.data.people.enrich_batch(handles=chunk, live=True)
ts
1const job = await renidly.data.people.enrichBatch({ handles: chunk, live: true });

Expect it to cost like a live resolution and take a little longer per item. That is the trade and it is the right trade for a small high-value cohort, the wrong one for a first-pass hydration of a hundred thousand records.

The Live API's deeper person surface

Live resolution is not only a fresher version of the same record. It exposes endpoints the dataset does not:

MethodReturns
live.people.enrich(entity_id=…, handle=…)Full current profile
live.people.resolve_handle(handle)Handle to stable entityId
live.people.employment_history(entity_id)Complete work history with parsed dates and parallel-position groupings
live.people.endorsements(entity_id)Recommendations written for the person, with author details
live.people.lookalikes(entity_id)Similar professional profiles
live.people.interests(entity_id)Entities the person follows

Two of these are genuinely underused.

lookalikes expands a shortlist from one ideal example. If you have a customer who fits perfectly, this finds comparable profiles without you having to articulate why they fit as a filter expression. That is a different and often better way to build a segment than guessing at criteria.

endorsements returns recommendations written for the person along with who wrote them. Beyond trust signals, the author list is a map of people who worked closely enough with your target to vouch for them, which is a warm-intro path you cannot get any other way.

python
1eid = renidly.live.people.resolve_handle("jane-doe")   # resolve once, reuse
2history = renidly.live.people.employment_history(eid)
3similar = renidly.live.people.lookalikes(eid)
ts
1const eid = await renidly.live.people.resolveHandle("jane-doe");
2const history = await renidly.live.people.employmentHistory(eid);
3const similar = await renidly.live.people.lookalikes(eid);

Resolve the handle to an entityId once and cache it. Handles change; entity IDs do not.


Part 5: Responses, errors and the semantics that matter

The envelope

Every endpoint across all three products returns the same shape, success or failure:

json
1{
2  "success": true,
3  "statusCode": 200,
4  "message": "Profile retrieved successfully",
5  "errors": null,
6  "data": { }
7}
json
1{
2  "success": false,
3  "statusCode": 400,
4  "message": "Validation failed",
5  "error_code": "VALIDATION_ERROR",
6  "errors": { "domain": "must be a bare hostname" },
7  "data": null
8}

Branch on success, never on HTTP status alone. This is a golden rule and the next section explains exactly why.

Not-found is a billed answer

The single most misunderstood behavior in the category.

A lookup that runs and resolves nothing returns HTTP 200, not 404, with success: false and a not-found error_code:

CodeSubject
1010Person not found
1020Organization not found
1030Skill not found
1040Institution not found

It is billed. A real lookup ran. Producing a definitive "this does not resolve" takes the same work as producing a record, which is precisely why the status is 200: the request succeeded, and the answer happens to be empty.

Two consequences you must design around.

A client checking only HTTP status sees success. It will then treat the empty payload as data and write nulls over populated fields. This is the single most common integration bug in enrichment, and it is silent.

Your budget is driven by submissions, not results. If you send 10,000 identifiers and 60% resolve, you pay for 10,000 lookups and receive 6,000 usable records. Effective cost per usable record is roughly 1.7 times the headline rate.

The SDKs make the safe path the default: retrieve() returns None in Python and null in Node when nothing resolved, rather than raising. If you would rather it raise, set raise_on_not_found (Python) or throwOnNotFound (Node) on the client config.

What is never charged

Not everything costs you. The rules worth knowing:

  • Validation failures (400)
  • Missing or invalid key (401, 403)
  • Rate-limit rejections (429)
  • Insufficient credits (402)
  • Transient server errors (500, 503), which are retried server-side
  • Semantically unsupported input on email reverse (422), rejected before any work runs
  • Cache hits, which are automatic, transparent and free

That last one deserves emphasis. Identical repeat requests are served from a short-lived cache at no cost and near-zero latency. You do not opt in and you do not choose; it happens per call. A cached response reports 0 in its billing metadata.

The full error taxonomy

Every failure raises a specific subclass of RenidlyError, and each implies a different response:

ErrorMeaningRetry?
AuthenticationErrorMissing or invalid keyNo, fix configuration
PermissionDeniedErrorValid key, not allowed here: premium-gated or opted-outNever, suppress permanently
InvalidRequestErrorBad input; read field_errors / fieldErrorsNo, fix the input
InsufficientCreditsErrorBalance exhaustedNo, alert operations
NotFoundErrorBatch job not found or expiredNo, resubmit that chunk
RateLimitErrorPer-minute ceiling hit; carries tier, limit, retry_afterYes, with backoff
ServiceUnavailableErrorTransientYes, with backoff
APIConnectionErrorNetwork or timeoutYes, then fall back

A complete handler:

python
1from renidly import (
2    Renidly, RenidlyConfig,
3    AuthenticationError, PermissionDeniedError, InvalidRequestError,
4    InsufficientCreditsError, NotFoundError, RateLimitError,
5    ServiceUnavailableError, APIConnectionError, RenidlyError,
6)
7
8renidly = Renidly(config=RenidlyConfig(timeout=15, max_retries=3, auto_rate_limit=True))
9
10
11def resolve(identifier: str) -> dict:
12    try:
13        person = renidly.data.people.retrieve(id=identifier)
14    except InvalidRequestError as e:
15        return {"status": "invalid", "fields": e.field_errors}
16    except PermissionDeniedError:
17        suppress_permanently(identifier)          # opted out: never retry
18        return {"status": "excluded"}
19    except InsufficientCreditsError:
20        alert_ops("renidly balance exhausted")
21        return {"status": "degraded"}
22    except RateLimitError as e:
23        return {"status": "retry", "after": e.retry_after}
24    except (ServiceUnavailableError, APIConnectionError):
25        return {"status": "retry", "after": 5}
26    except RenidlyError as e:
27        log.warning("lookup failed", code=e.error_code, msg=e.message)
28        return {"status": "error"}
29
30    if person is None:
31        return {"status": "unresolved"}           # billed, but no record
32
33    return {"status": "resolved", "person": person,
34            "cost": person.meta.credit_consumed}

The Node equivalent, with the same branch order, because the order matters: specific errors before the generic base class.

ts
1import {
2  Renidly, InvalidRequestError, PermissionDeniedError,
3  InsufficientCreditsError, RateLimitError,
4  ServiceUnavailableError, APIConnectionError, RenidlyError,
5} from "renidly";
6
7const renidly = new Renidly(undefined, { timeout: 15_000, maxRetries: 3, autoRateLimit: true });
8
9export async function resolve(identifier: string) {
10  let person;
11  try {
12    person = await renidly.data.people.retrieve({ id: identifier });
13  } catch (e) {
14    if (e instanceof InvalidRequestError) return { status: "invalid", fields: e.fieldErrors };
15    if (e instanceof PermissionDeniedError) {
16      await suppressPermanently(identifier);
17      return { status: "excluded" };
18    }
19    if (e instanceof InsufficientCreditsError) {
20      await alertOps("renidly balance exhausted");
21      return { status: "degraded" };
22    }
23    if (e instanceof RateLimitError) return { status: "retry", after: e.retryAfter };
24    if (e instanceof ServiceUnavailableError || e instanceof APIConnectionError) {
25      return { status: "retry", after: 5 };
26    }
27    if (e instanceof RenidlyError) return { status: "error", code: e.errorCode };
28    throw e;
29  }
30
31  if (!person) return { status: "unresolved" };
32
33  return { status: "resolved", person, cost: person.meta.creditConsumed };
34}

PermissionDeniedError is the one teams handle wrong. Individuals can opt out of resolution, and that is a permanent state rather than a transient failure. If it falls into a generic retry path, every batch run you ever execute will re-request the same records forever, at cost, for a guaranteed nothing. Build the suppression list on first occurrence and check it before submitting.


Part 6: Running lookups at scale

Batch, not loops

The first version of every bulk job is a loop, and it fails in a predictable order: slow, then rate-limited, then someone adds threads, then rate-limited harder, then the process dies at record 60,000 and nobody knows which 60,000 completed.

Batch is a different execution model. Submit up to 1000 identifiers in one request, receive a 202 with a job handle immediately, and the work happens in the background at a controlled pace.

python
1job = renidly.data.people.enrich_batch(
2    ids=[r.person_id for r in rows],
3)
4
5result = job.wait(on_progress=lambda n: print("resolved", n))
6
7print(result.status, result.resolved, "/", result.total)
8print("misses:", result.not_found)
9
10for row in result.results:
11    upsert(row.matched_input, row)
ts
1const job = await renidly.data.people.enrichBatch({ ids: rows.map(r => r.personId) });
2
3const result = await job.wait({ onProgress: (n) => console.log("resolved", n) });
4
5console.log(result.status, result.resolved, "/", result.total);
6console.log("misses:", result.notFound);
7
8for (const row of result.results) await upsert(row.matched_input, row);

Four properties fall out of this, each fixing a specific failure of the loop:

PropertyWhat it fixes
One request per 1000 itemsYou are not generating 1000 requests to be rate limited
A persistable job handleThe job survives your process dying
matched_input on every rowYou never guess which output belongs to which input
Balance pre-checked at submitYou learn you cannot afford it before the work starts, not at item 700
// Try it yourself

Stop stitching vendors together.

One endpoint resolves any email, domain, company or profile. Start with 100 free credits — no card required.

Get 100 free credits

Submission itself is free. Nothing is charged at submit; items bill on definitive result, and not-founds come back in a dedicated not_found list.

Streaming instead of waiting

job.wait() blocks until everything completes. For a large job feeding a database you usually want to persist as results land, so a crash at 80% does not discard 80% of the work.

python
1for row in renidly.data.people.enrich_batch(ids=chunk).stream():
2    upsert(row.matched_input, row)
ts
1const job = await renidly.data.people.enrichBatch({ ids: chunk });
2
3for await (const row of job.stream()) {
4  await upsert(row.matched_input, row);
5}

Stream when the result set is large, when you are writing to a database anyway, or when partial results are usable before the job finishes. Wait when the job is small and you need the complete set before acting.

Jobs larger than 1000

The cap is 1000 items per job, so a real backfill is a sequence of jobs. Track which chunks are done so a restart does not redo finished work.

python
1CHUNK = 1000
2
3def run_backfill(ids: list[str], run_id: str) -> None:
4    chunks = [ids[i:i + CHUNK] for i in range(0, len(ids), CHUNK)]
5
6    for idx, chunk in enumerate(chunks):
7        if chunk_done(run_id, idx):
8            continue
9
10        try:
11            job = renidly.data.people.enrich_batch(ids=chunk)
12        except InsufficientCreditsError:
13            alert_ops("backfill paused: balance exhausted")
14            return
15
16        save_job_id(run_id, idx, job.id)      # keep the id for your own audit trail
17
18        for row in job.stream():              # persist as results land
19            upsert(row.matched_input, row)
20
21        mark_chunk_done(run_id, idx)
ts
1const CHUNK = 1000;
2
3export async function runBackfill(ids: string[], runId: string) {
4  for (let idx = 0; idx * CHUNK < ids.length; idx++) {
5    if (await chunkDone(runId, idx)) continue;
6
7    const chunk = ids.slice(idx * CHUNK, (idx + 1) * CHUNK);
8    const job = await renidly.data.people.enrichBatch({ ids: chunk });
9
10    await saveJobId(runId, idx, job.id);
11
12    for await (const row of job.stream()) {
13      await upsert(row.matched_input, row);
14    }
15
16    await markChunkDone(runId, idx);
17  }
18}

Streaming rather than waiting is deliberate here. Results persist as they land, so a failure partway through a chunk costs you that chunk rather than the whole run, and chunk_done means the next attempt starts where you stopped.

Resubmitting a chunk you already completed is the most expensive mistake in this guide. It doubles the cost for zero additional data, which is the whole reason to track completion per chunk rather than per run.

Save the job_id even though stream() does not need it. Jobs are pollable until they expire, so having the id lets you check on a job out of band, reconcile a cost question, or inspect one that behaved oddly. A NotFoundError on a job id means it expired before anyone collected it, so keep your runs tighter than the expiry window.

Rate limiting

Rate limits are per-minute and tier-based. Discovering yours through 429 responses in production is avoidable.

python
1renidly = Renidly(config=RenidlyConfig(
2    auto_rate_limit=True,
3    rate_limit_safety=0.7,     # stay at 70% of the ceiling
4    max_retries=3,
5))
ts
1const renidly = new Renidly(undefined, {
2  autoRateLimit: true,
3  rateLimitSafety: 0.7,
4  maxRetries: 3,
5});

auto_rate_limit holds you under the limit using a sliding 60-second window. Regular keys read the ceiling from your tier automatically; enterprise keys require an explicit rate_limit_per_minute because the limit is negotiated rather than tiered.

rate_limit_safety below 1.0 is the parameter that prevents a backfill from taking down your production path. Running bulk work at 100% of your ceiling on the same key that serves your signup flow is a self-inflicted outage.

Pagination, all three models

Three models appear across the platform, and copying a helper from one to another produces bugs.

Cursor / keyset. People search, company search, company employees, email prospects, batch tracking. Omit the cursor for the first page, pass next_cursor back verbatim, an empty or absent cursor means the end.

python
1for person in renidly.data.people.search(title="engineering", limit=50).auto_paging_iter():
2    store(person.id)
ts
1for await (const person of renidly.data.people.search({ title: "engineering", limit: 50 })) {
2  await store(person.id);
3}

Page-based (page, limit). Institutions search, skills search, alumni, job-changes search. The response carries has_more.

Offset-based (start, count). Most Live API list endpoints. start is a zero-based offset, and some endpoints constrain it: activity reactions require start to be a multiple of 10, and anything else returns 400.

In Node the paginated result is a hybrid that is both awaitable and async-iterable, so await it for one page or for await over every page with no extra plumbing.

Each page is a separately billed request. An unbounded walk over a broad filter will page through a very large result set and charge for every page. Bound your loops explicitly.


Part 7: Cost, measured rather than estimated

The arithmetic

Per-endpoint pricing, per successful call. Simple reference lookups are cheap; richer resolutions cost more.

The formula that actually determines your spend:

text
1cost            = records submitted × rate per lookup
2usable records  = records submitted × resolution rate
3effective cost  = cost ÷ usable records

Because misses are billed, your resolution rate is the variable that matters, and it is specific to your data. A list of last quarter's inbound behaves nothing like a trade-show list from 2019. Measure it on a 500-record sample before sizing anything:

python
1sample = ids[:500]
2probe = renidly.data.people.enrich_batch(ids=sample).wait()
3
4rate = probe.resolved / probe.total
5print(f"resolution rate: {rate:.1%}")
6print("projected full-run cost:",
7      len(ids) * probe.meta.credit_consumed / probe.total)

Reading cost per call

Every SDK response carries a .meta object with the billing and HTTP metadata for the call that produced it. It sits outside the response data, so there is no field collision between your data and your telemetry.

.meta fieldPythonNode
Credits charged for this requestmeta.credit_consumedmeta.creditConsumed
Balance after the chargemeta.remaining_balancemeta.remainingBalance
HTTP statusmeta.status_codemeta.statusCode
Request idmeta.request_idmeta.requestId
Response headersmeta.headersmeta.headers
Parsed envelopemeta.bodymeta.body
python
1p = renidly.data.people.retrieve(handle="ryanroslansky")
2print(p.meta.credit_consumed, p.meta.remaining_balance)   # e.g. 1.0 19813.0
ts
1const p = await renidly.data.people.retrieve({ handle: "ryanroslansky" });
2console.log(p.meta.creditConsumed, p.meta.remainingBalance);

Details worth knowing:

  • .meta is on every result: single objects, list pages, and each item within a page. In Node it is non-enumerable, so it will not appear in JSON.stringify or Object.keys.
  • Values are None / undefined when a route is not credit-billed, such as account routes, or when nothing was charged: errors, cached hits, zero-result billing.
  • Result-billed endpoints report the real dynamic amount. A prospects call returning 18 addresses reports 18.
  • During auto-pagination each page is a separate billed request, so each item reflects its own page's cost.

Pricing before you integrate

Per-route costs are published at a public endpoint requiring no key, which means you can price an integration before you sign up for one:

text
1GET https://renidly.com/api/panel/credits/routes/costs/

It lists only routes whose cost differs from 1, so any route not listed costs 1 credit, and credits_cost: 0 means free. Batch and per-item endpoints show a base of 0 and bill per resolved item.

Through the SDK:

python
1costs   = renidly.account.route_costs()
2balance = renidly.account.balance()
3tier    = renidly.account.tier()
ts
1const costs   = await renidly.account.routeCosts();
2const balance = await renidly.account.balance();
3const tier    = await renidly.account.tier();

account.tier() returns your current tier name, its per-minute limit, your balance, and how many credits the next tier needs. Current rates and volume terms are on the pricing page.

Three habits that control spend

Do not enrich everything. Most databases contain a large tail of records nobody will ever query. Prioritize by business value times staleness, refresh within a fixed budget, and let the tail go.

Put a ceiling in the runner. Decide the maximum a job may spend and stop when it hits that, regardless of how many records remain. A runaway loop is the classic way to turn a routine backfill into an awkward conversation with finance, and a ceiling costs six lines.

python
1BUDGET, spent = 50_000, 0
2
3for idx, chunk in enumerate(chunks):
4    if spent >= BUDGET:
5        log.warning("budget ceiling reached at chunk %s", idx)
6        break
7    result = renidly.data.people.enrich_batch(ids=chunk).wait()
8    spent += result.meta.credit_consumed

Alert on balance. remaining_balance under a floor should page someone. A backfill draining the pool your production signup path depends on is an incident, and it happens to most teams once.


Part 8: Designing the storage layer

The API calls are the easy part. How you store what comes back decides whether the system is still useful in a year.

Schema principles

Key on the opaque ID. Covered already, and worth repeating because it is the mistake with the longest tail.

Store provenance per field, not per record. Three columns alongside each enriched value: source, confidence, and verification timestamp. Without them you cannot prioritize a refresh, cannot audit a bad decision, and cannot tell a user which fields to trust. Most databases do not give you this for free, so it usually means companion columns or a parallel table.

Separate enriched values from human-entered ones. An enriched_title column next to title lets you compare, audit and roll back. Writing enrichment straight into the field a human edits means you can never tell what came from where.

Keep the raw response. Store the full payload alongside the parsed fields. Schemas evolve, you will want to extract something you did not originally parse, and reprocessing stored responses is free while re-fetching is not.

Write rules

The rules that stop automated enrichment from degrading data a human entered:

python
1def should_write(field, current, incoming, confidence) -> bool:
2    if not incoming:                    return False   # never overwrite with nothing
3    if field in HUMAN_OWNED_FIELDS:     return False   # notes, owner, stage
4    if human_edited(field):             return confidence == "high"
5    if not current:                     return confidence in ("high", "medium")
6    if current == incoming:             return False   # no-op, do not churn the log
7    return confidence == "high"                        # replacing good data needs certainty
ts
1function shouldWrite(
2  field: string,
3  current: string | null,
4  incoming: string | null,
5  confidence: "high" | "medium" | "low" | "none",
6): boolean {
7  if (!incoming) return false;
8  if (HUMAN_OWNED_FIELDS.has(field)) return false;
9  if (humanEdited(field)) return confidence === "high";
10  if (!current) return confidence === "high" || confidence === "medium";
11  if (current === incoming) return false;
12  return confidence === "high";
13}

The asymmetry is the point. Filling an empty field is low risk, so medium confidence passes. Replacing a value someone may already have acted on requires certainty.

Idempotency

Queues retry. Your enrichment worker will run twice on the same record, and without a claim pattern you get duplicate writes, doubled cost tracking, and records that flip between states.

python
1def enrich_once(record_id: str, identifier: str) -> None:
2    key = f"enrich:{record_id}:{identifier}"
3
4    if not claim(key, ttl_hours=24):
5        return                                  # already done or in flight
6
7    try:
8        person = renidly.data.people.retrieve(id=identifier)
9    except (ServiceUnavailableError, APIConnectionError):
10        release(key)                            # transient: allow a retry
11        raise
12    except PermissionDeniedError:
13        mark_excluded(record_id)                # permanent: hold the claim
14        return
15
16    write_fields(record_id, person)
17    mark_complete(key)

Release the claim on transient failures so a retry can proceed. Hold it on permanent ones like opt-outs and validation errors, because those will not succeed on a second attempt and retrying them is spend with a guaranteed zero return.

Refresh by field, not by calendar

Position history is append-only and never needs refreshing. Current title and employer die on a job change. Education and certifications are effectively permanent.

Field groupRefresh
Current title, employerBefore outreach, and on a short cycle
Work emailBefore outreach
Skills, certificationsAnnually
Education, position historyNever
LocationOn a long cycle

Refreshing everything on one annual cycle means you simultaneously over-refresh the stable fields and under-refresh the volatile ones. The full field-level decay model is in CRM data decay.

Detect change instead of re-resolving

The biggest structural lever available, and the one most teams never build.

Re-enriching asks "what is true now?" Watching for movement asks "did anything happen?" The second is far cheaper at scale, because job changes drive most of the decay in a professional dataset.

Instead of refreshing 50,000 records to find the 4,000 that changed, watch for the 4,000 events and refresh only those:

python
1moves = renidly.data.job_changes.search(
2    event_type="joined",
3    organization_ids="org_5ehe8408s7tme,org_06d0d44dogo2m",
4    days_ago=7,
5    limit=50,
6)
ts
1const moves = await renidly.data.jobChanges.search({
2  event_type: "joined",
3  organization_ids: "org_5ehe8408s7tme,org_06d0d44dogo2m",
4  days_ago: 7,
5  limit: 50,
6});

Three event types are available: joined, left and title_change. The implementation detail that breaks every first version is deduplication, because the endpoint returns a recency window rather than a changelog, so a daily poll with a 30-day window returns the same people every day. Keep a seen-set with a TTL longer than your widest window. Full pattern in job change signals.

Note that this endpoint takes organization IDs (org_…) while people search takes organization slugs. They are not interchangeable.


Part 9: Operational concerns

Never send enriched data to the browser

The security mistake that turns your product into someone else's free API.

If your signup or lookup endpoint returns the enriched record to the client, anyone can submit arbitrary identifiers to your form, watch the network tab, and read resolved person and company data back. You are now paying to serve enrichment to anyone who finds it, and exposing data about people who never interacted with your product.

python
1# Wrong: leaks the full record
2return {"account_id": account_id, "enrichment": result.person}
3
4# Right: return only the decision
5return {"account_id": account_id, "next": "sales_handoff"}

If the UI genuinely needs to display something, return that one field and nothing else. A prefilled company name is a nice touch. Returning headcount, industry, full employment history and a profile picture is a data exposure with a nice touch attached.

Related: never put the API key in client-side code. The SDK reads RENIDLY_API_KEY from the environment for exactly this reason.

Multi-tenant and per-request overrides

If you serve multiple customers with separate keys, you do not need a client per tenant. Pass per-request options as the last argument:

python
1person = renidly.data.people.retrieve(
2    id="prsn_06d0d44dogo2m",
3    options={"api_key": tenant_key, "timeout": 5},
4)
ts
1const person = await renidly.data.people.retrieve(
2  { id: "prsn_06d0d44dogo2m" },
3  { apiKey: tenantKey, timeout: 5000 },
4);

Testing without spending credits

Your test suite should not make network calls. Stub at the SDK boundary and assert on your branching logic, which is where the bugs live.

Cases worth explicit tests:

  • Full record resolved
  • None / null returned (not-found, billed)
  • Partial result: company present, person null
  • InvalidRequestError with populated field_errors
  • PermissionDeniedError for an opted-out identifier
  • RateLimitError with retry_after set
  • APIConnectionError on timeout
  • A batch result containing both results and a populated not_found

The partial and not-found cases are the ones teams skip and the ones that break in production. Code assuming both person and company are present will throw on a null attribute access at 2am.

For a smoke test against the real API, use the health probe, which needs no key and costs nothing, rather than a billed lookup in CI that runs hundreds of times a day.

Monitoring

Six metrics. Emit them from the first call, because reconstructing this later from invoices is miserable.

python
1metrics.increment("lookup.attempted")
2metrics.increment(f"lookup.outcome.{outcome}")           # resolved / unresolved / error
3metrics.increment(f"lookup.confidence.{confidence}")
4metrics.gauge("lookup.credits", result.meta.credit_consumed)
5metrics.gauge("lookup.balance", result.meta.remaining_balance)
6metrics.timing("lookup.latency_ms", elapsed_ms)

Two alerts worth having on day one. Balance floor, because a backfill draining the production pool is an incident. Resolution rate drop, because a sharp week-over-week fall means something changed: your traffic mix, a form change, or a bug in your pre-filter. You want that from a dashboard rather than from a colleague complaining about empty records.

The escape hatch

For an endpoint not yet surfaced as an SDK method, call it directly rather than guessing at a method name:

python
1renidly.raw_request("GET", "/people/search", service="data", params={...})
ts
1await renidly.rawRequest("GET", "/people/search", { service: "data", params: {...} });

This is also the right tool when you are on an older SDK version and need something newer.


Part 10: Mistakes, evaluation and the honest limits

Nine mistakes that show up in production

Keying on handles or slugs. The longest-tail mistake. Fails silently, overwrites good data, discovered during an audit.

Checking HTTP status instead of the response body. A not-found returns 200. A client branching on status writes nulls.

Looping single calls over a list. Slower, rate-limited, no resumability, and it generates traffic that a single batch request would not.

Resubmitting batch chunks after a crash. Persist the job handle before you wait. Resubmitting doubles the cost for zero extra data.

Retrying opt-outs forever. PermissionDeniedError is permanent. Suppress on first occurrence.

Budgeting on results rather than submissions. Misses are billed. At a 60% resolution rate your effective cost is 1.7 times the headline.

Refreshing everything on one cycle. Position history never changes; current employer dies on a job change. Refresh by field.

Returning enriched records to the browser. You have built a free public API.

Unbounded auto-pagination. Every page is billed. Bound the loop.

How to evaluate a provider on your own data

If you are choosing between providers rather than implementing against one, the method matters more than the feature list.

Test on your own records, never a vendor sample. Every sample is drawn from where that vendor is strongest. A provider with excellent coverage of US software companies and thin coverage of European industrials will send you US software companies.

Build a golden set. Take 100 records and manually establish the truth: actual current title, actual employer. It takes a day and it converts your evaluation from an opinion into a measurement. Without it you can only compare providers against each other, and two providers agreeing on a wrong answer looks identical to two agreeing on a right one.

Include deliberately hard cases. Recent job changers, people at acquired companies, common names, non-Latin-script names, small companies. If your test set is all senior people at well-known companies, every provider scores well and you learn nothing about the hard third of your database.

Submit records that should fail. Fabricated identifiers, people who do not exist. If a provider returns a confident-looking record, every wrong answer it ever produces will reach your database wearing the same clothes as a right one.

Check whether confidence is calibrated. Measure accuracy on just the high-confidence subset. If it is not meaningfully better than the overall rate, the score is decoration and you cannot build routing on it.

Test freshness retrospectively. Take twenty people you personally know changed roles in the last ninety days and count how many the provider has in the new role. Small sample, direct evidence, one afternoon, and it measures detection lag rather than coverage claims.

Compute effective cost per usable record. Total spend divided by records that were both matched and correct. A provider at half the rate with two thirds the accuracy is more expensive, and it hands you confidently wrong records your team acts on.

The full framework is in how to evaluate a B2B data provider.

What profile lookup cannot do

Worth stating plainly, because overestimating it leads to bad design.

It cannot confirm someone still works somewhere in real time. Every provider has detection lag. A record is accurate as of when it was last resolved, and "no change detected" means "we have not detected a change," which is a weaker claim than "nothing changed."

It cannot resolve everyone. Expect a resolution rate between 50% and 75% on a realistic list, varying heavily by geography, seniority and industry. Senior and technical roles resolve better than frontline and shift-based ones. If you work in sectors at the lower end, plan for it rather than treating it as a vendor failure.

It cannot tell you intent. A resolved record tells you who someone is, not whether they want to hear from you. Movement data is the closest observable proxy, which is why trigger-based approaches outperform static list work.

It cannot replace verification. A resolved person and a working email address are different claims. Someone can be correctly resolved and have a dead address, or have a live address and have left four months ago. The pair is in the email verification guide.

Build or buy

If you are weighing an internal build, the API calls are not the hard part. What you would own instead: entity resolution across name and legal-entity variants, title and skill normalization taxonomies in every market you serve, search infrastructure serving multi-signal queries at acceptable latency, and a refresh pipeline that distinguishes a real job change from a profile edit.

Realistically one to three engineers permanently plus infrastructure. Justified if professional data is your product; hard to justify if it is one input into a go-to-market motion. The full cost model is in build vs buy.


Frequently asked questions

What is a profile lookup API?
An API that resolves an identifier for a working professional, such as a profile handle, stable ID, work email address, profile URL, or name plus company domain, into a structured record containing their current role, employer, career history, education, skills and location.

What is the difference between profile lookup and people search?
Lookup resolves a known identifier into one record. Search finds people matching criteria when you do not know who you are looking for. Most integrations use both: search to build a list, lookup to keep it current.

Can I look up a profile from an email address?
Yes, from a professional working mailbox. Public mailbox providers, role accounts, disposable addresses and relay services are rejected before any work runs, and that rejection is free.

Do I pay for lookups that return nothing?
Yes. Producing a definitive no-match takes the same work as producing a record, which is why a miss returns 200 rather than 404. Budget on records submitted rather than records returned.

Why does a not-found return 200 instead of 404?
Because the request succeeded and the answer is empty. A 404 would mean the endpoint does not exist. Branch on the success field in the response body rather than on HTTP status.

How accurate is profile data?
Expect a resolution rate between 50% and 75% on a realistic list. Accuracy on resolved records is typically high but concentrates its errors in recent changes, because every provider has detection lag. Measure both on your own golden set.

How often should profile data be refreshed?
By field. Current title and employer decay fastest because they change on a job change. Position history is append-only and never needs refreshing. Education and certifications are effectively permanent.

What is the difference between a dataset read and live resolution?
A dataset read queries a clean, deduplicated set of records: fast, consistent, right for bulk. Live resolution resolves one subject on demand for the freshest available snapshot: right when a human is waiting or accuracy matters more than cost.

How many records can I enrich at once?
Up to 1000 per batch job. Submit returns a job handle immediately, work proceeds in the background, and results are keyed back to your exact inputs.

What happens if someone has opted out?
The request returns a permission-denied error and is not charged. Record the exclusion permanently and never resubmit that identifier.

Can I use a profile lookup API in a user-facing product?
Yes, with two constraints. Keep the call off the critical path or give it a tight latency budget with a fallback, and never return the enriched record to the browser.

How do I know what a call costs before I make it?
Per-route costs are published at a public endpoint needing no key. Any route not listed costs one credit. Every SDK response also reports what that specific call consumed.


The one-line version

Resolve once from whatever identifier you hold, store the opaque ID, and refresh against that forever. Handles and names change; identifiers do not, and anything keyed on a changeable string fails silently rather than loudly.

Branch on success rather than HTTP status, because a miss returns 200 and a status-only check will write nulls over good records. Budget on submissions rather than results, because misses are billed and your resolution rate is the variable that decides your real cost. Use batch for anything beyond a handful, persist the job handle before you wait, and suppress opt-outs permanently on first occurrence.

Refresh by field rather than by calendar, since position history never changes and current employer dies the day someone leaves. And where you can, watch for change events instead of re-resolving everything, because that turns a database-wide refresh into a short queue of records you already know are wrong.

The rest is storage discipline: provenance per field, enriched values kept separate from human-entered ones, and write rules strict enough that nothing empty or uncertain can overwrite something good.

If you want to try a single lookup before writing any code, the free tools will resolve one person or address with no signup. The Quickstart takes about five minutes, the API playground runs real calls without an integration, and for volume pricing, an SLA or a procurement conversation, talk to us directly.

Try Renidly free

One API to enrich, search and verify any B2B identity. 100 credits on us.

Start Free
  • 100 free credits on signup
  • No credit card required
  • Credits never expire
// Keep reading

Related articles

All articles →
Firmographic vs Technographic vs Intent Data: And the Fourth Type Nobody ListsFeatured
//Guides

Firmographic vs Technographic vs Intent Data: And the Fourth Type Nobody Lists

The short version, since that is usually what people want first: Firmographic data describes what a company is. Industry…

Oct 31, 202611 min readRead
company data apiFeatured
//Guides

Company Data API: What Firmographic Enrichment Returns and How to Use It

Company data is the half of B2B enrichment that gets taken for granted. Person data is obviously hard. Names collide, people…

Oct 27, 202610 min readRead
verified candidate dataFeatured
//Guides

How to Find Verified Candidate Data: A Guide for Recruiting Teams

A recruiter opens a candidate record, reaches out, and gets nothing back. The email bounces or the message goes nowhere. They…

Oct 21, 202611 min readRead
Get started in minutes

Ship your first resolution today.
resolution today.

Get your API key on signup. Pay only for what you call — no subscription, and nothing gets taken back at month-end.

Start
Free

100 credits on signup

Scale
From $50

Pay-as-you-go top-ups

Volume
Custom

Negotiated + SLA

Try for freeSee full pricing
  • No credit card
  • No subscription
  • Credits never expire
  • 24/7 support
renidly

The identity layer modern teams build on. One API to enrich, search, and verify any email, domain, company, or profile. Powered by real-time lookup and a large-scale identity graph.

Start FreeSign in

Product

  • Data API
  • Live API
  • Email API
  • Free tools

Developers

  • Documentation
  • Quickstart
  • Integrations
  • API Reference

Company

  • Pricing
  • Blog
  • Trust Center
  • Contact

Legal

  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Refund Policy
  • Data Processing Agreement
  • Security Policy
  • Do Not Sell / Opt Out
© 2026 Renidly · operated by Droven Data Strategy LLC. All rights reserved.
GDPRCCPA
renidly