Search
LinkedIn filters on identifiers, not words. Resolve a name to its id, search with the id, and page with the cursor until it is null.
You need an API key and a connected account's acc_ id (read it
with GET /v1/accounts, or start with the
Quickstart). Core
search works on every connected account.
export CURVIATE_API_KEY=cvt_live_YOUR_API_KEY
export ACCOUNT_ID=acc_YOUR_ACCOUNT_IDHow filters compose
Fields combine with AND, values inside one field with OR. There is no grouping, negation, or operator syntax.
{
"keywords": "platform engineer",
"industry": ["4"],
"location": ["103035651", "90009712"],
"network_distance": [1, 2]
}Read: platform engineer AND industry 4 AND (location 103035651 OR
90009712) AND 1st or 2nd degree.
| Kind | Fields | What you pass |
|---|---|---|
| Free text |
| Words. |
| Id bearing |
| An array of taxonomy ids (a plain word is resolved for you). |
| Enum or scalar |
| Closed values; |
advanced_keywords.company matches a name string; current_company a company
entity by id. Results differ.
Resolving a name to an id
curl -sG "https://api.curviate.com/v1/$ACCOUNT_ID/search/parameters" \
-H "Authorization: Bearer $CURVIATE_API_KEY" \
--data-urlencode "type=INDUSTRY" \
--data-urlencode "keywords=Software Development" \
--data-urlencode "limit=5"{
"object": "search_parameter_list",
"items": [
{ "id": "4", "name": "Software Development" },
{ "id": "3102", "name": "IT System Custom Software Development" }
],
"cursor": null
}type is required and closed: LOCATION, PEOPLE, RELATION, COMPANY,
SCHOOL, INDUSTRY, SERVICE, JOB_FUNCTION, JOB_TITLE, EMPLOYMENT_TYPE,
SKILL. Anything else is rejected:
{
"code": "INVALID_REQUEST",
"message": "type: Invalid enum value. Expected 'LOCATION' | 'PEOPLE' | 'RELATION' | 'COMPANY' | 'SCHOOL' | 'INDUSTRY' | 'SERVICE' | 'JOB_FUNCTION' | 'JOB_TITLE' | 'EMPLOYMENT_TYPE' | 'SKILL', received 'GROUPS'",
"retry_hint": null,
"user_fixable": true,
"retry_likely_to_succeed": false
}keywords is required; a whole category cannot be listed.
| Filter field | type | Id shape |
|---|---|---|
industry | INDUSTRY | Numeric string, often short: |
location | LOCATION | Numeric string, nine digits: |
| COMPANY | Numeric string |
school | SCHOOL | Numeric string |
service | SERVICE | Numeric string |
connections_of | RELATION | Member id: |
followers_of | PEOPLE | Member id: |
An expected entry may be past page one: follow cursor, raise limit (up to
100 here), or sharpen keywords. Resolve once and keep the id.
FILTER_CANDIDATES_REQUIRED
A plain string in an id-bearing filter is resolved before the search runs: exact match after case folding and trimming, never fuzzy. A value matching several entries stops the search:
# location is a human word, not a filter id.
curl -s -X POST "https://api.curviate.com/v1/$ACCOUNT_ID/search/people?limit=3" \
-H "Authorization: Bearer $CURVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"keywords": "platform engineer", "location": ["Berlin"]}'{
"code": "FILTER_CANDIDATES_REQUIRED",
"message": "These filter values matched more than one option: Berlin.",
"user_fixable": true,
"retry_likely_to_succeed": true,
"unresolved": [
{
"field": "location",
"value": "Berlin",
"candidates": [
{ "id": "103035651", "name": "Berlin, Germany" },
{ "id": "106967730", "name": "Berlin, Berlin, Germany" },
{ "id": "90009712", "name": "Berlin Metropolitan Area" }
]
}
],
"next_action": "Re-call passing the picked ids for the values listed in unresolved."
}That is HTTP 422. Re-send with the value replaced by an id from its
candidates, such as ["103035651"]. Ids pass straight through, and every
unresolved value is listed at once:
{
"code": "FILTER_CANDIDATES_REQUIRED",
"message": "These filter values matched more than one option: AI, Berlin.",
"unresolved": [
{
"field": "industry",
"value": "AI",
"candidates": [
{ "id": "94", "name": "Airlines and Aviation" },
{ "id": "2366", "name": "Air, Water, and Waste Program Management" },
{ "id": "404", "name": "Steam and Air-Conditioning Supply" },
{ "id": "398", "name": "Water, Waste, Steam, and Air Conditioning Services" }
]
},
{
"field": "location",
"value": "Berlin",
"candidates": [
{ "id": "103035651", "name": "Berlin, Germany" },
{ "id": "106967730", "name": "Berlin, Berlin, Germany" },
{ "id": "90009712", "name": "Berlin Metropolitan Area" }
]
}
],
"next_action": "One or more filter values matched several options. Re-call passing the picked ids for the values listed in unresolved."
}Absurd candidates, as for "AI", mean retry with the full term
(Artificial Intelligence), not pick. Clean resolutions are cached per tenant for
up to 24 hours. A refused request still counts against your rate limit.
On MCP (https://app.curviate.com/mcp, see
MCP client setup) it is a tool error with the same
unresolved and next_action. Re-call with the picked id:
{
"name": "search_people",
"arguments": {
"account_id": "acc_YOUR_ACCOUNT_ID",
"keywords": "platform engineer",
"industry": "Software Development",
"location": "103035651",
"limit": 3
}
}A 422 or a notice
A value matching nothing is sent on as an id (a real id matches no name) and
reported in notices[]:
| Value matched several options | Value matched nothing | |
|---|---|---|
| Status | 422 | 200 |
| Reported in |
|
|
| Code | FILTER_CANDIDATES_REQUIRED | FILTER_VALUE_UNRESOLVED |
| Did the search run | No | Yes, with your value used as an id |
| Fix | Re-send with a chosen id | Look it up via |
One of each returns the 422, with the notice on the same body.
FILTER_VALUE_UNCHECKED flags an id-shaped value on a page that came back empty.
{
"object": "people_search_result",
"items": [],
"paging": { "total_count": null },
"cursor": null,
"notices": [
{
"code": "FILTER_VALUE_UNRESOLVED",
"message": "The location value \"Berlim\" matched no known filter option, so it was sent on as an id we could not check. If these results are not what you expected, look the value up with GET /v1/{account_id}/search/parameters and re-send the id it returns.",
"field": "location",
"value": "Berlim"
}
]
}Worked example
Industry is 4 (above). Resolve the location:
curl -sG "https://api.curviate.com/v1/$ACCOUNT_ID/search/parameters" \
-H "Authorization: Bearer $CURVIATE_API_KEY" \
--data-urlencode "type=LOCATION" \
--data-urlencode "keywords=Berlin" \
--data-urlencode "limit=5"{
"object": "search_parameter_list",
"items": [
{ "id": "103035651", "name": "Berlin, Germany" },
{ "id": "106967730", "name": "Berlin, Berlin, Germany" },
{ "id": "90009712", "name": "Berlin Metropolitan Area" },
{ "id": "105506608", "name": "Berlin, Connecticut, United States" },
{ "id": "107184029", "name": "Berlin, Maryland, United States" }
],
"cursor": "NQ"
}Pick the city, 103035651, and search:
curl -s -X POST "https://api.curviate.com/v1/$ACCOUNT_ID/search/people?limit=3" \
-H "Authorization: Bearer $CURVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keywords": "platform engineer",
"industry": ["4"],
"location": ["103035651"]
}'{
"object": "people_search_result",
"items": [
{
"id": "ACoAAExamplePersonOne000000000000000000",
"member_id": "481920367",
"public_identifier": "jordan-avery-example",
"full_name": "Jordan Avery",
"headline": "Platform Engineer, Developer Experience",
"location": "Berlin",
"avatar_url": "https://media.example.com/dms/image/v2/EXAMPLE/profile-displayphoto-shrink_100_100/0",
"profile_picture_url_large": "https://media.example.com/dms/image/v2/EXAMPLE/profile-displayphoto-shrink_800_800/0",
"profile_url": "https://www.linkedin.com/in/jordan-avery-example",
"network_distance": "OUT_OF_NETWORK",
"is_premium": true,
"visibility": "full"
},
{
"id": "ACoAAExamplePersonTwo000000000000000000",
"full_name": "LinkedIn Member",
"headline": "Site Reliability Engineer, Infrastructure Platform",
"location": "Berlin",
"avatar_url": "https://media.example.com/dms/image/v2/EXAMPLE/profile-displayphoto-shrink_100_100/1",
"profile_picture_url_large": "https://media.example.com/dms/image/v2/EXAMPLE/profile-displayphoto-shrink_800_800/1",
"network_distance": "OUT_OF_NETWORK",
"visibility": "hidden"
}
],
"paging": { "total_count": null },
"cursor": "Mw",
"notices": [
{
"code": "SOME_RESULTS_HIDDEN",
"message": "1 of the 2 results on this page carry no identity this account can see, so those entries cannot be opened, read back, or contacted. The connected LinkedIn account's own subscription level limits which profiles it is allowed to identify. Read each item's visibility field to tell them apart."
}
]
}Branch on visibility, not the name. A hidden item (LinkedIn Member, no
member_id, public_identifier, or profile_url) is a real person the account's
LinkedIn subscription cannot identify.
Notices: SOME_RESULTS_HIDDEN, ALL_RESULTS_HIDDEN (still a 200), or
PAGE_TRUNCATED (we stopped fetching early).
network_distance reads back as a string (OUT_OF_NETWORK), unrelated to
visibility.
id on a full result feeds a profile read, invitation, or new chat. Reading
a hidden one returns 403 LINKEDIN_FEATURE_NOT_SUBSCRIBED.
Pagination
limit (query string) is 1 to 50, default 10; over 50 errors:
{
"code": "INVALID_REQUEST",
"message": "limit: Number must be less than or equal to 50",
"retry_hint": null,
"user_fixable": true,
"retry_likely_to_succeed": false
}Send cursor back with the same body. It is opaque and tied to its query.
curl -s -X POST "https://api.curviate.com/v1/$ACCOUNT_ID/search/people?limit=3&cursor=Mw" \
-H "Authorization: Bearer $CURVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keywords": "platform engineer",
"industry": ["4"],
"location": ["103035651"]
}'| Bound | Value | What you see at the edge |
|---|---|---|
| Matches per query | Not reported |
|
| End of results | n/a |
|
| Upstream pages per request | 10 | a page cut short carries a |
A short page can have more behind the cursor, so while (items.length) stops
early. Deep offset (offset=200) returns an empty page with a live cursor and
can spend the upstream limit into a 429; split the query (city by city) instead.
Entitlement and quota
| Surface | Route prefix | Needs |
|---|---|---|
| Core search | /v1/{account_id}/search/... | Any connected account |
| Sales Navigator search | /v1/{account_id}/sales-navigator/search/... | A live Sales Navigator subscription on the LinkedIn account |
| Recruiter search | /v1/{account_id}/recruiter/search/... | A live Recruiter subscription on the LinkedIn account |
Without it: 403. Only Recruiter names the product, so go by the route:
{
"code": "LINKEDIN_FEATURE_NOT_SUBSCRIBED",
"message": "This account is missing the Recruiter subscription required for this operation.",
"retry_hint": null,
"user_fixable": true,
"retry_likely_to_succeed": false
}{
"code": "LINKEDIN_FEATURE_NOT_SUBSCRIBED",
"message": "The connected LinkedIn account is missing the subscription required for this operation.",
"retry_hint": null,
"user_fixable": true,
"retry_likely_to_succeed": false
}Do not retry; subscribe or use another account. NO_ACTIVE_SEAT (also 403) is
a Curviate billing fix. Search spends ordinary request budget:
RateLimit-Policy: "tenant";q=2500;w=60
RateLimit: "tenant";r=2499;t=29Over it: 429 RATE_LIMIT_TENANT or RATE_LIMIT_ACCOUNT. See
Rate limits.
Errors you will actually hit
| Code | HTTP | On search it usually means | Do this |
|---|---|---|---|
FILTER_CANDIDATES_REQUIRED | 422 | A filter value matched several entities. | Re-call with a picked id. |
INVALID_REQUEST | 400 |
| The |
RESOURCE_NOT_FOUND | 404 | Usually a wrong path shape. | It is |
ACCOUNT_NOT_FOUND | 404 | The account id is not yours. | Read your ids from |
LINKEDIN_FEATURE_NOT_SUBSCRIBED | 403 | A premium surface without the subscription, or reading a hidden person. | Use an entitled account; skip |
NO_ACTIVE_SEAT | 403 | No active Curviate seat on the account. | Buy or attach a seat in Billing, then retry. |
RATE_LIMIT_TENANT | 429 | Too many requests. | Honour |
LINKEDIN_RATE_LIMITED | 429 | LinkedIn is throttling the account. | Slow the whole job. |
BUDGET_EXHAUSTED | 429 | Not a rate limit: the account-safety | See Account safety. |
An empty result set is not an error: check notices and that cursor is null
before concluding nobody matched.