LinkedIn scraper API

pineapi is a LinkedIn scraper API that takes a set of filters (job title, location, industry, company headcount, seniority, school, years of experience and about thirty more) and returns every matching profile as 53 structured fields plus 16 nested collections. $0.012 per profile delivered, $0.12 minimum per search. No LinkedIn account, no session cookie and no proxies of your own: authentication is a bearer token, and a search that fails is not billed.

Quickstart · Generated reference · Use with AI

Start a search, then read the results

pineapi runs a search as a job because a real search takes minutes, not milliseconds: poll every 5 seconds for the first minute, then every 20 seconds. You start one, then read it when it is done.

curl
# 1. Create a run. Keep this key and body unchanged for retries.
IDEMPOTENCY_KEY=$(uuidgen)
curl --fail-with-body $PINEAPI_BASE_URL/v1/profile-searches \
  -H "Authorization: Bearer $PINEAPI_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"current_job_titles":["Head of Talent"],"locations":["Spain"],"max_items":100}'
# HTTP 202: { "run_id": "<uuid>", "status": "queued", ... }

# 2. Copy run_id from that response. Poll until succeeded or partial.
RUN_ID='<uuid>'
curl --fail-with-body "$PINEAPI_BASE_URL/v1/runs/$RUN_ID" \
  -H "Authorization: Bearer $PINEAPI_KEY"
# queued / starting / running / processing are in progress.
# failed / cancelled are terminal: inspect failure before retrying.

# 3. Download profiles after success. Follow next_cursor while has_more is true.
curl --fail-with-body "$PINEAPI_BASE_URL/v1/runs/$RUN_ID/profiles?limit=200" \
  -H "Authorization: Bearer $PINEAPI_KEY"
# { "data": [...], "has_more": false, "next_cursor": null }
node
import { randomUUID } from 'node:crypto';

const base = process.env.PINEAPI_BASE_URL + '/v1';
const auth = { Authorization: `Bearer ${process.env.PINEAPI_KEY}` };
async function json(path, options = {}) {
  const response = await fetch(base + path, { ...options,
    headers: { ...auth, ...options.headers }, signal: AbortSignal.timeout(30_000) });
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  return response.json();
}
// Persist this key and body if your application retries the creation request.
const key = randomUUID();
const { run_id } = await json('/profile-searches', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
  body: JSON.stringify({ current_job_titles: ['Head of Talent'], locations: ['Spain'], max_items: 100 }),
});
const pending = new Set(['queued', 'starting', 'running', 'processing']);
const started = Date.now();
const deadline = Date.now() + 30 * 60_000;
let run;
do {
  if (Date.now() > deadline) throw new Error(`Polling timeout; resume with run_id ${run_id}`);
  await new Promise(resolve => setTimeout(resolve, Date.now() - started < 60_000 ? 5_000 : 20_000));
  run = await json(`/runs/${run_id}`);
} while (pending.has(run.status));
if (!['succeeded', 'partial'].includes(run.status)) throw new Error(JSON.stringify(run.failure ?? run.status));

let cursor;
do {
  const query = new URLSearchParams({ limit: '200' });
  if (cursor) query.set('cursor', cursor);
  const page = await json(`/runs/${run_id}/profiles?${query}`);
  for (const profile of page.data) console.log(profile.full_name);
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
python
import os, time, uuid, requests

base = os.environ["PINEAPI_BASE_URL"].rstrip("/") + "/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PINEAPI_KEY']}"

def get(path, **kwargs):
    response = session.get(base + path, timeout=30, **kwargs)
    response.raise_for_status()
    return response.json()

# Keep this key and the same body if retrying the creation request.
key = str(uuid.uuid4())
response = session.post(base + "/profile-searches", timeout=30,
    headers={"Idempotency-Key": key},
    json={"current_job_titles": ["Head of Talent"], "locations": ["Spain"], "max_items": 100})
response.raise_for_status()
run_id = response.json()["run_id"]
started = time.monotonic()
deadline = started + 1800
while True:
    if time.monotonic() > deadline:
        raise TimeoutError(f"Resume polling run_id {run_id}")
    run = get(f"/runs/{run_id}")
    if run["status"] not in {"queued", "starting", "running", "processing"}:
        break
    time.sleep(5 if time.monotonic() - started < 60 else 20)
if run["status"] not in {"succeeded", "partial"}:
    raise RuntimeError(run.get("failure") or run["status"])

params = {"limit": 200}
while True:
    page = get(f"/runs/{run_id}/profiles", params=params)
    for profile in page["data"]:
        print(profile["full_name"])
    if not page["has_more"]:
        break
    params["cursor"] = page["next_cursor"]

Authentication is a bearer token and nothing else. pineapi runs no OAuth handshake, because an agent that has to complete a browser redirect before its first call is an agent that gives up and picks something else. Creation requires an Idempotency-Key header of up to 255 characters. Reuse the key and unchanged request on retries. Optional webhook_url must be a public HTTPS URL: it receives a signed event containing data.run metadata, not profiles. Customer access to the webhook signing secret is not yet implemented; use authenticated polling as the source of truth. Fetch profiles separately and follow next_cursor while has_more is true. Respect Retry-After on HTTP 429; the examples stop on HTTP errors so your application can apply its retry policy.

The whole reference as one file: auth, request, full response, error codes and price, with nothing to cross-reference.

Two ways to ask for profiles

pineapi implements two kinds of input and returns the identical 53-field response for both.

Search by filters

Describe who you are looking for: job titles, locations, industries, schools, company headcount, seniority, years of experience and around thirty more. Every matching profile comes back.

Lookup by profile URL

POST /v1/profile-lookups accepts linkedin_urls with 1 to 2,000 profile URLs. URLs are normalized and deduplicated within the request. Same response schema and per-profile price; no search minimum or block-of-25 requirement.

The computed fields for tenure, promotions and truncation are returned on every profile. They are not filterable in the request in v1.

How many profiles to ask for

pineapi accepts max_items between 25 and 2,000, in multiples of 25, defaulting to 100. That is not an arbitrary rule: LinkedIn paginates search results in blocks of 25, and a partial block costs exactly what a full one costs.

Accepted values for max_items
ParameterRule
max_itemsMinimum 25, maximum 2,000, in multiples of 25. Defaults to 100.
start_pageOptional pointer into the LinkedIn results. See pagination below before you use it.
webhook_urlOptional public HTTPS URL. Receives a signed event with run metadata; fetch profiles separately.
Idempotency-Key headerRequired for creation. Up to 255 characters; reuse with the unchanged body on retries.
DepthSingle tier. There is no cheap mode and no deep mode to pick between.

A month of 50,000 profiles is at least 25 searches, since one search caps at 2,000. There is no cheap mode and no deep mode to choose between: there is one tier, and it returns everything.

Why the official LinkedIn API will not give you profile data

LinkedIn's official API does not return other people's profiles. The Sign In with LinkedIn and Share on LinkedIn products return the authenticated member's own name, headline and picture, and the Marketing and Talent APIs sit behind partner programmes with commercial review, headcount requirements and an approved use case.

That mismatch is why most searches for "LinkedIn API" are looking for an unofficial one: the official documentation answers a question nobody asked. pineapi exists for the other question: describe who you are looking for, and get those people as structured data.

pineapi reads what LinkedIn shows publicly. It is not an official LinkedIn product, it is not affiliated with LinkedIn, and customers must establish a lawful basis for their collection and use of personal data.

The response schema, field by field

pineapi returns 53 top-level fields on every profile, plus 16 nested collections. Every field is listed below with its type and what it contains.

Profile fields

Generated from the API OpenAPI schema. Nullable values can be absent from the source profile.

idstring (uuid)

Id

ordinalinteger

Ordinal

public_identifierstring | null

Public Identifier

linkedin_urlstring

Linkedin Url

requested_urlstring | null

Requested Url

first_namestring | null

First Name

last_namestring | null

Last Name

full_namestring | null

Full Name

headlinestring | null

Headline

aboutstring | null

About

location_textstring | null

Location Text

location_citystring | null

Location City

location_statestring | null

Location State

location_region_codestring | null

Location Region Code

location_countrystring | null

Location Country

location_country_fullstring | null

Location Country Full

location_country_codestring | null

Location Country Code

open_to_workboolean | null

Open To Work

hiringboolean | null

Hiring

premiumboolean | null

Premium

influencerboolean | null

Influencer

creatorboolean | null

Creator

verifiedboolean | null

Verified

open_profileboolean | null

Open Profile

memorializedboolean | null

Memorialized

connections_countinteger | null

Connections Count

follower_countinteger | null

Follower Count

registered_atstring (date-time) | null

Registered At

profile_languagestring | null

Profile Language

profile_countrystring | null

Profile Country

profile_picture_urlstring | null

Profile Picture Url

photo_urlstring | null

Photo Url

cover_picture_urlstring | null

Cover Picture Url

current_titlestring | null

Current Title

current_company_namestring | null

Current Company Name

current_company_linkedin_urlstring | null

Current Company Linkedin Url

current_position_started_onstring (date) | null

Current Position Started On

total_months_experienceinteger | null

Total Months Experience

months_at_current_companyinteger | null

Months At Current Company

months_in_current_roleinteger | null

Months In Current Role

promoted_at_current_companyboolean

Promoted At Current Company

promotions_countinteger

Promotions Count

companies_countinteger

Companies Count

avg_months_per_companyinteger | null

Avg Months Per Company

linkedin_member_sincestring (date) | null

Linkedin Member Since

skills_flatstring[]

Skills Flat

languages_flatstring[]

Languages Flat

school_namesstring[]

School Names

company_namesstring[]

Company Names

section_totalsobject | null

Section Totals

experience_truncatedboolean

Experience Truncated

skills_truncatedboolean

Skills Truncated

scraped_atstring (date-time)

Scraped At

16 nested collections

education[]array of objects

Education

certifications[]array of objects

Certifications

languages[]array of objects

Languages

skills[]array of objects

Skills

projects[]array of objects

Projects

publications[]array of objects

Publications

courses[]array of objects

Courses

honors[]array of objects

Honors

volunteering[]array of objects

Volunteering

recommendations[]array of objects

Recommendations

interests[]array of objects

Interests

patents[]array of objects

Patents

organizations[]array of objects

Organizations

websites[]array of objects

Websites

top_skills[]array of objects

Top Skills

experiences[]array of objects

Experiences

ordinal · position · company_name · company_linkedin_url · employment_type · workplace_type · location · description · started_on · ended_on · is_current · duration_months · duration_text · skills

Field names are a contract. pineapi versions the schema under /v1/ and does not rename or remove a field inside a version, because pipelines and agents cache the shape of a response and a silent rename breaks them quietly. Profile responses do not include email addresses.

When a profile is incomplete, the profile says so

40% of LinkedIn profiles come back with truncated work history and 68% with truncated skills. LinkedIn reports a section's true item count separately from the items it actually serves, and the two frequently disagree.

The consequence is arithmetic. Any years-of-experience figure computed from a scraped profile is systematically under-counted on four profiles in ten, so downstream calculations need to check the truncation flags.

pineapi returns experience_truncated and skills_truncated as booleans, and section_totals with the count LinkedIn claimed, so you can tell a genuinely short career from a profile that arrived clipped.

Truncated work history40%
Truncated skills68%

Filters that LinkedIn silently answers with nothing

LinkedIn's search stops returning results past a boolean complexity limit. Combine groups with AND or NOT and go beyond 9 OR alternatives and the search returns an empty set with no error, no warning and no indication that the query was the problem.

pineapi validates the query before running it and rejects it with HTTP 422, invalid_parameter and an explanation, rather than running a search that was never going to match anything and billing you the $0.12 minimum for it.

Asynchronous runs and terminal states

Searches take minutes and lookups usually take seconds. These observations are not an SLA. Poll until succeeded, partial, failed or cancelled; HTTP 200 on the status endpoint does not mean the run succeeded.

Latency is driven by result size more than anything else, so plan around the size you request rather than an average.

Pagination, and why one big search beats five small ones

start_page is a pointer into a LinkedIn search results page, not a stable cursor. LinkedIn re-ranks and re-shuffles results over time, so the same search run twice does not return the same profiles in the same order.

pineapi measured about 30% overlap between consecutive paginated calls on the same search. Since billing counts profiles delivered and duplicates are billed, paging through a large result set costs more and returns less of it.

Request a large result in a single search with a high max_items instead. One search for 500 profiles is cheaper and more complete than five searches for 100.

Errors, retries and what is billed

pineapi bills $0.012 per profile delivered, with a $0.12 minimum on any search that runs. Every error carries a machine-readable code, a sentence saying what went wrong and the fix, so an agent can correct itself without a human reading the log.

Error codes, what causes them, the fix, and whether the call is billed
CodeHTTPWhat happenedWhat to doBilled
idempotency_key_required400The creation request has no Idempotency-Key header.Send a unique key (a UUID works); reuse the same key and body when retrying.No new charge
invalid_api_key401The API key is missing, invalid or revoked.Send a valid bearer token.No new charge
insufficient_scope403The key cannot perform this operation.Use runs:write for creation/cancellation and runs:read for reads.No new charge
invalid_parameter422A field or filter is invalid, including max_items or an overly complex search_query.Read error.param and error.message; correct the request before retrying.No new charge
insufficient_balance402Available credit does not cover the run reservation.Top up the account before creating the run.No new charge
run_not_found404The run does not exist, belongs to another account, or has been deleted.Check the run_id and retention window.No new charge
idempotency_key_reused409The same key was reused for a different request.Use a new key for new work; keep the original key for an unchanged retry.No new charge
run_not_cancellable409The run has already finished.Only cancel a run that has not finished.No new charge
rate_limited429The account token bucket is exhausted.Wait for the Retry-After header duration before retrying.No new charge
too_many_queued_runs429The account queue is full.Wait for queued work to start; respect Retry-After.No new charge
request_too_large413The request body exceeds 1,000,000 bytes.Reduce the request body.No new charge
service_unavailable503The service could not complete this HTTP request in time.Retry shortly; preserve Idempotency-Key on creation requests.No new charge

Transient failures are retried inside the search, so HTTP success when reading a run does not mean the run succeeded. Inspect status and failure: failed and cancelled are terminal and not billed. A partial run returns the profiles that were delivered. Retry a failed run as new work only after inspecting the reason.

How long results stay available

pineapi keeps search results available through the API and as CSV for 30 days. Re-downloading inside that window is free and is not billed again.

After 30 days the results are deleted. Store anything you need to keep.

No LinkedIn account and no cookies

pineapi never asks for your LinkedIn credentials, your session cookie or a browser extension, and it does not act as your account. Tools that do ask for a cookie put your own profile at risk of a restriction, and they stop working the moment you log out on your phone.

Because pineapi needs nothing from your account, a key can be issued programmatically and handed to an agent or a CI job with no human in the loop. Create a key from a signed-in dashboard session; paid runs need prepaid balance.

Also: the assistant integration brief, and the price per profile.

Get an API key

Tell pineapi roughly what volume you are planning and what you are building. We can help you choose search or lookup and explain the onboarding steps. There is no sandbox or free test key.

pineapi bills $0.012 per profile delivered with a $0.12 minimum per search, so the first thing worth testing is a search you would actually run. All 53 fields come back on every profile.

A human replies within one business day. What we do with your details.

LinkedIn scraper API questions

What is the best LinkedIn scraper API?

The right one returns the fields you need at a price you can predict without asking for your LinkedIn account. pineapi returns 53 fields plus 16 nested collections for $0.012 per profile delivered, needs no cookie, has no subscription, so a 500-profile month costs what 500 profiles cost.

How do I scrape LinkedIn profiles programmatically?

Describe who you want with filters, start a search with one POST, then read the result when it is done or receive a webhook with the completed run metadata. With pineapi, create a run with a bearer token and Idempotency-Key, poll its status, then download its profiles, with no headless browser to run, no proxy pool to rent and no login to keep alive. The complete request is on this page.

Is there a LinkedIn scraper that does not need cookies or an account?

Yes. pineapi requires no LinkedIn account, no session cookie and no browser extension, Authentication uses a bearer token issued to your account. You remain responsible for lawful use of the returned personal data.

What fields can I get from a LinkedIn profile?

53 top-level fields plus 16 nested collections: identity, headline and about, parsed location down to country code, status badges, follower and connection counts, current role, computed tenure and promotion counts, flattened skills and schools, truncation flags, and arrays for experience, education, skills, certifications, languages, projects, publications, patents and more. Email addresses are not returned.

How long does a search take?

Searches take minutes; lookups usually take seconds. Poll every 5 seconds for the first minute, then every 20 seconds. Resume with the same run_id if your client stops waiting.

Does the official LinkedIn API return other people’s profiles?

No. The official API returns the authenticated member’s own basic profile, and the richer Talent and Marketing APIs sit behind partner programmes with commercial review. There is no official endpoint that takes a set of filters and returns the people who match, which is why unofficial APIs exist.

What am I billed when a search returns nothing?

The $0.12 search minimum, because the search ran and completed. An empty result means no profile matched your filters, not that anything failed. A search that actually fails is not billed at all, and neither is a query pineapi rejects as too complex before running it.

Can I filter on the computed fields like promotions or tenure?

Not in v1. total_months_experience, months_in_current_role, promotions_count and the truncation flags are computed on every profile pineapi returns, so you can filter on them yourself after the fact, but they are not accepted as search filters in the request.

How long do you keep search results?

30 days, through the API and as CSV, with free re-downloads inside that window. After 30 days pineapi deletes them. Anything you need for longer belongs in your own storage.

Will the response schema change?

Not inside a version. pineapi versions the schema under /v1/ and does not rename or remove a field within it. New fields can appear; existing ones keep their names and types, because agents and pipelines cache the shape of a response.

Last updated 2026-10-19