Search
Search is the most complex surface Curviate exposes, and almost all of that complexity is one idea: LinkedIn does not filter on words, it filters on identifiers. This page teaches the model behind the parameters, so that when a call comes back empty or refuses to run you already know which of the two things happened and what the next call is.
You need two things: an API key, and at least one connected LinkedIn account
whose acc_ id you have. Every search route is account scoped,
so without an account id there is no search to make. Get the key from the
key chip in the dashboard top bar and the account id from
GET /v1/accounts; if you have not connected an account yet,
start with
Authentication and accounts
. Everything on this page runs against the core search surface, which every connected account has. Sales Navigator and Recruiter search are separate surfaces with their own entitlement, covered under "Tier entitlement and quota" below.
The examples use two shell variables so you can paste them as written:
export CURVIATE_API_KEY=cvt_live_YOUR_API_KEY
export ACCOUNT_ID=acc_YOUR_ACCOUNT_IDThe model in one paragraph
A search has three moving parts. A surface decides which index you are querying and which filter vocabulary applies (core people search, company search, job search, and so on). Filters narrow that index, and they come in two kinds that behave completely differently: free-text filters take the words you type, and id-bearing filters take an opaque identifier and nothing else. The taxonomy is LinkedIn's own controlled list of those identifiers, one list per filter category, and it is the thing you resolve against before you can filter. Almost every surprise in search traces back to treating an id-bearing filter as if it were a free-text one.
How filters compose
Fields combine with AND. Values inside one field combine with OR. There is no grouping, no negation, and no operator syntax; the shape of the body is the whole query language.
{
"keywords": "platform engineer",
"industry": ["4"],
"location": ["103035651", "90009712"],
"network_distance": [1, 2]
}That reads as: matches the free text platform engineer, AND is in industry
4, AND is in location 103035651 OR 90009712, AND is a 1st OR 2nd degree
connection. Adding a field can only ever shrink the result set.
Three kinds of field appear in a people search body:
| Kind | Fields | What you pass |
|---|---|---|
| Free text |
| The words themselves. |
| Id bearing |
| An array of opaque taxonomy ids. A human readable word is resolved for you, or you are told which options to pick from. |
| Enum or scalar |
| Fixed values from a closed set. |
Note which of your terms is really free text
advanced_keywords.company matches a company
name string and is free text. current_company filters
on a company entity and needs its id. They are not two spellings of
one filter, and they do not return the same people.
Taxonomy resolution
An id-bearing filter accepts one thing: the identifier LinkedIn's taxonomy gives
that entity. "Software Development" is not that identifier. "4" is.
Resolution has its own endpoint. It takes a category and a human term, and returns the candidate entries with their ids:
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. The accepted values are LOCATION, PEOPLE,
RELATION, COMPANY, SCHOOL, INDUSTRY, SERVICE, JOB_FUNCTION,
JOB_TITLE, EMPLOYMENT_TYPE, and SKILL. Anything else is rejected before
the call runs:
{
"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 too, for every type. There is no way to enumerate a whole
category; you always resolve a term you already have in mind.
Which filter resolves against which type
| 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: |
Why a wrong id is worse than a wrong word
An empty page is never left unexplained
Putting a human readable string in an id-bearing filter does not run a search
that quietly finds nobody. The value is resolved before the search runs. If
it matches several options you get a 422 naming the offending
field and listing the ids to pick from. If it matches no option at all we
cannot tell whether you sent a typo or an id we simply do not recognise, so
the value is sent on as an id and named in notices on the
200. Either way an empty items array is explained:
it means nobody matched, unless a notice tells you one of your filters was
never verified.
# 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 the same query with location set to a chosen id,
such as ["103035651"], and you get a full page. A value that matches nothing at
all, rather than several things, is not an error; see the section below.
FILTER_CANDIDATES_REQUIRED
Every structured search surface resolves id-bearing filter values for you before
the search runs: you may pass a plain string such as "Software Development"
instead of looking its id up first. When a value cannot be resolved to exactly
one entry, the search does not run and FILTER_CANDIDATES_REQUIRED is raised
instead, carrying the candidates you can choose from.
This is one error with one payload, on both surfaces. On REST it arrives as
HTTP 422 with the body shown above. On the MCP tools, which expose search as
tools rather than routes, it arrives as a tool error carrying the same
unresolved and next_action fields. Anything below that describes the shape
applies to both.
Where the MCP server actually is. It is not on api.curviate.com, the host
every other call on this page uses. It is a separate Streamable HTTP MCP
endpoint at https://app.curviate.com/api/mcp/v1 (also reachable at the
shorter https://app.curviate.com/mcp). Point your MCP client at that URL and
authenticate the same way as REST, Authorization: Bearer $CURVIATE_API_KEY.
An unauthenticated request to it returns 401 UNAUTHORIZED rather than 404,
which is how you can tell you have reached it.
What it means. One or more of your filter values did not map to a single taxonomy entry. The search itself did not run, and no partial result was returned. The attempt is still a request, so it counts against your rate limit like any other; resolving before you call, rather than after a rejection, is what keeps a busy loop cheap.
Why it is raised rather than guessed. Resolution is deterministic and
strict: it accepts an exact name match, or an exact match after case folding and
whitespace trimming, and nothing else. There is no fuzzy matching and no
semantic matching, by design. "Berlin" is not the name of any location entry,
so it resolves to zero exact matches out of ten plausible looking ones. Silently
picking the first would quietly change the meaning of your query, and returning
zero results would hide the problem entirely. So the request is refused and the
decision is handed back to you.
What it looks like. This is a real response, captured live. The intent was platform engineers in Berlin working in software development:
{
"code": "FILTER_CANDIDATES_REQUIRED",
"message": "These filter values matched more than one option: Berlin.",
"retry_hint": null,
"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" },
{ "id": "105506608", "name": "Berlin, Connecticut, United States" },
{ "id": "107184029", "name": "Berlin, Maryland, United States" },
{ "id": "107096287", "name": "Berlin, New Jersey, United States" },
{ "id": "101301095", "name": "10115, Berlin, Berlin, Germany" },
{ "id": "105606863", "name": "Berlin, New Hampshire, United States" },
{ "id": "104944500", "name": "10178, Berlin, Berlin, Germany" },
{ "id": "110428803", "name": "Berlin, Wisconsin, United States" }
]
}
],
"correlation_id": "mcp_cdd89fd6a3753f77b1402d17",
"next_action": "One or more filter values matched several options. Re-call passing the picked ids for the values listed in unresolved."
}Note what did not fail. industry: "Software Development" resolved cleanly
to 4 and is absent from unresolved. Only the value that needs a human
decision is listed.
The exact next call. Repeat the identical tool call with the offending value
replaced by an id you picked from its own candidates array. Everything else
stays as it was. These are the tool call arguments; in a chat client you would
simply say which of the candidates you meant:
{
"name": "search_people",
"arguments": {
"account_id": "acc_YOUR_ACCOUNT_ID",
"keywords": "platform engineer",
"industry": "Software Development",
"location": "103035651",
"limit": 3
}
}That call returns a result set. An id is recognised as already resolved and passes straight through, so a filter body can mix picked ids and plain strings freely, and re-calling is one substitution rather than a rewrite.
It reports every unresolved value at once
Resolution never stops at the first problem. Every id-bearing value is attempted, and a single error lists all of them, each with its own candidates. Two unhelpful values produce one round trip, not two:
{
"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."
}"AI" is the instructive one. Its candidates are airlines and air conditioning,
because the taxonomy has no entry abbreviated AI and the lookup fell back to
matching those two letters inside longer names. Reading those candidates and
picking one would produce a confidently wrong query. The right move when the
candidates look absurd is not to pick from them; it is to search again with the
full term the taxonomy actually uses, such as Artificial Intelligence. Prefer
full terms over abbreviations everywhere in search for the same reason.
A value that resolves cleanly is cached per workspace for up to 24 hours, so
a repeated industry: "Software Development" costs no extra lookup on
later calls. Values that need a pick are never cached, because the pick is
yours, not the resolver's.
When it is a 422 and when it is a notice
Two different things can happen to a filter value we cannot turn into exactly one id, and they are handled differently because we know different things about them.
A value that matched several options is unambiguously your call to make, so it
is an error and you pick. A value that matched nothing is not proof you sent
something invalid. GET .../search/parameters is a name index: it answers "what
is called this", and it cannot answer "is this a valid id". Ids never look like
names, so a real id matches nothing there, exactly as a typo does. Rejecting
what it does not recognise would reject the very ids the 422 above tells you
to re-send. So the value is sent on unchanged and reported instead.
| Value matched several options | Value matched nothing | |
|---|---|---|
| Status | 422 | 200 |
| Where it is reported |
|
|
| Code | FILTER_CANDIDATES_REQUIRED | FILTER_VALUE_UNRESOLVED |
| It means | Pick one of these | We could not check this, so we sent it as you wrote it |
| Did the search run | No | Yes, with your value used as an id |
| How you fix it | Re-send with a chosen id | Nothing, if the results look right. Otherwise look the value up with
|
Both report every offending value in one response rather than stopping at
the first, so a single correction round trip is enough. A request carrying one
of each returns the 422, and the value that matched nothing rides along in
notices[] on that same body, so you still see both problems at once.
There is a second notice code, FILTER_VALUE_UNCHECKED. It appears when a
value already looked like an id, so no lookup was attempted, and the page came
back empty. A non-existent id returns exactly the same empty page as a search
that genuinely has no matches, so the notice tells you which of the two you
might be looking at. It never appears on a page that has results.
{
"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, end to end
The intent, in plain language: find platform engineers in Berlin who work in software development. Three of those five terms are id-bearing, so the intent cannot be sent as written. Here is the whole path.
Step 1: turn the words into ids
Two resolution calls, one per id-bearing term.
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
}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"
}This is the step that has to be a decision, not a lookup. Berlin, Germany is
the city. Berlin Metropolitan Area is wider and will include commuter towns.
Berlin, Connecticut, United States is a different continent. Nothing in the
API can make this choice for you, which is the whole reason the MCP tools stop
and ask instead of guessing. Take the city: 103035651.
The cursor on that response is worth noticing: there were more than five
matching locations. When the entry you expect is not in the first page, raise
limit (up to 100 on this resolution endpoint, /search/parameters, a
different bound from the search endpoint's page size below) or make keywords
more specific, rather than assuming it is absent.
Step 2: run the search with the ids
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."
}
]
}Step 3: read the result set honestly
Three things in that response routinely trip people up.
visibility tells you which results you can actually use, and it is always
present. The second item is hidden: it has no member_id, no
public_identifier, and no profile_url, and its full_name is the literal
placeholder LinkedIn Member. That is a real person LinkedIn did not disclose
to the connected account, not a bug and not a deleted profile. The cause is the
connected LinkedIn account's own subscription level, which decides which
profiles it is allowed to identify, so the same query run from a different
account can return a different mix. Branch on visibility, never on the name
string, which is a display label that changes with the viewer's language.
Count the full results. total_count is null on people search, so
there is no total to read, and the size of a page says nothing about how many
of its results you can act on. On an account with limited visibility a page can
be entirely hidden, and that is still a 200: it is a truthful answer, not an
error. The notices[] array on the response says so directly,
with SOME_RESULTS_HIDDEN when part of the page is hidden and
ALL_RESULTS_HIDDEN when none of it can be used. A third code,
PAGE_TRUNCATED, means the page is short for an unrelated reason: we stopped
fetching, not LinkedIn. Branch on the code.
network_distance is a string, not the number you filtered on. You filter
with network_distance: [1, 2] and you read back values like
OUT_OF_NETWORK. They are not the same vocabulary, and a value of
OUT_OF_NETWORK says nothing about whether the result is usable: plenty of
out-of-network results are full.
id is the handle for everything downstream, on the full results. Feed it
straight into a profile read, a connection request, or a new chat; no second
lookup is needed. On a hidden result there is nothing to feed: a profile read
against it returns 403 LINKEDIN_FEATURE_NOT_SUBSCRIBED, because the account
that ran the search is not entitled to see that person.
Pagination
Pass limit on the query string, between 1 and 50, defaulting to 10. Over 50 is
a validation error rather than a silent clamp:
{
"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
}Every response carries a cursor. Send it back as a query parameter, with the
same body, to get the next page:
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"]
}'The cursor is opaque. Do not parse it, do not build one, and do not carry one across a change of filters; a cursor only means anything against the query that produced it.
The bounds
| Bound | Value | What you see at the edge |
|---|---|---|
| Page size | 1 to 50 |
|
| Matches per query | Not reported |
|
| End of results | n/a |
|
| Upstream pages per request | 10 | a page cut short by this carries a |
Not knowing the size of the result set is the thing that changes how you design
a job. cursor coming back null is the only end signal there is. Walk until
it does, and do not read anything into a short or empty page: one can arrive
with results still waiting behind the cursor, and when it does it carries a
PAGE_TRUNCATED notice saying so. A loop written as while (items.length)
stops early on that page and reports no results when there were results.
That page exists because we assemble yours by fetching upstream pages until
your limit is filled, and one request fetches at most ten of them. A deep
offset spends that budget skipping rather than collecting: offset=200
returns an empty page with a live cursor, and takes several requests to reach
the results, most of them returning nothing. Those upstream fetches are also
what LinkedIn rate limits, so one deep-offset request can spend the whole
per-window allowance and leave the next one with a 429.
So page forward with cursor and treat offset as a way to resume at a
position you already reached, not a way to jump to a distant one. A single
query also stops serving pages well before it has shown you everyone who
matches, so for broad coverage split the query along a filter axis (city by
city, industry by industry) rather than paging deeper.
Tier entitlement and quota
There are three separate search surfaces, and having a connected account only entitles you to the first.
| 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 itself |
| Recruiter search | /v1/{account_id}/recruiter/search/... | A live Recruiter subscription on the LinkedIn account itself |
Calling a premium surface from an account without the subscription is a 403.
For Recruiter, the message names which surface is missing:
{
"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
}For Sales Navigator, it does not name the surface. The body is the same
code, but a generic message naming the LinkedIn account without naming
which product it lacks:
{
"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
}Use the route you called, not the message, to tell the two apart. Either way,
do not retry it. Nothing about the request is wrong, and no backoff will help;
the fix is a subscription on the LinkedIn account, or a different account.
TIER_NOT_ACTIVE is the neighbouring error with the same HTTP status and a
different cause: that one is the Curviate side seat add-on, and it carries a
required_tier field naming what to enable. The two codes are the branch
point, because the remedies are opposite: TIER_NOT_ACTIVE is settled in your
Curviate billing, LINKEDIN_FEATURE_NOT_SUBSCRIBED on LinkedIn itself.
The same LINKEDIN_FEATURE_NOT_SUBSCRIBED code also comes back from a plain
core profile read, with no premium surface involved, when the person being read
is one the connected account is not entitled to see. Those are the results core
search returns with visibility: "hidden", and reading one back is the one
case where a 403 here is expected rather than a misconfiguration. Skip them
instead of retrying: no request shape makes a hidden person readable from an
account that cannot see them.
What search costs you
Search has no separate daily allowance. What it consumes is ordinary request budget, and every authenticated response tells you where you stand:
RateLimit-Policy: "tenant";q=2500;w=60
RateLimit: "tenant";r=2499;t=29q is the workspace allowance per window, w the window in seconds, r what
is left, and t the seconds until reset. Exceeding it is 429 with
RATE_LIMIT_TENANT, or RATE_LIMIT_ACCOUNT when a single LinkedIn account is
the one running hot. Read the headers and pace yourself rather than discovering
the ceiling; the algorithm to do that is on
Rate limits.
Two things to budget for that are easy to miss. Resolution calls count.
GET .../search/parameters is a request like any other, so a naive loop that
re-resolves the same city on every iteration can spend more budget on lookups
than on searches. Resolve once and keep the id. A page is a request. Walking a
thousand people at limit=50 is twenty calls, plus whatever resolution it took
to get there.
Errors you will actually hit
| Code | HTTP | On search it usually means | Do this |
|---|---|---|---|
FILTER_CANDIDATES_REQUIRED | 422 | A filter value did not resolve to exactly one entity. | Pick an id from that value's own |
INVALID_REQUEST | 400 |
| The |
RESOURCE_NOT_FOUND | 404 | The path did not match a route. On search this is nearly always a wrong path shape rather than a wrong id. | Check the shape before the ids: it is
|
ACCOUNT_NOT_FOUND | 404 | The account id is not owned by this API key. | Read your ids from |
LINKEDIN_FEATURE_NOT_SUBSCRIBED | 403 | A Sales Navigator or Recruiter surface on an account without that subscription, or a profile read of a person the connected account is not entitled to see. | Use core search, or use an account that has the subscription. On a
profile read, skip results whose |
TIER_NOT_ACTIVE | 403 | The Curviate side add-on for that surface is not enabled. | Enable the tier named in |
RATE_LIMIT_TENANT | 429 | Too many requests in the window, often from paging or resolving. | Honour |
LINKEDIN_RATE_LIMITED | 429 | LinkedIn is throttling the account directly. | Back off substantially and slow the whole job, not just this call. |
An empty result set is not on this list, because it is not an error. It usually
means the query was genuinely narrow. A filter value that matched several
options is refused before the search runs, with a 422 that names the field.
One that matched nothing is reported in notices[] on the page itself, and so
is a page we cut short before it filled. So check notices, and check that
cursor is null, before concluding an empty items array means nobody
matched.
Next steps
- Error codes: the full envelope and taxonomy behind every code above.
- Rate limits: the header contract and a client algorithm for pacing a search loop.
- API quick start: your first authenticated call, if search is where you started.
- Authentication and accounts: connecting the account every search route is scoped to.