- The filter operators.
- The searchable fields, with a flag that shows if you can sort on each field.
- The response fields.
- The request parameters.
- Preview mode.
- The error responses.
Pricing: Search costs
0.03 credits per result. Each premium group that you
filter on or receive adds a per-result charge. These are the premium groups
in the tables below:experienceeducationskillscertificationshonors- The
professional_networksignals, withlanguagesandsummary - The filter-only
dev_platform_profilesfields
/account/endpoints.Filter operator reference
Person Search accepts the followingfilters.type operators.
Each endpoint supports a different set of operators.
/person/search does
not support contains. Use (.) for word matching. Company Search accepts
contains. has_all works only on nested-array fields, for example
experience.employment_details.*. On other fields, has_all returns 400.=, !=, in, and not_in match the whole stored value
These four operators compare your value with the complete stored value, never
with part of it. For example, "Qorvo" does not match an employer stored as
"Qorvo, Inc.". The index holds both spellings as separate employers, and both
return people. So a filter that looks correct can return almost no profiles.
in and not_in apply this rule to each value in the list. So a list of near
misses matches nobody. The API does not ignore a difference in punctuation, a
legal suffix, or a missing word. It does not map one value to another.
All four operators ignore case. = "qorvo, inc." and in ["qorvo, inc."] each
return the same 4,146 profiles as = "Qorvo, Inc.".
You can work around the whole-value rule in two ways:
- If you know only part of the value, filter with
(.).(.) "Qorvo"matches every stored spelling that contains the word. - Get the stored spelling from
Autocomplete first, then filter
with
=orin. Autocomplete coverstitle,name,company_industries,seniority_level, andfunction_categoryon each of the three employment paths. Use it before you filter on any of these fields. Autocomplete takes the returned path, not the filter path. For example, get employer names fromexperience.employment_details.current.name, then filter onexperience.employment_details.current.company_name. For the five fields that hold a value from a fixed list, see Closed value sets.
Choose a text operator
Pick the operator by how exact the match needs to be.(.) is the loosest text operator. Every word must appear, in any order, and on
some fields close misspellings also match. So it can return lookalike words. For
example, (.) "Austria" on basic_profile.location.country also returns people
in Australia. Do not use (.) when a near miss would be a wrong result. [.],
=, and in never match misspellings.
(!): fuzzy negation
(!) excludes profiles whose value contains the substring that you send. The
match ignores case. (!) is the opposite of (.). The API matches a
multi-word value as a literal phrase. It does not split the value into words:
-
{ "type": "(!)", "value": "New York" }excludes only profiles that contain the literal phrase"New York". This filter does not exclude a profile whose location is"New Yorker". It excludes"New York City", because that value contains the full phrase. -
To exclude each word on its own, put one
(!)condition for each word in anandgroup:
geo_distance: radius around a point
Set the center of the radius with one of these fields:
location: A string. The server geocodes it, for example"San Francisco, CA".lat_lng: Explicit coordinates as[lat, lng]. This skips geocoding.
lat_lng. distance is required. unit
defaults to km.
For full requests that use
location and lat_lng, see
geo_distance examples.
geo_exclude: exclude a radius
geo_exclude is the inverse of geo_distance. It removes profiles inside the
radius and keeps everyone else. It takes the same value object: location or
lat_lng, a required distance, and an optional unit (default km). It
works on the same location fields. Use it to remove a metro area that you
already cover. You can also use it to find candidates outside a region.
geo_exclude examples.
geo_polygon: inside a polygon
geo_polygon keeps profiles located inside a polygon you draw with explicit
coordinates. Use it when a radius is the wrong shape: a sales territory, a
commute zone, or an area traced on a map. It works on the same location fields
as geo_distance.
The polygon closes itself, so repeating the first point is optional. Edges may
not cross, and no point may be revisited. Points are coordinates, not place
names, and are not geocoded. A polygon that crosses the 180° meridian must
list its points counter-clockwise, or it is read as everything outside it.
400 with a message naming the problem,
for example geo_polygon 'points' needs at least 3 distinct points.
For a full request, see
geo_polygon examples.
Null checks: is_null and is_not_null
Both operators take a literal null value. The value key is required. A
request without it returns 400:
is_nullmatches profiles where no element has the field, for example an emptyexperience.employment_details.currentarray.is_not_nullmatches profiles where at least one element has a non-null value.
Example: people in transition (left a job, no new one yet)
Example: people in transition (left a job, no new one yet)
This request finds people with no current employer and at least one past employer:The response has the standard Person Search shape.
Request
Example: only profiles that carry education data
Example: only profiles that carry education data
Exclude profiles that do not have a field that your workflow needs. Then later
steps do not get empty values:
Request
Nested-array matching: all_of and has_all
Some fields are arrays of nested objects. For example, a person’s employment
history (experience.employment_details.*) is an array. Each entry has its own
title, company_id, company_name, and dates. When you put several
conditions on one of these fields, you can mean one of two things:
- Same element: One array entry meets all the conditions. For example, the person was an Engineer at company X, in one job.
- Cross element: Different entries each meet one condition. For example, the person was an Engineer at company A and a Manager at company B, in two separate jobs.
and group on one nested field means same element. All the conditions
must match in one entry. For cross element, use an all_of group.
Two conditions on one field
Two conditions on the same nested field in anand group ask for one element
that meets both. So two title conditions ask for one job title that contains
every word of both values. They do not ask for a person who held one title and
later the other:
The 4,177 profiles have a title such as
"Business Analyst, Test Manager and Project Manager". This one title contains
all three words.
Each condition that you add this way makes the set smaller, and the set can
become empty. If you add (.) "CMO" to the two conditions above, the search
returns zero profiles. But 118,586 people have CMO in a title. A chief
marketing officer is not also a test analyst in the same job.
Pick the group operator that matches what you mean:
orfor people who match either title.all_offor people who held one title in one job and the other in another job. See theall_ofsection below.andfor one job that meets every condition.
all_of: each condition matched by some element
all_of is a group operator, like and and or. At least one array element
must meet each condition in it. The API checks each condition on its own, so
different elements can meet different conditions. A condition can be a single
filter or an and or or group. The API matches a group within one element.
Same element: one job has the title Engineer and is at company 629097:
Engineer at one company and Manager at another company, in
different jobs:
all_of groups inside and and or groups. So you can combine
cross-element conditions with document-level filters, such as location. A
document-level filter applies to the whole profile, not to one array element.
Conditions inside all_of accept any positive operator: =, (.), [.],
in, <, >, =<, and =>.
has_all: every value matched by some element
has_all is a short form for a common cross-element case: a list of values
where some element of the array matches each value.
This filter finds people who worked at both company 629097 and company
632500, in two separate roles:
all_of group with one equality condition for each value:
{ "op": "all_of", "conditions": [company_id = 629097, company_id = 632500] }.
Rules and validation
- Send the operator in lowercase:
all_of, likeandandor. The API rejectsallOfand other casings. - All fields in one
all_ofcondition, or in itsandororgroup, must resolve to one nested-array path. The API rejects a group that mixes paths, for example an employment field and an education field. Put them in separateall_ofconditions. all_ofandhas_allwork only on nested-array fields: employment, education, certifications, and honors. The API rejects them on a scalar field, such as a name.- Conditions inside
all_ofaccept positive operators only. The API rejects the negation operators!=,not_in,(!), andgeo_excludethere. Apply negation at the document level instead. - Do not put a
has_allcondition or anotherall_ofgroup inside anall_ofgroup. Expand it into=conditions instead. has_allneeds a non-empty list.
Searchable fields
- Some returned fields use a different filter path. For example, to filter on the returned
basic_profile.current_title, useexperience.employment_details.current.title. For the employer paths, see Filter paths and their returned twins. contact.has_business_emailistruewhen a business email for the person is on file. Filter on it with=or!=andtrueorfalse. Other operators return a400. The flag does not promise that Contact Enrich returns an email: Contact Enrich checks the address, and it can also find one when the flag isfalse. To filter on a verified business email at a role, useexperience.employment_details.current.business_email_verified,experience.employment_details.past.business_email_verified, orexperience.employment_details.business_email_verified.contact.has_personal_emailandcontact.has_phone_numberare not fields. A filter on either returns a400.- Search results include
social_handles.professional_network_identifier.profile_url, but the API rejects it as a filter. To look up a person by profile URL, use Person Enrich. - Some searchable fields, such as
certifications.*andhonors.title, are not in the response summary below. - A person can hold several current roles at the same time. Exactly one of them has
experience.employment_details.current.is_defaultset. Filter on this field to match people by their primary employer, not by any employer that they list. Thepast.path and the all-roles path work the same way. - To filter by employer profile ID, use
experience.employment_details.professional_network_id. To limit the filter to current or past roles, use thecurrent.orpast.path. Get the value frombasic_info.professional_network_idon Company Identify or Company Enrich, and send it as a string. If you have a Crustdata company ID instead, useexperience.employment_details.company_id.
Country filters do not all use the same value format. Use full country names
for person location fields. Employer headquarters country fields accept a
full country name or an ISO 3166-1 alpha-3 code.
Country and region value formats
Headquarters country filters accept an ISO 3166-1 alpha-3 code, such as
USA,
or a full country name, such as United States. Both forms return the same
people, and the response returns the full country name. For the full code list,
see the ISO 3166-1 alpha-3 country code list.
For basic_profile.location.country, use Autocomplete
to find the indexed full country names, then filter with = or in.
Website value formats
To filter on a website, use the
company_website_domain fields with a bare
domain and no scheme. The all-roles company_website filter matches the stored
URL exactly, including the scheme and any query string. A stored value can look
like https://example.com?utm_source=profile. So use the domain fields, unless
you need an exact URL match.
Search responses return the website as a full URL under
experience.employment_details.current.company_website or
past.company_website. The company_website_domain paths are filter names
only. They never appear in the search response.
Person Enrich does the reverse: it
returns the bare domain under company_website_domain.
Employer profile URL values
The API also accepts the alias
...company_linkedin_profile_url on both
paths. It matches the same data. You cannot filter on the bare all-employers
path.
These fields match the company page of the employer. You can send the page URL
in any usual form. Search accepts the URL:
- With or without the scheme.
- With or without
www.. - With a country subdomain, such as
de.linkedin.com. - With a trailing slash.
- With a query string, such as
?trk=.... - With a sub-path, such as
/about.
400
with the value that you sent. These values return 400:
- A school page (
/school/...). - A person profile (
/in/...). - A showcase page (
/showcase/...). - A bare slug, such as
"stripe". /company/with no slug after it.
education.schools.school. For one person, use
Person Enrich, which takes a profile
URL.
The API accepts an empty value. It matches the employers that have no company
page.
Boolean syntax in a value
The API matches a filter value literally. A filter value is not a query language. For example, the API searches for("development" OR "developer") as
one whole string. That string matches no profile, so search returns 400.
Write the OR as a group of conditions instead:
400 message contains this group, built from your field and operator. You
can copy it into your next request. For an in filter, put one term in each
element of the list instead.
This rule is narrow. OR and AND are ordinary words in stored values. So
[.] "Head of OR" and (.) "Sales And Marketing Specialist" still match
profiles. Search rejects a value only when the value has one of your own quote
characters next to an uppercase OR or AND. Search checks values under =,
in, has_all, (.), and [.]. These are the operators where a boolean
string matches nothing.
A value that joins its terms with AND gets the same 400, but with no group
to copy. You must decide what the AND means. An and group asks one employment
to match every condition. An all_of group asks for a different employment for
each condition. See
Nested-array matching.
Closed value sets
Five filter fields hold a value from a fixed list. A value outside the list, such as"Founder", "vp", or "1-10", matches no profile. So search returns
400 with the field name and the full list. The API ignores case, but nothing
else. It always rejects a value with a leading or trailing space, and part of a
value, such as "Vice". It never maps one value to another. For example,
"vp" stays rejected. It does not become "Vice President".
=, !=, in, and not_in ignore case. So "director", "DIRECTOR", and
["director"] all match "Director". Copy the values from the table below or
from Autocomplete.
Each employer field in the table applies to all three employment paths:
experience.employment_details., experience.employment_details.current., and
experience.employment_details.past..
These are the near misses that customers send most often:
"Founder"and"CEO"are not seniority levels. Those people have the index values"Owner / Partner"and"CXO"."Manager"is not a seniority level. The list has"Entry Level Manager"and"Experienced Manager".- A full job title is never a seniority level. So the API rejects
"Chief Executive Officer","Chief Financial Officer","Chief Revenue Officer","Senior Director", and"Managing Director". - The smallest headcount band is the phrase
"myself only". - The band below
"11-50"is"2-10", not"1-10". - Bands have no thousands separator. Send
"501-1000", never"501-1,000".
"" on function_category, company_headcount_range, and company_status
when the value for the employer is unknown. So = "" matches those profiles and
does not return 400.
You can read the current lists at any time with
Autocomplete.
Identity & metadata
Basic profile
Professional network
Skills
Experience: all employers
Experience: current employer
Experience: past employer
company_status is the lifecycle state of the employer in the Crustdata
entity graph. It is the same value that
Company Search returns as
basic_info.status. Every employment object has it. It has three values:active: The employer resolves to a live company record.deleted: The employer resolves to a company record that Crustdata retired.null: Crustdata did not resolve the employer to a company record. Only the free-text company name on the role is available.
experience.employment_details.company_status. To
check only current or only past roles, use the current. or past. path. A
profile matches when any role meets the condition. So is_null and
is_not_null both return large sets that overlap. A person with a mix of
resolved and unresolved employers matches both.Education
Certifications & honors
Social handles
Contact
GitHub dev platform (filter-only)
These fields filter and sort on the GitHub account that best matches the person. The field paths follow thedev_platform_profiles shape that
Dev Platform Enrich returns. You can
use them only to search. The search response does not include any of these
fields. Filter here, then get the values with Dev Platform Enrich.
Account-level fields describe the whole account. The API calculates the rollup
values (all_languages, all_topics, total_*, max_stars, and
median_stars) across all of the person’s owned repositories.
Repository-level fields match the 25 owned repositories that the person pushed
to most recently:
Example: hireable Rust developers with real traction
Example: hireable Rust developers with real traction
This request finds Rust developers with the GitHub “available for hire” flag
set. Each one has a repository with 500 or more stars and a code push on or
after 2026-07-01. The request sorts the results by the most-starred repository:The response has the profile fields in
Request body
fields, not the GitHub data. To see
the repositories and stats, call
Dev Platform Enrich on a match.Example: SF software engineers with 5+ years and a starred Rust repo
Example: SF software engineers with 5+ years and a starred Rust repo
You can combine GitHub filters with the regular profile filters. This request
finds software engineers who match all of these conditions:
- They are within 50 km of San Francisco.
- They have at least 5 years of experience.
- They have at least one repository whose primary language is Rust.
- They have more than 20 stars across all owned repositories.
(!) condition removes interns. Several internship roles can add up to
enough years to pass the years-of-experience filter:Request body
repos.primary_language matches only the 25 most recently pushed
repositories. To match a language in any of the person’s repositories, filter
on dev_platform_profiles.all_languages instead.The API checks each repository-level condition on its own, against any of
the 25 repositories. Forked repositories count. So a person whose only Rust
repository is a fork still matches. If you combine
repos.primary_language
with repos.is_fork, the two conditions can match different repositories.
This is also true inside all_of. The star rollups (total_stars and
max_stars) also include forks.Research profiles
These fields filter, sort, and return the person’s matched academic author profiles. The paths follow theresearch_profiles shape that
Person Enrich
returns. The group is enabled per account. Without the grant, a filter or a
fields entry on any research_profiles.* path returns 403 with
Access denied to fields: research_profiles. Contact Crustdata to turn it on.
Autocomplete supports
research_profiles.research_interests and
research_profiles.affiliated_organisation. Get the stored spelling there,
then filter with = or in.
A person can hold more than one author profile, so research_profiles is an
array, best match first. A filter matches when any of the person’s profiles
matches. Each metric object (citations_count, h_index, i10_index) has
all, recent, and recent_since_year. You can filter and sort on all and
recent. recent_since_year is returned but not filterable.
research_profiles.id, research_profiles.profile_url,
research_profiles.thumbnail, and the recent_since_year keys are returned
but not filterable. A filter on one of them returns 400 with
Unsupported filter field. The search copy of each profile is smaller than the
enrich copy: articles[] carries only title, url, and publication_year,
co_authors[] carries only id and name, and there is no
citations_timeseries. Get the full lists with
Person Enrich.
Profile assessment
These fields hold the Crustdata assessment of how well a profile passes verification. Filter on them to remove low-quality profiles before you spend an enrich credit. Person Enrich returns the values. Search does not.
You can sort on
tier only. A sort on verdict returns
Unsupported sort field(s).
Use
tier to remove low-quality profiles. Few untrusted profiles have a
verdict. Of the 25,434 profiles at tier 9, only 11 have a verdict. So a
verdict filter misses almost all of them. A null verdict means “not
labeled”, not “genuine”. To select on it, use is_null or is_not_null.Other
Open-to signal values
professional_network.open_to_cards holds codes from a fixed list of three. Filter with the in operator and one or more of these codes:
Filter example
Response fields
Each profile in the response hascrustdata_person_id, even when you do not request it. It can also include these sections, depending on fields. A field you request that has no data for the profile is null. This table shows only the returned sections. It is not a complete filter reference.
Person Search does not return
skills or dev_platform_profiles. Search
returns a smaller set of sections. These two sections are not in the
response for any account, whatever you send in fields. They are not
gated. They are just not part of search. Get skills with
Person Enrich and GitHub data with
Dev Platform Enrich. You can still
filter on skills.professional_network_skills and the
dev_platform_profiles.* GitHub fields. See
Filterable but not returned.Remarks
Every search response has aremarks array. It is [] when the API has no remark. A remark is a machine-readable note about the request that you sent.
In your code, branch on
code and path. Do not parse message or hint.
To explain an empty result, send "explain": true with a filters-only request.
When the search returns total_count 0, the API removes one top-level condition
at a time and counts again. It names each condition whose removal gets profiles
back. The request below returns nobody, and the API names two conditions:
- The nine exact titles. 22 people match without them.
- The company page URL. 422,469 people match without it.
- The API tests only the conditions of your top-level
and, one at a time, and at most six of them. A nestedorgroup counts as one condition. The API never opens it. The API tests exact-match conditions (=,in) on titles, company names, and website domains first. - The API can name several conditions. If no single condition explains the empty result, you get one
no_single_culpritremark. Its message says how many conditions the API tested. - The tests cost nothing. An empty result uses no credits, with or without
explain. - These requests return
remarks: []: a request with asearchblock, a request with results, and a cursor page. Error responses never includeremarks.
- Remove the title condition, and keep the rest of your filters.
- Put the title in
search.query, with the top-level"mode": "exact".
conditions list.
The title operators also differ more than they look. = and in match the whole stored string and ignore case, so ["head of procurement"] and ["Head of Procurement"] return the same profiles. [.] matches a phrase anywhere in the value and takes one string, not a list.
When the named condition is a company name or website domain under = or in, the explain step also looks for company records that are the same company:
- A domain counts when it is the registered domain with no subdomain, and a record has exactly that domain.
- A name counts when it equals a stored name after the API ignores case, punctuation, and legal suffixes (
Inc,Ltd,GmbH). SoQorvoreachesQorvo, Inc.but notQorvo China.
company_id in <those ids> added to the condition with OR. If that gets profiles back, the remark has a resolution. Your result does not change. To get those profiles, send the request again with the filter in resolution.field and resolution.value. For example, a search for rubrik.com returns nobody, because the stored domain is a different host. The remark names the company ID that has the profiles:
Remark
resolution, no record passed the rule, or no record got profiles back. The hint then gives the reason:
- The name or domain has no company record.
- The domain is a subdomain. The index does not hold subdomains.
- The stored form is different from yours.
matches_without, because a search without its only condition would count everyone.
Filter paths and their returned twins
Six employer filter paths return their data under a different path in the response. Filter on the path in the left column. Read the value from the path in the right column:
If you put a filter path in
fields, the API returns 400. The message gives
you the twin path to use instead:
experience.employment_details.company_name,
experience.employment_details.company_id, and
experience.employment_details.company_website_domain. If you put one of them
in fields, the API returns 400 with this error:
current. or past. twin above.
Filterable but not returned
You can use some fields infilters, but the search response does not include
them. Search is for discovery, not for full enrichment. Filter on these fields,
then get their values with Person Enrich.
The most common example is skills.professional_network_skills. You can filter
on it here, but only Enrich returns it. assessment.authenticity.* works the
same way. If you put it in fields here, the API returns 400 Invalid fields.
The reverse is true for a few fields. Search returns the person’s profile
URL (
social_handles.professional_network_identifier.profile_url), but you
cannot filter on it. To find people at a company, filter on the company URL
of the employer
(experience.employment_details.current.company_professional_network_profile_url)
instead.Request parameter reference
Preview mode
Preview mode
Preview search is a premium feature. To turn it on for your account, book a
demo.
preview: true to get smaller results before you run a full search. A preview response has the same top-level shape, but it can have fewer profile fields.
If your account does not have preview, the API returns 403 with this error:
Errors
No results
When no people match the filters, the API returns200 with an empty profiles array:
- Send the request again with
"explain": trueto learn which condition removed every result. See Remarks. - Use wider filters.
- Check the field values with Autocomplete.
To match many values of a field, put all of them in one
in condition, not
in many separate conditions. The API runs one in condition as one
efficient query. An in list of up to about 5,000 to 10,000 values returns
in a few seconds. Larger lists get slower, and the API rejects very large
lists of about 50,000 values or more. For a larger set, split the values
into chunks of about 5,000 to 10,000. Send one request for each chunk, and
merge the results. The request body has a hard limit of 10 MB. A larger
body returns 413 request_too_large. With typical payloads, you reach the
query-size limit first.API reference summary
Paginate through results
When your search matches more profiles than yourlimit, use the cursor to get every page.
For the first page, send your normal search request.
next_cursor value from the response and send it as cursor in your next request. Keep filters, sorts, search, and mode the same.
next_cursor is null. A null value means that you have the last page.
The cursor has these rules:
- The cursor is tied to the query that made it. Keep
filters,sorts,search, andmodethe same on every page. You can changelimit,fields, andsearch.explainon any page. The API does not reject every change. A filter-only search ties the cursor tofiltersandsorts. A search ranked on query text ties it tofilters,search, andmode, and does not acceptsorts. You cannot tell which of the two ran your request, because the parser can move a wholesearchblock into filters and leave no text to rank. So keep all four the same, and do not wait for an error.search.query_syntaxis tied only when you set it toboolean. - A cursor has no expiry time. It does not fail because it is old. But for a
query that has
search, the API keeps the result order for 20 minutes. After that, the API calculates the order again, and rows can move between pages. Get all the pages in one session. - Send the cursor back exactly as you received it. Treat it as an opaque
string. A re-encoded cursor, a removed
=padding character, or a changed character fails withInvalid cursor. There is one exception: a change to the last character can decode to the same value. So the absence of an error does not prove that your copy is correct.
Sort results
Use thesorts parameter to sort the results by a field. You need a sort for stable pagination.
crustdata_person_id, metadata.updated_at, basic_profile.name, basic_profile.location, basic_profile.location.city, basic_profile.location.state, basic_profile.location.country, basic_profile.location.full_location, professional_network.connections, professional_network.followers, experience.employment_details.start_date, experience.employment_details.company_id, experience.employment_details.company_headcount_latest, experience.employment_details.years_at_company_raw, recently_changed_jobs, years_of_experience_raw, assessment.authenticity.tier, dev_platform_profiles.followers, dev_platform_profiles.following, dev_platform_profiles.metadata.created_at, dev_platform_profiles.public_repo_count, dev_platform_profiles.total_repos, dev_platform_profiles.total_stars, dev_platform_profiles.max_stars, dev_platform_profiles.median_stars, research_profiles.article_count, research_profiles.co_author_count, research_profiles.citations_count.all, research_profiles.citations_count.recent, research_profiles.h_index.all, research_profiles.h_index.recent, research_profiles.i10_index.all, research_profiles.i10_index.recent, and research_profiles.metadata.last_scraped_source. A sort on a field that the API does not support returns 400 with Unsupported sort field(s). The research_profiles.* sort fields are enabled per account, see Research profiles.
On the dev_platform_profiles.* sort fields, profiles without a linked GitHub account come last, for both asc and desc.
The Sortable column in Searchable fields shows which
fields you can sort on. The Filterable column shows which fields you can filter
on.
For the complete OpenAPI schema, see the full API reference.
