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

COMPANY · LEGAL

Privacy Policy

Redmer Holding GmbHLast updated September 17, 2026

Who we are

Curviate is operated by Redmer Holding GmbH ("Curviate", "we", "us"), a German GmbH registered at Amtsgericht Bonn, HRB 29957, registered address Hostertstraße 16, 53332 Bornheim, Germany. Full company details are on our Imprint. We haven't appointed a statutory Data Protection Officer, since our processing doesn't reach the scale or sensitivity that requires one. Privacy questions go to privacy@curviate.com.

The two roles we play

When you create an account and use Curviate, we process your own data (identity, billing, API keys, connector authorizations). For that data, we are the controller.

When you use Curviate to act on your own connected LinkedIn account, viewing profiles, sending messages, managing engagement, that content and those contacts belong to that account and its people. You are the controller of that data; we are the processor, acting only on your instructions, under a Data Processing Agreement available on request (see below). If one of your contacts has a question about being reached through Curviate, you're who they should contact first; email privacy@curviate.com if you need help routing it.

What we collect, and why

DataWhy
Account identity (name, email, sign-in method)Create and secure your account
Your LinkedIn credentialsOperate the actions you request
LinkedIn content Curviate retrieves on your behalfFulfil your requests, and answer later requests for the same records without fetching them from LinkedIn again
API keys and connector (OAuth) authorizationsAuthenticate your API, CLI, MCP, or SDK requests
Billing detailsCharge you correctly and meet our tax obligations
Usage and security logsKeep the service reliable and abuse-free
Support messagesRespond to you
Website analytics, only if you opt inUnderstand how the site is used

We rely on our contract with you, our legitimate interest in running and securing the service, our legal obligations (tax law, for example), and, for analytics, your consent. We never sell your data or use it to train models.

Where it's processed, and who else touches it

Our infrastructure runs in the EU. Hosting: Railway. Database and auth: Supabase, Ireland. Email: Resend. Payments: Stripe. Network security: a DDoS-protection provider sits in front of our app and never sees or stores request content. LinkedIn connectivity: a third-party infrastructure provider that lets us execute LinkedIn actions on your behalf. Error tracking: Sentry, Frankfurt. Product analytics: PostHog, Frankfurt. Uptime monitoring: Better Stack.

We give the current, named list of every provider above to any customer who asks: security@curviate.com.

Data processing agreement

A data processing agreement under Article 28 of the GDPR is available to business customers on request. Email security@curviate.com and we will send you the current version.

Outside the EU

All customer LinkedIn data, account data, and telemetry are processed and stored exclusively in EU regions of our sub-processors. A few providers we rely on (Stripe and Sentry, for example) are headquartered outside the EU/EEA; where that applies, it's covered by their own GDPR safeguards, typically the EU Standard Contractual Clauses.

How long we keep it

DataRetention
Account and workspace dataWhile your account is active
Closed accountDeleted immediately and irreversibly; see Deleting your account below
LinkedIn credentialsUntil you disconnect that account
LinkedIn profiles, companies, posts, jobs, comments, and groupsWhile your account is active
Chats and messages from your connected accounts' personal LinkedIn inboxesWhile your account is active
LinkedIn activity events we receive for your accountsKept briefly, then deleted automatically
Search queries, recruiter screening answers and candidate ratings, and personal contact details from profiles, such as email addresses and phone numbersNot kept
Short-lived cacheCleared within 1 hour
API keysUntil you revoke or rotate them
Connector (OAuth) authorizationsAccess token ~1 hour; refresh token up to ~12 months, or until you revoke it, whichever comes first
Billing recordsAs required by German tax law, currently up to 10 years
LogsA short operational window; metadata only, never message content

We keep LinkedIn content only when Curviate retrieves it from LinkedIn on your behalf or receives an event for one of your connected accounts. It is kept only for your workspace, and we never use it to train models. It is deleted when you close your account, except copies queued for delivery to your webhooks, which expire on their own shortly afterwards.

The 12-month figure above is a server-side credential for a connected AI agent or app. It is not a cookie and doesn't touch your browser session; see Cookies below for that. You can see and revoke every connector from Authorized applications in your dashboard at any time.

Cookies

We keep cookies to a minimum, and ask before anything beyond the essentials runs.

Strictly necessary, no consent needed:

NamePurposeExpiry
cc_cookieRemembers your cookie choice12 months
sb-*-auth-tokenKeeps you signed inWhile active; cleared on sign-out

Analytics, only if you accept:

NamePurposeExpiry
_gaGoogle Analytics: distinguishes visitors2 years
_gidGoogle Analytics: distinguishes visitors24 hours
_ga_<container id>Google Analytics: persists session state2 years

No advertising cookies, ever. Accept and reject are equally easy, and you can change your mind any time via Cookie Preferences in the footer; we won't ask again for 12 months unless something material changes. Our LinkedIn connect flow and OAuth authorization screen never set anything beyond the essentials, so no banner appears there.

Connecting an AI agent or app

Curviate is built for AI agents and automated clients as much as for people. If you connect an app like Claude, or your own code, via an API key or an OAuth connector, it can act on your workspace within the access you gave it. What it does with anything it receives back, including what it sends to its own AI model, is between you and that provider; review its practices before connecting it. Review and revoke any connection any time from your dashboard.

Deleting your account

You can delete your account yourself, from Settings in your dashboard. It takes effect immediately and it cannot be undone. There is no grace period and nothing to restore afterwards, so export anything you want to keep before you start.

Deleting removes your sign-in identity, which frees your email address for reuse straight away, along with your profile, your workspace membership and settings, your API keys, and your seats. For any connected LinkedIn account, we instruct our infrastructure provider to delete it, and your access ends immediately. Records of the connection itself can remain in our systems; email privacy@curviate.com if you need those removed as well. The LinkedIn content we kept for your account, including chats and messages, is deleted with it. The one exception is event copies already queued for delivery to your webhooks, which expire on their own shortly afterwards.

A few things are kept on purpose. We would rather name them than claim a clean sweep:

  • Billing records, for as long as German tax law requires. They hold plan, seat count, amount, and payment references; no name, no email, no LinkedIn data.
  • A record that the deletion happened, so we can show you or a regulator that we did it.
  • A security log of which requests were made, kept for 90 days and then removed automatically. It records that a request happened, never what was in it.
  • A one-way fingerprint, if you used a free trial, that lets us recognise a repeat trial. It holds no readable identifier and cannot be read back into your name, your email, or your LinkedIn profile.

Internal workspace identifiers can also remain in operational records such as queue entries and rate-limit counters. Those carry no name, no email, and no content. If you want to know exactly what is left for your own account, ask us at privacy@curviate.com.

Your rights

You can access, correct, delete, restrict, or object to your data, port it elsewhere, and withdraw consent at any time: email privacy@curviate.com. A copy of your data in a machine-readable format is available on request. We don't make automated decisions about you that have a legal or similarly significant effect. You can also complain to a supervisory authority; ours is the Landesbeauftragte für Datenschutz und Informationsfreiheit Nordrhein-Westfalen (LDI NRW), www.ldi.nrw.de, though you're free to complain to the one in your own country instead.

Keeping it secure

Credentials are encrypted and never logged, returned, or shared. LinkedIn actions run through native, humanized flows; full detail is on our Security & Compliance page. If a breach puts your rights at risk, we'll notify the authorities and you, as GDPR requires. Curviate isn't directed at, or offered to, anyone under 16.

Changes

We'll update this page when our practices change, and reset the cookie prompt if the change is material.

Contact