Support has an email address. The CRM has a LinkedIn URL. Your commit history has a GitHub username. These are three systems with three identifiers but no shared key. Is it the same person?
You could try to join on name, but names collide. You could parse an email domain and guess a company, but people change jobs and keep addresses. You could try fuzzy matching, and you will get the wrong answer with high confidence (common failure mode).
This problem has a name: identity resolution. It's the process of taking scattered identifiers (an email, a social profile, a handle) and deciding whether they point to the same real person. This post is about those decisions.
Why the obvious approaches fail
The most natural instinct is string matching. Does the name on the email match the name on the LinkedIn? Does the domain in the email match the company on the profile? Each of these breaks in certain ways:
Name matching returns the wrong person at full confidence. If you search for "Jordan Whitfield" at a large company, there may be someone by that name, but they might work in a different office, hold a different role, and share nothing with your record but the string. This isn't a theoretical edge case. Name collisions are common enough that the resolution decision (accept or reject a close match) needs to be explicit in any pipeline that runs at scale.
Domain heuristics assume people stay. An email address encodes the employer someone had when the address was created. If they moved and kept the address, the domain no longer tells you where they work, and a match against the new employer's directory will either miss them or return a different person.
Company names are not stable identifiers. "The Gates Foundation" and "Bill & Melinda Gates Foundation" are the same entity. So are "Meta" and "Facebook" and "Meta Platforms, Inc." A domain collision, where two companies claim the same website domain, has returned the wrong company in production for a customer building an integration pipeline. Display-name variants are a recurring source of silent mismatches.
These failures share a pattern where matching is easy, but knowing when a match is wrong is hard.
Resolve once then learn everywhere else they exist
An identity resolution API takes an identifier you have and returns everything it can connect to that person, including identifiers you don't have. One call, one entity, and the result gives you the links.
With Enrich Layer's /v3/person endpoint, you pass a LinkedIn slug:
curl "https://api.enrichlayer.com/v3/person?profile=mitchellh&socialLink=include&developerProfile=include" \
-H "Authorization: Bearer $API_KEY"
The response resolves a single person and returns their linked identifiers:
{
"fullName": "Mitchell Hashimoto",
"headline": "Co-founder of Superlogical. Prev: Co-founder of HashiCorp...",
"contactInformation": {
"socialLinks": {
"linkedinUrl": "https://www.linkedin.com/in/mitchellh",
"twitterUrl": "https://x.com/mitchellh",
"websiteUrl": "http://mitchellh.com"
}
},
"developerProfile": {
"github": {
"username": "mitchellh",
"followersCount": 18919,
"publicReposCount": 112,
"languages": ["Assembly", "C", "C++", "Go", "Rust", "Zig"]
}
}
}
You passed one identifier. You got back four others, plus the GitHub activity data that tells you this person writes systems software. That cross-identifier payload, not the initial lookup, is where resolution delivers value.
The base call costs 1 credit. Each enrichment parameter (socialLink, developerProfile, workEmail, personalEmail, careerInsight) adds 1 credit to the response. You choose what you pay for.
What happens when the match is uncertain
This section covers what none of the ranking guides on this topic document: the point where the API has found something that might be the right person, and has to decide what to tell you.
The similarity_checks decision
Enrich Layer's v2 Person Lookup endpoint (/api/v2/profile/resolve) has a parameter called similarity_checks. It controls what happens when the best available match does not look right.
We tested this with a deliberately ambiguous query: a plausible name at a real company, where the person probably does not work there.
Request: first_name=Jordan&last_name=Whitfield&company_domain=stripe.com
With similarity_checks=include (the default):
{
"url": null,
"name_similarity_score": null,
"company_similarity_score": null,
"title_similarity_score": null,
"location_similarity_score": null
}
The API found a potential match, decided it was not confident, and returned nothing. Cost: 2 credits. You paid for certainty.
With similarity_checks=skip:
{
"url": "https://www.linkedin.com/in/jordan-whitfield-32a472278",
"name_similarity_score": 1.0,
"company_similarity_score": 0.33,
"title_similarity_score": 0.0,
"location_similarity_score": 0.0
}
The API returned its closest match without filtering. The name is a perfect match (1.0). The company is a 33% match. The title and location are zero. Cost: 2 credits. Same price, different answer, different human.
This is essentially a policy decision. You're choosing between a confident nothing and a probable something. Both cost the same. And the API's own documentation says to use skip "to avoid charges on null results," which is nearly self-defeating advice, because skip is the mode least likely to return null. It returns the closest match regardless.
The right choice depends on what happens downstream. If a wrong match poisons a CRM record that takes a human to fix, you want include. If a miss means a sales lead never gets routed and nobody notices, you want skip. There's no safe default. The default (include) is simply the more conservative mistake.
The lookup_depth decision
The reverse email lookup endpoint (/api/v2/profile/resolve/email) exposes a different kind of confidence tradeoff through lookup_depth:
| depth | method | cost on a miss |
|---|---|---|
superficial | Database only | 0 credits |
deep (default) | Database + heuristics | 3 credits |
Both returned null in our test. The address wasn't in the database and the heuristics didn't find it. But deep charged 3 credits for looking harder and finding nothing. superficial charged nothing.
This is a budgeting decision. At volume (say you are resolving a list of 10,000 email addresses), the miss rate determines your cost, and you don't know the miss rate before you start. Running superficial first on the full list, then deep only on the misses, means you pay the 3-credit deep price only on addresses the free pass couldn't find. Addresses that miss on both stages still cost 3 credits. The savings come from what superficial finds for free, and from any misses you choose not to escalate.
What Company Lookup does differently
Person Lookup lets you avoid the charge on a miss. Company Lookup (/api/v2/company/resolve) does not: a no-match returns HTTP 200 with a null result, and you're charged 2 credits. The resolution attempt happened and the null is the answer. There's no similarity_checks parameter on this endpoint.
This isn't an inconsistency but rather a design decision about where the resolution work happens. Person matching has enough ambiguity that the caller's policy matters. Company matching does not expose that part.
Picking a join key that survives
Once you've resolved a person, you need to store the result in a way that survives the next lookup. This is where most implementations can erode later on.
Do not key on the full LinkedIn URL. The URL contains a vanity slug that the user can change. Worse, the format is not even stable across API responses. In our test run, the same person resolved via two different email addresses came back as:
https://www.linkedin.com/in/mitchellhhttps://linkedin.com/in/mitchellh
Same person, same API, two different strings. Anything joined on the full URL treats these as two people.
Key on public_identifier, and plan for it to change. This is the vanity slug itself (mitchellh), returned as a standalone field in the v2 person response. It removes the URL format differences above, so both variants map to one key. It does not survive a slug change, because the person response has no permanent ID. Re-resolve stored people on a schedule, and treat a slug that stops resolving as a signal to reconcile, not as a deleted person. For companies, the v2 schema also exposes linkedin_internal_id (a numeric identifier that never changes) and /api/v2/company/resolve-id converts it to the current slug at 0 credits.
The principle is to store the most stable identifier available, normalize it on write, and know which keys can drift. For companies, linkedin_internal_id never changes. For people, public_identifier is the best key available, but it needs a refresh path.
What this doesn't do
Identity resolution has a vocabulary borrowed from customer data platforms, like golden records, survivorship rules, identity graphs, and entity deduplication. If you arrived here looking for those, this section is for you, because Enrich Layer doesn't do any of them.
What we described above is per-call resolution. You send an identifier, you get back a person and their linked identifiers. The API doesn't maintain a persistent graph of your resolved entities. It doesn't merge two of your CRM records. And it doesn't decide which of two conflicting phone numbers to keep.
Those are your decisions, in your system, with your merge logic. The API gives you the data to make them (the similarity scores, the linked identifiers, the confidence that a match is real), but the policy is yours. This is a deliberate scope boundary. If your resolution provider also controls how records merge, switching vendors means rebuilding your identity model from scratch.
Choosing the right call
Each resolution scenario maps to a specific endpoint. Here is the decision table:
| You have | You want | Endpoint | Cost | On a miss |
|---|---|---|---|---|
| LinkedIn slug or URL | Full profile + linked identifiers | /v3/person?profile= | 1 cr + extras | 404, free |
| Personal email | Profile | /v3/person?email= | 1 cr + extras | 404, free |
| Work email | LinkedIn URL | /api/v2/profile/resolve/email | 3 cr (deep) | Charged |
| Name + company | LinkedIn URL | /api/v2/profile/resolve | 2 cr | Depends on similarity_checks |
| Company name or domain | Company profile | /api/v2/company/resolve | 2 cr | Charged |
This table doesn't show a few things because they depend on your use case:
- Which endpoint do you start with? If you have a LinkedIn URL already, skip the resolve step. This guide covers choosing between Search, Lookup, and Enrichment.
- What do you do with the miss? The
similarity_checksandlookup_depthparameters from earlier are the answer, and they aren't in this table because the right setting depends on what a wrong match costs you downstream. - How do you key the result?
public_identifierfor people (with a refresh path),linkedin_internal_idfor companies, as above.
The live responses, similarity scores, and credit costs in this post come from production API calls made in September 2026. The fixtures are reproducible using the same inputs, same endpoint versions, and same parameter values.
