Skip to main content
This page is the reference for Person Search. It lists:
  • 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.
For examples, see Person Search and its example requests.
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:
  • experience
  • education
  • skills
  • certifications
  • honors
  • The professional_network signals, with languages and summary
  • The filter-only dev_platform_profiles fields
For the prices, see Premium fields in search. For your own prices, see /account/endpoints.

Filter operator reference

Person Search accepts the following filters.type operators.
Use => for greater than or equal and =< for less than or equal. The API does not support >= and <=.
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 = or in. Autocomplete covers title, name, company_industries, seniority_level, and function_category on 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 from experience.employment_details.current.name, then filter on experience.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 an and group:

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.
If you send both, the API uses 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.
For a full request, see 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.
A polygon with fewer than 3 distinct points, a malformed pair, crossing edges, or an out-of-range coordinate returns 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:
On nested-array fields, the check applies to each element:
  • is_null matches profiles where no element has the field, for example an empty experience.employment_details.current array.
  • is_not_null matches profiles where at least one element has a non-null value.
Combine the two operators to match on which fields a profile has:
This request finds people with no current employer and at least one past employer:
Request
The response has the standard Person Search shape.
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.
A plain 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 an and 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:
  • or for people who match either title.
  • all_of for people who held one title in one job and the other in another job. See the all_of section below.
  • and for 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:
Cross element: Engineer at one company and Manager at another company, in different jobs:
You can put 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:
The API expands this to an 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, like and and or. The API rejects allOf and other casings.
  • All fields in one all_of condition, or in its and or or group, 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 separate all_of conditions.
  • all_of and has_all work 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_of accept positive operators only. The API rejects the negation operators !=, not_in, (!), and geo_exclude there. Apply negation at the document level instead.
  • Do not put a has_all condition or another all_of group inside an all_of group. Expand it into = conditions instead.
  • has_all needs 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, use experience.employment_details.current.title. For the employer paths, see Filter paths and their returned twins.
  • contact.has_business_email is true when a business email for the person is on file. Filter on it with = or != and true or false. Other operators return a 400. 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 is false. To filter on a verified business email at a role, use experience.employment_details.current.business_email_verified, experience.employment_details.past.business_email_verified, or experience.employment_details.business_email_verified.
  • contact.has_personal_email and contact.has_phone_number are not fields. A filter on either returns a 400.
  • 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.* and honors.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_default set. Filter on this field to match people by their primary employer, not by any employer that they list. The past. 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 the current. or past. path. Get the value from basic_info.professional_network_id on Company Identify or Company Enrich, and send it as a string. If you have a Crustdata company ID instead, use experience.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.
The case of the slug does not matter. Search converts each of these forms to the stored form before it matches. So each form returns the same profiles. A URL that is not a company page matches no profile. So search returns 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.
For a school, filter on 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:
The 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".
The API accepts one value outside the lists: the empty string. The index stores "" 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.
You can filter on it on all three employment paths. To match a person by any role that they held, use 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 the dev_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:
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:
Request body
The response has the profile fields in fields, not the GitHub data. To see the repositories and stats, call Dev Platform Enrich on a match.
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.
The (!) 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 the research_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:
The index holds only the three codes above. A human-readable string, such as "open_to_work" or "Open to Work", returns 400 with the list of the three codes. The match ignores case, so "career_interest" also works.
Filter example

Response fields

Each profile in the response has crustdata_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 a remarks 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.
Each condition removes everyone that the other conditions find. So each one is a culprit on its own: a condition that empties the result by itself.
How the explain step works:
  • The API tests only the conditions of your top-level and, one at a time, and at most six of them. A nested or group 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_culprit remark. 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 a search block, a request with results, and a cursor page. Error responses never include remarks.
When the named condition is a title: a title is free text. So a title condition that empties the result usually means that the people in scope (the people that your other filters select) have the role under other words. It does not mean that nobody there does the job. Do these steps:
  1. Remove the title condition, and keep the rest of your filters.
  2. Put the title in search.query, with the top-level "mode": "exact".
The search then ranks the people in scope by meaning, not by string match. If you remove the last condition of a group, remove the group too, because the API rejects an empty 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). So Qorvo reaches Qorvo, Inc. but not Qorvo China.
The API then counts again with 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
If a remark has no 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.
A remark on a request with one condition has no 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:
Three all-employers paths filter across every job, but the API never returns them: 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:
Filter on these three paths when you do not care which job matched. Then read the value from the current. or past. twin above.

Filterable but not returned

You can use some fields in filters, 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.
If your account has preview, send 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 returns 200 with an empty profiles array:
To fix an empty result, do one of these:
  • Send the request again with "explain": true to 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 your limit, use the cursor to get every page. For the first page, send your normal search request.
For the next page, take the next_cursor value from the response and send it as cursor in your next request. Keep filters, sorts, search, and mode the same.
Continue until next_cursor is null. A null value means that you have the last page.
When you paginate, always send sorts so that the order stays the same across pages.
The cursor has these rules:
  • The cursor is tied to the query that made it. Keep filters, sorts, search, and mode the same on every page. You can change limit, fields, and search.explain on any page. The API does not reject every change. A filter-only search ties the cursor to filters and sorts. A search ranked on query text ties it to filters, search, and mode, and does not accept sorts. You cannot tell which of the two ran your request, because the parser can move a whole search block into filters and leave no text to rank. So keep all four the same, and do not wait for an error. search.query_syntax is tied only when you set it to boolean.
  • 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 with Invalid 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 the sorts parameter to sort the results by a field. You need a sort for stable pagination.
You can sort on these fields: 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.