Renidlyrenidly
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
Renidlyrenidly
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
Renidlyrenidly
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

Reverse Email Lookup: Turning a Work Address Into a Person and Company

Aug 2, 202610 min read
Share

Contents

  • What reverse lookup returns
  • Business mailboxes only, and why that is the right constraint
  • Confidence is the whole design
  • Resolution and verification answer different questions
  • Three places this pays for itself
  • 1. Signup enrichment, synchronous
  • 2. Backfilling an existing database
  • 3. Support and account routing
  • Modeling the cost properly
  • Adjacent patterns worth knowing
  • Errors you should handle explicitly
  • What to do this week

A work email address is the most underused field in B2B software.

It is the one thing you get from almost every inbound touchpoint. A demo form. A trial signup. A support ticket. A webinar registration. A calendar invite from someone you have never met. In most systems it sits there as a login credential and a delivery target, and nothing more.

It is actually a key. [email protected] identifies a specific person at a specific company, and if you can resolve it, you can populate a full account record before that person finishes filling out the form.

This post covers how reverse email lookup works, what it can and cannot tell you, and how to wire it into the three places it pays for itself fastest. Code is in Python and Node throughout.

What reverse lookup returns

One input: a business email address. Two outputs, each independently resolvable.

The person. Full name, headline, summary, current title, profile URL, picture, location, industry, follower count, premium status, open-to-work status, plus arrays for experience[] and education[].

The company. Name, slug, website, profile URL, logo, description, industry, size band, staff count, follower count, location, and specialities.

Plus a confidence value of high, medium, low, or none, and a found boolean.

The critical design point is that person and current_company are separate nullable objects. You can get a company without a person. That is a partial result, and partial results are useful. Knowing that an inbound signup came from a 4,000-person logistics company in Rotterdam is enough to route the lead correctly even if you never identify the individual.

Teams routinely discard these. Do not.

python
1from renidly import Renidly
2
3renidly = Renidly()  # reads RENIDLY_API_KEY from the environment
4
5result = renidly.emails.reverse("[email protected]")
6
7print(result.found)          # bool
8print(result.confidence)     # "high" | "medium" | "low" | "none"
9print(result.person)         # object or None
10print(result.current_company)  # object or None
ts
1import { Renidly } from "renidly";
2
3const renidly = new Renidly(); // reads RENIDLY_API_KEY from the environment
4
5const result = await renidly.emails.reverse("[email protected]");
6
7console.log(result.found, result.confidence);
8console.log(result.person, result.currentCompany);

Business mailboxes only, and why that is the right constraint

Reverse lookup accepts professional working mailboxes. It rejects five categories before doing any work, returning 422:

  • Public mailbox providers (Gmail, Outlook, Yahoo, and similar)
  • Role accounts (info@, support@, sales@, hello@, and similar)
  • Disposable and throwaway providers
  • Relay and address-masking services
  • Syntactically invalid addresses

This looks like a limitation and is actually a correctness guarantee. Consider what a "successful" resolution of [email protected] would even mean. There are many Jane Does with Gmail addresses. Any system that returns a confident person record for a public mailbox is guessing, and a guess written into your CRM at high confidence is worse than no data, because your team will act on it.

Role accounts fail for a different reason. [email protected] is not a person. It is a queue, often staffed by a rotating team. Resolving it to whichever individual happens to own it this quarter produces a record that is wrong by design.

The practical implication for your architecture: filter obvious public providers on your side before the call if you want to save the round trip, but you do not need to build an exhaustive blocklist. The 422 is a clean, fast, structured rejection. Catch it and route those addresses to your consumer-signup path instead.

python
1from renidly import Renidly, InvalidRequestError, RenidlyError
2
3def resolve_or_classify(email: str):
4    try:
5        return renidly.emails.reverse(email)
6    except InvalidRequestError:
7        # Not a professional working mailbox
8        return None
ts
1import { Renidly, InvalidRequestError } from "renidly";
2
3async function resolveOrClassify(email: string) {
4  try {
5    return await renidly.emails.reverse(email);
6  } catch (e) {
7    if (e instanceof InvalidRequestError) return null;
8    throw e;
9  }
10}

Confidence is the whole design

If you take one thing from this post: confidence determines write behavior, not just logging.

Most teams treat enrichment as binary. Data came back, write it. That is how a CRM accumulates records nobody trusts, and once trust is gone, reps go back to manual research and your enrichment spend produces nothing.

Four tiers, four behaviors:

ConfidenceMeaningRecommended action
highStrong, unambiguous resolutionWrite directly to the record, no review
mediumGood resolution with some ambiguityWrite to the record, flag for periodic review
lowPlausible candidateHold in a staging field, route to human review
noneNo usable resolutionDo not write, do not overwrite existing values

The none case has a subtlety worth spelling out. Never let an empty resolution overwrite a populated field. If a rep manually entered a title six months ago and today's resolution returns nothing, the correct outcome is that the manual value survives. Enrichment should be additive to unknown fields and corrective to stale ones, never destructive to known ones.

python
1def apply_resolution(record_id: str, result) -> str:
2    if not result.found or result.confidence == "none":
3        return "no_write"
4
5    person = result.person
6    company = result.current_company
7
8    if result.confidence == "high":
9        write_fields(record_id, person, company, review=False)
10        return "written"
11
12    if result.confidence == "medium":
13        write_fields(record_id, person, company, review=True)
14        return "written_flagged"
15
16    stage_for_review(record_id, person, company)
17    return "staged"
ts
1async function applyResolution(recordId: string, result): Promise<string> {
2  if (!result.found || result.confidence === "none") return "no_write";
3
4  const { person, currentCompany } = result;
5
6  if (result.confidence === "high") {
7    await writeFields(recordId, person, currentCompany, { review: false });
8    return "written";
9  }
10
11  if (result.confidence === "medium") {
12    await writeFields(recordId, person, currentCompany, { review: true });
13    return "written_flagged";
14  }
15
16  await stageForReview(recordId, person, currentCompany);
17  return "staged";
18}

Resolution and verification answer different questions

Reverse lookup tells you who an address belongs to. It does not tell you whether that address still receives mail. Those are separate calls because they are separate questions, and conflating them is a common and expensive mistake.

An address can resolve perfectly and be undeliverable. Jane left the company four months ago. The record is accurate about who the address belonged to. The mailbox is closed.

An address can also be deliverable and resolve to nothing, which is common at small companies and new domains.

If you are about to send mail, do both:

python
1result = renidly.emails.reverse(email)
2check = renidly.emails.verify(email)
3
4if result.found and check.deliverable and not check.catch_all:
5    send(email, personalize_from=result.person)
ts
1const [result, check] = await Promise.all([
2  renidly.emails.reverse(email),
3  renidly.emails.verify(email),
4]);
5
6if (result.found && check.deliverable && !check.catchAll) {
7  await send(email, { personalizeFrom: result.person });
8}

The catch_all flag deserves its own paragraph, because it is the one that damages sending reputation weeks after the mistake. On a catch-all domain, the mail server accepts everything, including addresses that do not exist. Every verify returns deliverable. If you treat that as clean and load it into a sequence, your bounces arrive later, in volume, and by then they have affected your domain reputation.

Segment catch-all separately from the start. verify exposes catch_all as its own boolean precisely so you can.

The full reason enum from verify, and what each implies:

reasonAction
mailbox_acceptsSend
mailbox_rejectedSuppress permanently
catch_allSeparate segment, monitor bounce rate closely
riskyLow-volume segment only
invalid_syntaxFix at the point of capture, this is a form bug
disposableSuppress, and consider blocking at signup
relaySuppress from cold outreach
no_mxSuppress permanently

Three places this pays for itself

1. Signup enrichment, synchronous

The highest-value placement, because it changes what happens next in the funnel.

A visitor enters a work email. Before they reach step two, you know their title, their company, its headcount, and its industry. That drives immediate branching: enterprise-shaped signups get routed to sales, self-serve-shaped signups get the product tour, and neither has to answer six qualification questions.

The constraint is latency. Someone is waiting. Keep the timeout tight and always have a fallback path.

python
1from renidly import Renidly, RenidlyConfig, RenidlyError
2
3renidly = Renidly(config=RenidlyConfig(timeout=5, max_retries=1))
4
5def enrich_signup(email: str) -> dict:
6    try:
7        result = renidly.emails.reverse(email)
8    except RenidlyError:
9        return {"enriched": False}
10
11    if not result.found:
12        return {"enriched": False}
13
14    company = result.current_company
15    return {
16        "enriched": True,
17        "confidence": result.confidence,
18        "title": result.person.title if result.person else None,
19        "company": company.name if company else None,
20        "headcount": company.staff_count if company else None,
21        "route": "sales" if company and company.staff_count and company.staff_count > 500 else "self_serve",
22    }
ts
1const renidly = new Renidly(undefined, { timeout: 5_000, maxRetries: 1 });
2
3export async function enrichSignup(email: string) {
4  let result;
5  try {
6    result = await renidly.emails.reverse(email);
7  } catch {
8    return { enriched: false };
9  }
10
11  if (!result.found) return { enriched: false };
12
13  const company = result.currentCompany;
14  return {
15    enriched: true,
16    confidence: result.confidence,
17    title: result.person?.title ?? null,
18    company: company?.name ?? null,
19    headcount: company?.staffCount ?? null,
20    route: (company?.staffCount ?? 0) > 500 ? "sales" : "self_serve",
21  };
22}
// 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

Never block signup on enrichment. If the call is slow or fails, the user completes the flow and you enrich asynchronously. A failed enrichment should cost you a data point, not a conversion.

2. Backfilling an existing database

You have tens of thousands of email-only records. Batch is the correct tool, and looping single calls is not.

Reverse lookup does not have a dedicated batch endpoint, so the pattern is a bounded worker pool with the SDK's rate limiter engaged. Turn on auto_rate_limit and let the client hold you under your per-minute ceiling using a sliding window, rather than discovering the limit through 429 responses in production.

python
1from concurrent.futures import ThreadPoolExecutor
2from renidly import Renidly, RenidlyConfig, RenidlyError
3
4renidly = Renidly(config=RenidlyConfig(
5    auto_rate_limit=True,
6    rate_limit_safety=0.8,   # stay at 80% of the ceiling
7    max_retries=3,
8))
9
10def resolve_one(row):
11    try:
12        r = renidly.emails.reverse(row.email)
13    except RenidlyError as e:
14        return {"id": row.id, "status": "error", "code": e.error_code}
15    return {
16        "id": row.id,
17        "status": "found" if r.found else "unresolved",
18        "confidence": r.confidence,
19        "person": r.person,
20        "company": r.current_company,
21        "cost": r.meta.credit_consumed,
22    }
23
24with ThreadPoolExecutor(max_workers=8) as pool:
25    for out in pool.map(resolve_one, rows):
26        persist(out)
ts
1import pLimit from "p-limit";
2import { Renidly, RenidlyError } from "renidly";
3
4const renidly = new Renidly(undefined, {
5  autoRateLimit: true,
6  rateLimitSafety: 0.8,
7  maxRetries: 3,
8});
9
10const limit = pLimit(8);
11
12await Promise.all(
13  rows.map((row) =>
14    limit(async () => {
15      try {
16        const r = await renidly.emails.reverse(row.email);
17        await persist({
18          id: row.id,
19          status: r.found ? "found" : "unresolved",
20          confidence: r.confidence,
21          person: r.person,
22          company: r.currentCompany,
23          cost: r.meta.creditConsumed,
24        });
25      } catch (e) {
26        if (e instanceof RenidlyError) {
27          await persist({ id: row.id, status: "error", code: e.errorCode });
28        } else throw e;
29      }
30    }),
31  ),
32);

Set rate_limit_safety below 1.0 when the same key serves production traffic, so a backfill cannot starve your interactive path. Enterprise keys need an explicit rate_limit_per_minute because the limit is negotiated rather than read from a tier.

Run a sample of 500 records before the full job. You need to know your resolution rate on your own data, and the next section explains why that number is the one that determines the cost.

3. Support and account routing

A ticket arrives from an address nobody recognizes. Resolve it and you know whether it is a 50-seat customer or a 10,000-employee enterprise before anyone reads the first line.

This is the highest-margin placement because the volume is low and the value of correct routing is high. A misrouted enterprise ticket costs more than a year of enrichment on the whole queue.

Modeling the cost properly

The number that determines your spend is your resolution rate, not your record count.

A lookup that runs and returns nothing is a completed lookup. Producing a definitive "no match" requires the same work as producing a record, and it bills accordingly. That is why a miss returns HTTP 200 rather than 404: the request succeeded, and the answer is empty.

So the arithmetic is not "cost per enriched record." It is:

text
1total cost = records attempted × cost per lookup
2effective cost per usable record = total cost ÷ (records attempted × resolution rate)

If you send 10,000 addresses and 55% resolve, you pay for 10,000 lookups and get 5,500 usable records. Your real cost per record is roughly double the headline rate. Teams that model the headline rate and budget accordingly get an unpleasant surprise at the first invoice.

Do not take a vendor's word for the resolution rate, including ours. Run a sample of your own data and measure it. Data from a 2019 trade-show list resolves very differently from last quarter's inbound.

Two mechanisms let you measure this precisely rather than estimate it.

Per-call telemetry. Every SDK response carries a .meta object sitting outside the response data, so there is no field collision:

python
1r = renidly.emails.reverse(email)
2print(r.meta.credit_consumed)     # what this specific call cost
3print(r.meta.remaining_balance)   # balance after it
ts
1const r = await renidly.emails.reverse(email);
2console.log(r.meta.creditConsumed, r.meta.remainingBalance);

Emit both to your metrics pipeline from the first call you ever make. Cost per resolved record is the number your finance team will ask for at renewal, and reconstructing it from invoices after the fact is miserable.

Live per-route costs. Rather than hardcoding prices that can change, read them:

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

Route costs are published at GET https://renidly.com/api/panel/credits/routes/costs/, which is public and requires no key, so you can price an integration before you sign up for one. Wire a balance check into your alerting so a backfill cannot silently exhaust the pool that your production signup path depends on.

Adjacent patterns worth knowing

Reverse the other direction. If you have a name and a company domain but no address, emails.find(first_name=..., last_name=..., domain=...) goes the other way. The domain parameter is strict: a bare hostname with a dot, so northwind-logistics.com and not https://northwind-logistics.com or [email protected]. Normalize before the call, because user-entered company fields are full of full URLs.

Start from a profile URL. emails.find_by_url(url) in Python, emails.findByUrl(url) in Node, takes a professional profile URL or its bare public slug and returns the email plus the resolved first name, last name, domain, and company.

Enumerate a domain. emails.prospects(domain, kind) pages through known addresses at a company. kind is required: full returns everything known, verified_only returns deliverable addresses only. Pagination uses opaque cursors, so pass next_cursor back exactly as received and never construct one yourself. This endpoint bills per email returned rather than per call, so page deliberately and stop when you have enough.

python
1cursor = None
2while True:
3    page = renidly.emails.prospects("northwind-logistics.com", "verified_only", cursor=cursor)
4    for p in page.prospects:
5        handle(p.email, p.first_name, p.last_name)
6    if not page.next_cursor:
7        break
8    cursor = page.next_cursor
ts
1let cursor: string | undefined;
2while (true) {
3  const page = await renidly.emails.prospects(
4    "northwind-logistics.com",
5    "verified_only",
6    { cursor },
7  );
8  for (const p of page.prospects) handle(p.email, p.firstName, p.lastName);
9  if (!page.nextCursor) break;
10  cursor = page.nextCursor;
11}

Errors you should handle explicitly

The SDKs raise typed errors, and each one implies a different response:

ErrorMeaningResponse
InvalidRequestErrorBad input, read field_errors / fieldErrorsFix and do not retry
PermissionDeniedErrorNot permitted for this address or keySkip permanently, record the exclusion
InsufficientCreditsErrorBalance exhaustedAlert operations, degrade gracefully
RateLimitErrorPer-minute ceiling hit, carries retry_afterBack off, or enable auto_rate_limit
ServiceUnavailableErrorTransientRetry with backoff
APIConnectionErrorNetwork or timeoutRetry, then fall back

PermissionDeniedError is the one to handle carefully. Individuals can opt out of resolution. When that happens, the correct behavior is to record the exclusion permanently and never retry that address. Retrying an opted-out record on every batch run is both wasteful and the wrong posture. Build the suppression list on the first 403 and honor it.

What to do this week

The fastest way to know whether this is worth building is not a demo record. It is your own data.

Take 500 rows from your existing database where you have an email but sparse firmographics. Run them through reverse resolution. Measure three things:

  1. Resolution rate. What percentage came back found with high or medium confidence?
  2. Correction rate. Of those, how many disagree with what your CRM currently believes about title or company?
  3. Effective cost. Total credits consumed divided by usable records.

The second number is usually the one that ends the internal debate, because it quantifies staleness you already have rather than accuracy you might gain. Most teams are surprised by it.

bash
1pip install renidly
bash
1npm install renidly

Both SDKs cover every endpoint and handle auth, retries, pagination, batch jobs, rate limiting, and typed errors. The free tier includes 100 credits with no card required, which is enough to run a real sample of your own data.

The Quickstart takes about five minutes. For volume pricing, an SLA, or a procurement conversation, get in touch.

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 →
What Is B2B Identity Resolution? A Technical Primer
//Guides

What Is B2B Identity Resolution? A Technical Primer

What Is B2B Identity Resolution? A Technical Primer Every B2B company runs on a fiction: that the person in the CRM, the person…

Jul 31, 202613 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.

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
Renidlyrenidly

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