A B2B data enrichment API takes a partial record and returns a complete one. You send an identifier you already have, usually an email address, a company domain, or a profile URL. You get back structured data about the person or company behind it: job title, employer, headcount, industry, location, and whatever else the provider holds.
That is the whole concept. The difficulty is never the concept. It is that enrichment APIs differ enormously in what they return, how they behave when they fail, what they cost at volume, and how quickly their data goes stale, and most of those differences are invisible until you are already integrated.
This guide covers the mechanics end to end: the types, the request and response model, identifiers, batch versus real-time, error semantics, pricing models, integration patterns, evaluation, and the mistakes that show up in production rather than in a demo.
What a data enrichment API actually does
Three steps, always the same shape:
- You send an identifier. Something you already have that points at a person or company.
- The provider resolves it to a record in their system.
- You get structured data back, usually JSON, with some indication of how confident the match is.
The step that matters is the middle one, and it is the part vendors describe least. Resolution is not a lookup in a table keyed on your input. It is a matching problem, because your input is fuzzy and the underlying records are messy. "Northwind Logistics Inc" and "Northwind Logistics LLC" may or may not be the same company. Two people named Jane Doe may both work in logistics. The provider has to decide.
Which means every enrichment response carries an implicit claim: we think this is the right record. How strongly a provider expresses that claim, and whether it is honest about uncertainty, is the single biggest quality difference between providers. More on that below, because it determines how you write your integration.
The full reasoning behind why matching is hard and why identifiers beat names is in what B2B identity resolution actually is.
The four types of enrichment API
"Enrichment API" covers four genuinely different products. Most teams eventually need more than one, and buying the wrong type first is a common and expensive mistake.
| Type | What you send | What you get | Typical use |
|---|---|---|---|
| Contact enrichment | Email, name + domain, profile URL | Person: title, employer, seniority, location | Lead routing, personalization, CRM fill |
| Company enrichment | Domain, company name | Firmographics: headcount, industry, HQ, revenue band | Scoring, territory, segmentation |
| Email discovery and verification | Name + domain, or an address | An address, or a deliverability verdict | Outbound, list hygiene, signup validation |
| Signal and event data | A watch definition | Events: job changes, hiring activity, funding | Trigger-based prospecting, churn warning |
The first two are what people usually mean by enrichment. The third is a separate discipline with its own failure modes, covered in depth in the email verification API guide. The fourth is the one most teams do not know exists as an API and is often the highest-value of the four, because it tells you when rather than who.
Renidly splits these across three products against one key and one credit pool: the Data API for dataset reads and search, the Live API for freshest-available resolution, and the Email API for verify, find and reverse. The practical benefit of a shared key and envelope is that mixing types is a per-call decision rather than a second vendor integration.
What you can enrich from
Your available identifier determines everything downstream, so it is worth being precise about the four common starting points.
A work email address. The strongest starting point in B2B, and the one you get free from almost every inbound touchpoint. It identifies both a person and a company. Reverse resolution turns it into both, and the mechanics are in reverse email lookup.
A company domain. Identifies a company reliably. Does not identify a person. Good for account-level scoring, useless for personalization on its own.
A name plus a company domain. Enough to find a person and often their email address. Weaker than an email because names are not unique and title strings vary.
A profile URL or handle. Precise for the person, but handles can change, which breaks anything you keyed on them.
A rule that saves real pain: whatever you send, store what comes back as a stable identifier. Good providers return opaque IDs that never change, as opposed to slugs and handles which can. Key your records on the ID, treat the human-readable value as display text, and refresh against the ID forever.
Request and response model
Almost every enrichment API is a REST endpoint with an API key in a header. The interesting differences are in the response.
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 NoneThe Node client mirrors it, with camelCase on the response fields:
1import { Renidly } from "renidly";
2
3const renidly = new Renidly();
4
5const result = await renidly.emails.reverse("[email protected]");
6
7console.log(result.found, result.confidence);
8console.log(result.person, result.currentCompany);Three things to check in any provider's response model before you integrate.
Are person and company independently nullable? A resolution that identifies the company but not the individual is still useful for routing and scoring. If the API returns nothing unless both resolve, you are discarding usable partial results.
Is there a confidence signal, and is it granular? A boolean forces you to trust every result equally, which in practice means trusting all of them at the level of the worst one.
Is cost reported per call? If spend only arrives as a monthly invoice, you cannot compute cost per usable record without reconstructing it later, and you will not.
Not-found is a real answer, and it is billed
This trips up most first integrations, so it is worth stating plainly.
A lookup that runs and resolves nothing typically returns HTTP 200 with an empty or flagged result, not a 404. The request succeeded. The answer is that no record matched. Producing a definitive "this does not resolve" takes the same work as producing a record, so it is billed accordingly.
Two consequences.
Branch on the response body, not the HTTP status. If your client treats 200 as a hit, a miss will look like a success and you will write null over a populated field.
Budget on records submitted, not records returned. If you send 10,000 addresses and 55% resolve, you pay for 10,000 lookups and get 5,500 usable records. Your true cost per usable record is roughly double the headline rate. Teams that model the headline number are surprised by the first invoice, and nothing was actually mispriced.
Real-time versus batch
Two execution models, and picking wrong is the most common performance problem in enrichment.
Real-time is one record, synchronously, while something waits. Correct for signup forms, inbound routing, and anything user-facing. Keep the timeout tight and always have a fallback, because your availability should not depend on a third party's.
Batch is many records submitted as a job. You get a job handle immediately, work happens in the background at a controlled pace, and you collect results as they land. Correct for backfills, scheduled refreshes, and anything where nobody is waiting.
1job = renidly.data.people.enrich_batch(handles=chunk) # up to 1000
2result = job.wait(on_progress=lambda n: print("resolved", n))
3
4for row in result.results:
5 save(row.matched_input, row)1const job = await renidly.data.people.enrichBatch({ handles: chunk });
2const result = await job.wait({ onProgress: (n) => console.log("resolved", n) });
3
4for (const row of result.results) save(row.matched_input, row);The matched_input field on each result links it back to exactly what you submitted, so you are never zipping arrays by position and hoping ordering held. Look for that in any batch API you evaluate. Without it, partial results and retries become genuinely dangerous.
The mistake to avoid is firing a thousand parallel single calls instead of using batch. That generates a thousand requests to be rate limited, has no resumability, and is slower. The full pattern, including resumable job handles and why you should persist the job ID before waiting rather than after, is in batch enrichment at scale.
Freshness: the axis nobody tests
Accuracy is a snapshot. Freshness is whether that snapshot is still true.
This is where enrichment quietly fails, because a stale record looks exactly like a fresh one. Nothing in your CRM UI distinguishes a title verified last week from one captured in 2021.
Ask any provider one specific question: is freshness something I choose per request, or is it baked into the dataset? A provider offering only one mode has made a tradeoff on your behalf that should be yours to make per workload. Bulk hydration of a hundred thousand cold records and a live check on an account a rep just opened are different jobs with different requirements.
Then store per-field verification timestamps on your side regardless of what the provider does. Without them you cannot prioritize a refresh, cannot audit a bad decision, and cannot tell your reps which fields to trust. Field-level decay rates and how to build a refresh strategy around them are in CRM data decay.
Error semantics, which matter more than accuracy
A provider's behavior when things go wrong tells you more about engineering quality than its accuracy claims do.
The errors worth checking for explicitly, and what each should mean:
| Condition | Should be | Retry? |
|---|---|---|
| Bad input | Typed error naming the offending field | No, fix it |
| Not found | 200 with a clear flag | No |
| Opted out | 403, not charged | Never, suppress permanently |
| Balance exhausted | Distinct error, not a generic 4xx | No, alert operations |
| Rate limited | 429 with a retry-after value | Yes, with backoff |
| Transient failure | 503, not charged | Yes, with backoff |
1from renidly import (
2 Renidly, InvalidRequestError, RateLimitError,
3 InsufficientCreditsError, PermissionDeniedError, RenidlyError,
4)
5
6try:
7 result = renidly.emails.reverse(email)
8except InvalidRequestError as e:
9 return {"status": "invalid", "fields": e.field_errors}
10except PermissionDeniedError:
11 suppress_permanently(email) # opted out, never retry
12 return {"status": "excluded"}
13except RateLimitError as e:
14 return {"status": "retry", "after": e.retry_after}
15except InsufficientCreditsError:
16 alert_ops("enrichment balance exhausted")
17 return {"status": "degraded"}
18except RenidlyError as e:
19 log.warning("enrichment failed", code=e.error_code)
20 return {"status": "error"}The one people get wrong is opt-out. Individuals can withdraw from resolution, and that is a permanent state, not a transient failure. If it falls into your generic retry path, every batch run you ever execute will re-request the same records forever, at cost, for guaranteed nothing. Build the suppression list on the first occurrence.
Rate limits and throughput
Every provider has a per-minute ceiling. Discovering it through 429 responses in production is avoidable.
Two things to get right:
Use a client-side limiter. A sliding-window limiter that holds you under the ceiling is far better than reactive backoff. Renidly's SDKs expose auto_rate_limit (autoRateLimit in Node) which reads your tier's limit and paces requests automatically.
Leave headroom when one key serves multiple workloads. Set a safety factor below 1.0 so a backfill cannot starve the interactive path a user is waiting on. Running a bulk job at 100% of your ceiling on the same key as your signup flow is a self-inflicted outage.
1from renidly import Renidly, RenidlyConfig
2
3renidly = Renidly(config=RenidlyConfig(
4 auto_rate_limit=True,
5 rate_limit_safety=0.7, # bulk work stays at 70% of the ceiling
6 max_retries=3,
7))Pricing models, and what to actually compare
Four models are common, and they are not directly comparable, which is the point of having four.
Per-record credits. You buy credits, each call consumes some. Transparent and easy to model. Watch whether misses are billed, because they usually are and should be.
Per-successful-match. Sounds better and creates a bad incentive, since the provider is rewarded for calling uncertain matches successful. Be careful here.
Monthly subscription with a quota. Predictable, wasteful if your volume is spiky, expensive if you exceed the quota.
Enterprise contract. Negotiated rate, an SLA, usually a committed minimum.
Whatever the model, the comparable number is the same one:
1effective cost = total spend ÷ records that were matched AND correctNot cost per call. Not cost per match. A provider at half the headline rate with two thirds the accuracy is more expensive, and it also hands you a pile of confidently wrong records your team will act on before anyone notices.
Renidly is credit-based with per-route costs published at a public endpoint requiring no key, so you can price an integration before signing up for one, and every response reports the credits it consumed. Current rates and the free tier are on the pricing page.
Integration patterns
Four places enrichment goes, each with its own constraints.
At signup, inbound. The highest-value placement because it changes what happens next in the funnel. Someone enters a work email and before they finish the form you know their title, company, and headcount, which drives routing and lets you drop qualification questions. The constraint is latency, and the rule is never block signup on enrichment. Patterns for sync, async and hybrid designs are in email enrichment at signup, and the use case is mapped out on the inbound lead enrichment page.
Into the CRM, scheduled. Backfill and refresh. Batch is correct here. The thing that causes incidents is write policy rather than the enrichment itself: never let an empty result overwrite a populated field, never let low confidence overwrite high, and preserve human edits. See sales intelligence for the shape of this.
Into a warehouse, for analysis. Enrichment as a data pipeline step. Store the raw response alongside the parsed fields, because schemas evolve and you will want to reprocess without re-paying.
As a tool for an AI agent. Newer, and it exposes response-shape stability as a hard requirement. An agent that has learned a field name breaks when the field is renamed. Prefer providers with versioned, documented schemas and stable identifiers over ones that quietly reshape responses.
How to evaluate a provider
Short version, since this deserves its own treatment and has one in how to evaluate a B2B data provider.
Test on your data, never the vendor's sample. Every sample is drawn from where that vendor is strongest. Yours is what you are buying against.
Build a golden set. A hundred records where you have manually established the truth. Without it you can only compare providers to each other, and two providers agreeing on a wrong answer looks identical to two agreeing on a right one.
Measure accuracy, not fill rate. Fill rate is the number vendors lead with and the easiest to inflate by loosening the matching threshold.
Submit records that should fail. Fake companies, people who do not exist, malformed input. If a provider returns a confident-looking result for a company that does not exist, every wrong answer it produces will reach your CRM 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 with a retrospective. Take twenty people you personally know changed jobs in the last ninety days. Count how many the provider has in the new role. Small sample, direct evidence, one afternoon.
Six mistakes that show up in production
Keying records on handles or slugs. They change, and when they do your pipeline breaks silently with no error. Key on stable IDs.
Treating enrichment as binary. Writing every match at equal trust is how a CRM becomes a system nobody believes. Route on confidence.
Letting empty overwrite populated. A refresh that returns nothing should never delete a value a human entered. This is usually a bulk job treating null as a value.
Blocking a user-facing flow on a third-party call. Enrichment failure should cost you a data point, not a conversion.
Re-requesting permanent failures forever. Opt-outs and dead records need a permanent exclusion list, not a retry.
Returning enriched data to the browser. If your signup endpoint sends the enriched record back to the client, you have built a free public lookup API that anyone can query by submitting arbitrary addresses to your form. Enrichment stays server-side, and the client receives a decision rather than a record.
Build or buy
The API calls are never the hard part. What you would own instead:
Entity resolution. Deduplicating people and companies across name variants and legal-entity differences is a hard problem with a large research literature and no clean library answer.
Normalization taxonomies. Titles, skills, industries and geographies all need canonical forms, in every language and convention you serve. These are ongoing projects, not one-time work, because new tools and titles appear constantly.
Search infrastructure. Serving multi-signal conjunctive queries over tens of millions of records at reasonable latency, plus index tuning when a query plan degrades on a Friday.
Refresh. The one always underestimated. A dataset is stale the moment it lands, and building change detection that distinguishes a real job change from a profile edit is most of the work.
Realistically that is one to three engineers permanently plus infrastructure. Build it if professional data is your product. Buy it if it is one input into a go-to-market motion, and spend the team on the motion.
Frequently asked questions
What is a B2B data enrichment API?
An API that takes a partial record, usually an email address, company domain, or profile URL, and returns structured data about the person or company behind it, along with some indication of match confidence.
How accurate are enrichment APIs?
It varies enormously by geography, industry, company size and seniority, which is why published accuracy figures are close to meaningless for your situation. Expect a match rate somewhere between 50% and 75% on a realistic B2B list, and measure accuracy on your own golden set rather than trusting a benchmark.
Do I pay for lookups that return nothing?
With most providers, yes, because producing a definitive "no match" takes the same work as producing a record. Budget on records submitted rather than records returned.
What is the difference between enrichment and verification?
Enrichment tells you who an address belongs to. Verification tells you whether it can receive mail. They are separate questions and you often need both, since an address can resolve perfectly and be undeliverable because the person left.
How often should enriched data be refreshed?
It depends on the field. Work email and job title decay fastest because they die on a job change, while company industry and headquarters location are stable for years. Refresh by field-level decay rate rather than on a single calendar cycle.
Can I use an enrichment API in a signup form?
Yes, and it is one of the highest-value placements, but never block form submission on it. Give it a short latency budget, fall back to the default path if it is exceeded, and let a worker finish the job in the background.
What is a catch-all domain?
A domain that accepts mail for every address whether or not the mailbox exists. It makes verification unable to confirm a specific mailbox, and treating catch-all as valid is a common cause of delayed bounce damage.
Where to start
If you are evaluating enrichment for the first time, do not start with a feature comparison. Start with a measurement.
Take 500 records from your own database where you have an email but sparse firmographics. Run them through one provider. Measure three things: what percentage resolves at high confidence, how many of those disagree with what your CRM currently believes, and your effective cost per usable record.
The second number usually ends the internal debate faster than any evaluation document, because it quantifies staleness you already have rather than accuracy you might gain.
There are 100 free credits with no card required, which covers that test comfortably. The Quickstart takes about five minutes, and the endpoint playground will run a single record without any code at all.


