API

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.

Before you start

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_ID

How 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.

KindFieldsWhat you pass
Free text

keywords, and the five scoped variants inside advanced_keywords (first_name, last_name, title, company, school)

Words. keywords spans the whole profile.

Id bearing

industry, location, current_company, past_company, school, service, connections_of, followers_of

An array of taxonomy ids (a plain word is resolved for you).

Enum or scalar

network_distance, profile_language, open_to_volunteering

Closed values; network_distance is 1, 2, or 3.

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 fieldtypeId shape
industryINDUSTRY

Numeric string, often short: 4

locationLOCATION

Numeric string, nine digits: 103035651

current_company, past_company

COMPANYNumeric string
schoolSCHOOLNumeric string
serviceSERVICENumeric string
connections_ofRELATION

Member id: ACoAA...

followers_ofPEOPLE

Member id: ACoAA...

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 optionsValue matched nothing
Status422200
Reported in

unresolved[] on the error body

notices[] on the result page

CodeFILTER_CANDIDATES_REQUIREDFILTER_VALUE_UNRESOLVED
Did the search runNoYes, with your value used as an id
FixRe-send with a chosen id

Look it up via GET .../search/parameters

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"]
  }'
BoundValueWhat you see at the edge
Matches per queryNot reported

total_count is null

End of resultsn/a

cursor is null; nothing else means the end

Upstream pages per request10

a page cut short carries a PAGE_TRUNCATED notice

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

SurfaceRoute prefixNeeds
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=29

Over it: 429 RATE_LIMIT_TENANT or RATE_LIMIT_ACCOUNT. See Rate limits.

Errors you will actually hit

CodeHTTPOn search it usually meansDo this
FILTER_CANDIDATES_REQUIRED422A filter value matched several entities.

Re-call with a picked id.

INVALID_REQUEST400

limit above 50, an unknown type, or no keywords on a resolution call.

The message names the field.

RESOURCE_NOT_FOUND404Usually a wrong path shape.

It is /v1/{account_id}/search/people.

ACCOUNT_NOT_FOUND404The account id is not yours.

Read your ids from GET /v1/accounts.

LINKEDIN_FEATURE_NOT_SUBSCRIBED403

A premium surface without the subscription, or reading a hidden person.

Use an entitled account; skip hidden.

NO_ACTIVE_SEAT403No active Curviate seat on the account.Buy or attach a seat in Billing, then retry.
RATE_LIMIT_TENANT429Too many requests.

Honour Retry-After.

LINKEDIN_RATE_LIMITED429LinkedIn is throttling the account.Slow the whole job.
BUDGET_EXHAUSTED429

Not a rate limit: the account-safety search ceiling (result lines) is spent, or outside working hours.

See Account safety.

An empty result set is not an error: check notices and that cursor is null before concluding nobody matched.

Next steps