Search API vs enrichment API cover image

Search API vs enrichment API, and when to chain them

September 2, 2026/Enrich Layer Team·8 min read

Two Enrich Layer endpoints accept a company name and return company data.

GET /api/v2/search/company?name=Accenture
GET /api/v2/company/resolve?company_name=Accenture
text

The first returns a page of profile URLs for companies whose name matches "Accenture", however many of those there are. The second returns a single URL for the one company it thinks you meant, or null when nothing is close enough to be worth returning.

A customer working through our documentation hit exactly this and asked whether the two endpoints were genuinely different or whether one of them was wrong.

They are different. Both calls accepted "Accenture" and both returned company data, so the output shape alone doesn't tell you which one to use. The input does.

If your CRM already has a row that says "Accenture" and you need to confirm which company that is, call Company Lookup. You have one entity and you want to resolve it. If you need consulting firms in Ireland with more than 10,000 employees, call Company Search. You have criteria and you want every company that matches. One call starts from an entity you can name. The other starts from attributes no single entity needs to satisfy. Everything else, how many results come back, how matching works, what "no result" means, and what it costs, follows from which of those two situations you are in.

Three operations, not two

Most API documentation labels everything after search as "enrichment." In practice there are three operations, not two, and each one takes a different input and answers a different question.

1. Search      GET /api/v2/search/company?country=IE&industry=consulting&employee_count_min=10000
               →  { "results": [{ "linkedin_profile_url": "…/accenture" }, { "linkedin_profile_url": "…/deloitte" }, ...],
                    "next_page": "…?next_token=abc123", "total_result_count": 74 }

2. Resolution  GET /api/v2/company/resolve?company_name=Accenture
               →  { "url": "https://www.linkedin.com/company/accenture" }

3. Retrieval   GET /api/v2/company?url=https://www.linkedin.com/company/accenture
               →  { "industry": "Business Consulting and Services",
                    "company_size_on_linkedin": 541251,
                    "hq": { "country": "IE", "city": "Dublin 2" }, ... }
text

Search takes filter criteria and returns every company that matches. Resolution takes a company name and figures out which specific company it refers to. Retrieval takes a profile URL and returns the full structured record. Our quick reference is organized around this split: filter criteria go to the Search endpoints, partial identifiers like a company name go to the Lookup endpoints, and profile URLs go to the Profile endpoints.

Role Lookup is resolution keyed on a job title instead of a person's name: pass "CTO" and a company name, get back the one person who most closely matches. Employee Search takes a company profile URL and searches for people within that company.

SearchLookupProfile retrieval
Inputcountry=IE&industry=...company_name=AccentureA profile URL
OutputArray of profile URLs, paginatedOne profile URL, or nullFull structured record
Question answeredWhich entities match these filters?Which entity does this name refer to?What attributes does this entity have?

How search behaves

Company Search takes filter parameters and returns an array of results with a next_page URL and a total_result_count.

GET /api/v2/search/company?country=IE&industry=consulting&employee_count_min=10000

→  { "results": [
       { "linkedin_profile_url": "https://www.linkedin.com/company/accenture" },
       { "linkedin_profile_url": "https://www.linkedin.com/company/deloitte" },
       ...
     ],
     "next_page": "https://enrichlayer.com/api/v2/search/company?next_token=abc123",
     "total_result_count": 74 }
text

The result set is bounded twice. It's bounded by what Enrich Layer has indexed, and bounded again by how well country=IE&industry=consulting&employee_count_min=10000 approximates the business question you had in mind. Seventy-four results means seventy-four indexed companies matched those three filters. It does not mean seventy-four consulting firms exist in Ireland with more than 10,000 employees.

Filter fields carry the same staleness as any other field. The Person Search documentation says this directly about current-role filters: a profile that has not been refreshed can still show a previous job as the current one, so current_role_title="VP Engineering" will return people who have already left that role. Filtering on a field does not verify it.

When you pass next_token, the query is restored from the token itself. Filter parameters in the URL are ignored from that point on, though they still have to be syntactically valid or the request returns a 400. Code that changes its filters between pages will keep returning the original query's results.

For filter syntax, we have a separate guide to Boolean search.

How lookup behaves

Person Lookup takes first_name, company_domain, and optionally last_name, title, and location. It normalizes those inputs, generates candidate persons, scores each one for similarity, and either returns the best match or rejects it.

similarity_checks controls whether the endpoint rejects a bad match, and it changes how much you're billed.

GET /api/v2/profile/resolve?first_name=Jane&company_domain=example.com
    &similarity_checks=include

→  The closest match is "Tom Garcia at example.com."
   That is obviously not Jane. The endpoint returns null.
   Credits are charged, because the matching work happened
   and null is the answer.

GET /api/v2/profile/resolve?first_name=Jane&company_domain=example.com
    &similarity_checks=skip

→  The closest match is "Tom Garcia at example.com."
   No similarity check runs. The endpoint returns Tom Garcia's URL.
   If no match exists at all, null is returned and no credits are charged.
text

With include (the default), the endpoint pays for the work of establishing that the best candidate is wrong, and charges you for that answer. With skip, a null costs nothing, but a wrong match comes back unchecked. The API prices the two precision policies differently because a false positive that reaches your CRM costs far more to unwind than an empty field.

Company Lookup returns HTTP 200 with { "url": null } when no company matches, but credits are still charged. The 200 status code means the request succeeded; the null means the answer to your question is "no matching company."

Other APIs represent no-match as a 404 instead of a 200 with null. Both are answers and not errors. Your integration code should translate both into one local state (something like unresolved) at the boundary, so that nothing downstream has to branch on which convention a given provider uses.

Three failure modes in the same pipeline

Search and lookup fail differently.

Search false negative. You searched for VP Engineering candidates in Germany and someone you know qualifies did not appear. The problem lies in the query or the data: current_role_title did not match their actual title string, their profile still shows their previous role, or the dataset does not cover them. You fix it by adjusting the filter, broadening the search, or accepting the coverage boundary.

Lookup false negative. You passed first_name=Jane&company_domain=example.com and got null, but Jane works there. Either the identifiers were too weak (common name, wrong domain variant), or similarity_checks=include rejected a candidate that happened to be correct. You fix it by adding last_name or title, trying a different domain, or switching to similarity_checks=skip if you are willing to accept the trade-off.

Lookup false positive. You passed the same parameters and got back a URL for a different Jane at the same company. Nothing about the response signals an error. The record enters your CRM, your team emails the wrong person, and the mistake surfaces weeks later. You prevent it by using the default similarity_checks=include, by supplying more identifiers, and by treating a lookup result as a candidate rather than a fact when the input was weak.

A lookup null where you expected a match is not a coverage problem. A search returning too few results is not a matching-threshold problem.

Deciding which one to call

Do you know which specific person or company the record represents?

no  → Search.
      Filter the results yourself, then retrieve profiles only for the ones you need.

yes → Do you already have the profile URL?

      yes → Call the Profile endpoint directly.

      no  → Call Lookup with the identifiers you have.

            Match returned?  yes → retrieve it
                             no  → keep it as unresolved
text

When a lookup keeps returning null, the tempting fix is to drop similarity_checks to skip, or strip out identifying fields until something comes back. That converts a lookup false negative into a lookup false positive: the rows fill in, but with the wrong people.

If the actual goal has shifted from "identify this specific person" to "show me plausible people matching these attributes," the right call is Person Search with current_role_title and country filters. Relaxing a resolution threshold until the lookup starts returning arbitrary matches is running a search through the wrong endpoint.

Freshness and cost are independent of the operation

Lookup resolves identifiers to a profile URL; freshness is controlled when that URL is retrieved. Both Search and the Profile endpoints accept use_cache. To control the freshness of a Lookup result, pass its resolved URL to the corresponding Profile endpoint and set use_cache there:

  • if-present returns whatever is cached, regardless of age.
  • if-recent returns the cache if it is recent, otherwise attempts a live fetch.

The Profile endpoints add fallback_to_cache, which governs what happens when a live fetch fails. Set to on-error, the stale cached value comes back. Set to never, the failure surfaces as a 404 or 503. A product page showing a company profile can accept stale data over an error. A pipeline computing a freshness-sensitive signal should see the failure and skip the row.

Search and Lookup have different base cost structures. Search charges per returned profile URL, with an additional per-profile charge when enrich_profiles=enrich is set. Lookup charges per request regardless of whether a match is found (with similarity_checks=include) or only on a match (similarity_checks=skip). In both cases, chaining to a Profile endpoint for fresh data adds its own cost. Search supports enrich_profiles=skip to return URLs only, so you can filter candidates before paying for full profiles.

Give records explicit states

In anything larger than a one-off script, different records are at different stages at the same time. Some arrived from a search and have not been verified. Some were looked up and matched. Some were looked up and got null.

Tracking those states explicitly, as candidate, resolved, profile_retrieved, and unresolved, lets you answer "why is this field empty?" six months from now. A record with status unresolved tells you that a lookup ran and returned null, so the fix is better identifiers or a manual review.

A record with status candidate tells you it came from a search and was never resolved at all. Collapsing both into "not enriched" loses that information and turns every empty field into the same undifferentiated problem.

apidata-engineerslead-enrichmentrevops