LinkedIn Jobs API
Query LinkedIn job postings through one API — and why LinkedIn is a search, not a board, and what that changes about the data you get.
LinkedIn is not an ATS. Every other source on this site is an employer's own job board — the Greenhouse or Ashby instance a company runs for itself. LinkedIn is a job board: a posting here was aggregated by LinkedIn, and the company didn't publish it to LinkedIn's API the way it publishes to its own careers page. That difference isn't pedantry — it changes how the data is collected, how fresh it is, and whether you can trust the apply link.
It's the second-largest source in this dataset at 87,462 postings. Read the live
figure from /v1/meta.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=linkedin" \
--data-urlencode "time_frame=24h" \
--data-urlencode "limit=2"{
"data": [
{
"id": "linkedin:4127883941",
"title": "Senior Product Manager",
"company": {
"slug": "orbital-payments",
"name": "Orbital Payments",
"domain": "orbitalpayments.com",
"website": "https://orbitalpayments.com",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Financial Services",
"employee_count": 900
},
"location": {
"remote": true,
"countries": ["United States"],
"cities": ["San Francisco"],
"regions": null,
"raw": ["San Francisco, CA"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "Remote",
"seniority": "Mid-Senior level",
"salary": {
"min": 150000,
"max": 350000,
"currency": "USD",
"unit": "YEAR",
"annual_min": 150000,
"annual_max": 350000,
"summary": "Base pay range: $150,000.00/yr - $350,000.00/yr"
},
"skills": ["product strategy", "payments"],
"taxonomies": ["Product"],
"apply_url": "https://www.linkedin.com/jobs/view/4127883941",
"url": "https://www.linkedin.com/jobs/view/4127883941",
"source": "linkedin",
"posted_at": "2026-07-14T08:30:00.000Z",
"expires_at": null,
"updated_at": "2026-07-14T09:05:22.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}How LinkedIn actually works#
There's no board to fetch. LinkedIn's public guest endpoints are a search:
# Search — returns HTML cards, job ids in data-entity-urn
curl "https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search?keywords=&geoId=103644278&f_TPR=r86400&start=0"
# Detail — HTML, the full description is in .show-more-less-html__markup
curl "https://www.linkedin.com/jobs-guest/jobs/api/jobPosting/<id>"Both are server-rendered HTML, unauthenticated, and need no browser. The job id
lives in data-entity-urn="urn:li:jobPosting:<id>". The search cards carry free
per-card metadata — title, company, location, posted date — so a metadata-only
pass costs nothing beyond the search itself.
The two hard limits#
Everything about collecting LinkedIn follows from these:
| Limit | Value |
|---|---|
| Results per search | ~1,000 — start caps around 975 |
| Requests per IP before a 429 | ~20–25 rapid ones |
The first one is the interesting one. You cannot enumerate LinkedIn. Any query returning more than about a thousand jobs is truncated, and nothing tells you it was truncated — you just get a thousand.
So coverage means slicing the query space until each slice fits under the cap,
then unioning the slices with global id dedup. The facets are the knives:
geoId, f_TPR (time posted), f_E (experience), f_JT (job type), f_WT
(workplace type), f_C (company).
f_TPR is the one that makes it tractable. Run f_TPR=r86400 — posted in the
last 24 hours — and each day's new jobs per keyword × geo is comfortably under
1,000. You get complete fresh coverage, cheaply, indefinitely. That's why
LinkedIn here is refreshed as an hourly delta of the last hour's postings
rather than by diffing a board: there is no board to diff.
What the detail page gives you#
The guest fragment carries a structured "job criteria" list — seniority level, employment type, job function, industries — so you get employment type without inference. LinkedIn's own compensation block is on roughly 15% of postings, mostly US pay-transparency states:
<section class="compensation">
<h3 class="compensation__heading">Base pay range</h3>
<div class="salary compensation__salary">$150,000.00/yr - $350,000.00/yr</div>That's ground truth — cleaner than mining the prose, and present on some postings whose description omits pay entirely.
What bites you#
`apply_url` is not the employer's form on this source
Everywhere else here, apply_url is the employer's real application form,
because the posting came from the employer's own board. A linkedin posting's
apply_url points wherever LinkedIn points.
Some LinkedIn postings are "Easy Apply" — you apply on LinkedIn without leaving.
Others redirect offsite. When a posting matters enough to verify, check source
before assuming the URL is the real ATS. Sources has
the full attribution story.
The detail page gives a relative date, not a timestamp. 3 days ago,
2 weeks ago. There's a datetime attribute on the search card, which is why the
card metadata is worth keeping — the detail alone forces you to reconstruct an
approximate date from prose, and "3 days ago" has a day of slack in it either way.
The default logo is a placeholder. Logos on static.licdn.com are LinkedIn's
generic grey building, not the company's mark. Store it and every logo-less
company gets a fake logo instead of falling back to its domain favicon. Real logos
are on media.licdn.com.
Not Applicable is a real seniority value. LinkedIn emits it as a placeholder.
It means nothing and shouldn't be stored as a level.
A thousand results looks exactly like all the results. Worth repeating, because it's the failure that produces a confident, wrong dataset. If your slice returns ~980+ ids, there are more jobs than the query can return, and the slice needs splitting.
Or use ours#
LinkedIn is the source where "just scrape it" is furthest from the truth. There's no endpoint that returns a company's jobs. There's a search with a hard cap, a per-IP throttle that trips in about twenty requests, and a query space you have to slice by geography, recency, seniority and job type to get underneath the cap — then union with global dedup and re-slice anything that comes back at the ceiling.
You get that already run: an hourly delta, structured pay where LinkedIn published it, employment type and seniority from the criteria block, placeholder logos and placeholder seniorities dropped, and the company join.
And you get it deduplicated against the employer's own board. This is the one
that matters most on this source: a role on both a company's Greenhouse board and
LinkedIn is collapsed to one row. Which means
?source=linkedin&limit=1 doesn't count "jobs on LinkedIn" — it counts jobs where
LinkedIn won dedup. Any per-source breakdown you publish is a breakdown of
surviving rows, and it's worth saying so out loud.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key