How Discover Works
Discover turns criteria into a list of people. Enrich turns an identity into a record.
Why it matters: three habits decide whether Discover is cheap or expensive. Validate and count are free; enriching everyone you find is not; and no search pages past 5,000 people.
Two versions, one engine#
Same filters, same vocabularies, same engine. Only the request shape differs, and the v2 / v3 switch in the sidebar or on any sample shows the one you are building against.
- Build against Advanced. Filters grouped by entity —
company,person,workExperience,vendorgraphics— each field takingand/or/not, ranges takingmin/maxorgte/lte. It expresses a full ICP: “VP+ in Engineering at SaaS companies of 200–2,000, excluding current customers.” - Basic is deprecated. Flat top-level fields ANDed together. It keeps working while v2 does, but new capabilities ship to Advanced only, and anything Basic can say, Advanced can say.
- One endpoint,
discover/people. The body is a list of filter groups underor. Each group is the entity-grouped filter set; a person matches if they satisfy any group. - One group is a v2 Advanced request. Several are a union — “current employees or people who left in the last year” — in one call.
- Name your groups. Every result carries
matchedFilterGroups, so you can tell which arm produced it.
Search the prose, not just the taxonomy#
Prose search is v3-only. Switch this page to v3 to see it.
Taxonomy filters find roles. keywords finds what people wrote.
- Group-level
keywordssearches job titles, company names, work-experience descriptions and bios. A node is a leaf (phraseorterm) or a branch (all,any,none), nested as deep as you need: (a OR b) AND (c OR d) is anallof twoanynodes. phrasemeans adjacent words;sloploosens it. Descriptions are stored with whitespace flattened, so a slop above 2–3 matches across what were separate bullet lines.fieldsnarrows a leaf to any oftitle,companyName,description,bio. It is the main lever for keeping a long term list inside the request’s clause budget.- Two grains.
title,companyNameanddescriptionmatch per work experience;biomatches per person. company.keywordsis a different tool. It selects companies by their own description and tags, then returns people at them: “who works at a mattress retailer”, not “whose title says mattress”. Hardand/or/not. Capped at the 10,000 most relevant companies;warningstells you when more matched.- Results show their evidence.
matchedSnippetscarries each matching fragment, the field it came from and the words that hit. Plain text — escape it before rendering. Onlydescriptionandbioare quoted; a title or company-name hit is already visible inmatchingPositions. minimumMatchsays “at least N of these.” Set on ananynode, it asks for that many children: two of four skills, say. A flatorcannot express it.
keywords with activeWithin.Recency and the company graph#
activeWithin and company.expansion are v3-only. Switch this page to v3 to see them.
activeWithin: "2y" is how you ask for recency. A count of 1–999 plus d, m or y; it matches roles that ended inside the window or have not ended. Do not use endDate for this — it requires an end date to exist, so it silently drops every role the person still holds. The two cannot be combined.
company.expansion grows the seed list before searching. Seeds are company.aggkCompanyId. includeSubsidiaries and includeParents add the corporate tree, and includeCompetitors adds the competitors on each seed’s own record; seeds are kept. relationships.type replaces the seeds with their VENDOR (the seeds’ customers), CUSTOMER (the seeds’ vendors) or PARTNER (both directions) relationships, up to the 1,000 most recently seen. Firmographic filters apply after expansion.
What changes between versions#
Filter semantics and vocabularies are identical. Everything in this table breaks a request moved across versions unchanged:
| v2 Advanced | v3 | |
|---|---|---|
| Body shape | one filter block at the top level | or: list of named filter groups |
| Employment | workExperience.personFilter | workExperience.employmentStatus |
| Person location | person.personCountry / personRegion / personLocality | person.country / region / locality |
| Detection window | vendorgraphics.filterByDetectionPeriod + detectionPeriodBuffer | vendorgraphics.detectionWindow.enforce + buffer |
| Pagination | cursor + limit | pagination.page + size |
| What a count counts | Matching work experiences — two matching roles count twice | Distinct people, matching what the search returns |
| Company graph expansion | Relationships only: company.companyFilter: RELATIONSHIP + relationshipType, whose CUSTOMER/VENDOR values are the reverse of v3’s | company.expansion (subsidiaries, parents, competitors, relationships) |
| School | not available | person.schoolAggkCompanyId / schoolName |
| Any employer, current or past | not available | person.employerAggkCompanyId / employerName |
Validate, count, then discover#
The first two steps are free. Run them every time.
- Validate request — syntax and semantics check. Catches malformed filters before they cost anything.
- Count results — the size of the matching population. Tells you in one call whether filters are too tight (12) or too loose (4.2M).
- Discover people — page through results once the count looks right.
Paths: /discover/people/advanced/validate and /discover/people/advanced/count, same body as the search.
Paths: /discover/people/validate and /discover/people/count, same body as the search.
- The count is over distinct people and ignores the 5,000 paging ceiling. Exact to 40,000, estimated above —
isApproximatesays which. - Validate catches migration mistakes: filters under the wrong entity, v2 field names v3 renamed, and a v2 body with filters at the root instead of inside an
orgroup. Every warning names its group. - Validate also catches what search would quietly drop: a key no filter has (
UNKNOWN_FIELD, listing the ones that exist), a filter written on the group instead of under its entity, a top-level field such aspaginationinside a group, and a repeated keyword. Search and count bind only the keys they know and ignore the rest, so a strayexpansionbesidecompanysearches the seed companies rather than their customers.
{
"workExperience": {
"seniority": { "or": ["vp", "cxo"] },
"role": { "or": ["engineering"] },
"personFilter": "CURRENT"
},
"person": {
"personCountry": { "or": ["united states"] }
},
"company": {
"employeeCountRange": { "or": ["201-500", "501-1000"] },
"industry": { "or": ["computer software"] }
}
}id is the personId you pass to Enrich.Pagination and shape#
Cursor-based. Each page returns an opaque cursor; pass it back unchanged for the next page.
Numbered. pagination.page is 0-indexed; size is at most 1,000.
One employer cannot swamp a page. grouping.maxResultsPerCompany caps results per company, on either version.
A result is a person, with every role that matched. matchingPositions lists each of the person’s work experiences that satisfies the search, up to 10. The first is the one the person ranked on; the rest follow, most recent first. currentPositions lists every role they hold now, matched or not.
estimatedCountsaturates at 5,000. It is the population you can page through, not the population. Count results gives the full size: exact to 40,000, estimated above, withisApproximatesaying which.- Grouping trims the shape. With
maxResultsPerCompanyset, each result carries only its ranked position andestimatedCountis omitted. matchedFilterGroupsis per role. It names the groups the ranked role satisfied. An unnamed group readsgroup_0,group_1, … by position.
Tag your requests. metadata attaches up to 10 key/value pairs of your own to the request.
(page + 1) × size ≤ 5000; v2 stops returning a cursor. Bigger segment? Slice it — by region, employee-count band, or product — and page each slice. Count results is not capped, so use it for population size.Results are a snapshot. The graph updates continuously; the same search later may return a slightly different population.