DEV Community

Zakaria Blefkih
Zakaria Blefkih

Posted on

How to scrape LinkedIn jobs from a search URL (no login, Python)

Disclosure: I built this. LinkedIn Jobs Scraper is my actor on Apify, and I earn money when people run it. The parameter table below comes from measurements on LinkedIn's public job search on 1 November 2026, and the output is a real private test run from 2 November 2026.

Build a LinkedIn job search by hand and the whole search ends up in the address bar: keywords, a place (geoId), a date window (f_TPR), a company (f_C). That URL is the spec, so the simplest scraper input is the URL itself.

LinkedIn Jobs Scraper takes that URL and returns one row per job, 27 columns each. It needs no LinkedIn login, no cookies and no LinkedIn API key: it reads LinkedIn's public, logged-out job search and job pages.

Step 1: build the search and copy the URL

Open https://www.linkedin.com/jobs/search in a private (logged-out) window, search, set your filters and copy the address bar. Data analyst jobs in the United States, posted in the past 24 hours:

https://www.linkedin.com/jobs/search/?keywords=data%20analyst&geoId=103644278&f_TPR=r86400
Enter fullscreen mode Exit fullscreen mode

geoId=103644278 is LinkedIn's id for the United States, and f_TPR=r86400 means "posted in the last 86,400 seconds".

Step 2: send it to the actor

Apify's run-sync-get-dataset-items endpoint starts a run, waits for it and returns the rows as the response body. You need an Apify account and its API token; the free plan works.

import os

import requests

ENDPOINT = ("https://api.apify.com/v2/acts/"
            "flash_scraper~linkedin-jobs-scraper/run-sync-get-dataset-items")

search_url = ("https://www.linkedin.com/jobs/search/"
              "?keywords=data%20analyst&geoId=103644278&f_TPR=r86400")

resp = requests.post(
    ENDPOINT,
    headers={"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"},
    json={"searchUrls": [search_url], "maxItems": 50},
    timeout=330,
)
resp.raise_for_status()

for job in resp.json():
    print(job["postedDate"], job["companyName"], "|", job["jobTitle"], "|", job["location"])
Enter fullscreen mode Exit fullscreen mode

searchUrls takes up to 25 URLs, one search each, and maxItems applies to each URL. A job found by two URLs is delivered and billed once.

Keep maxItems at 100 or less here: Apify cuts synchronous calls at 300 seconds, and the actor reads each job page one at a time (about 1-2 s each) for the description, seniority and applicant count. For bigger pulls, start the run asynchronously. The same input works from the Apify Console, a schedule, an n8n HTTP Request node or an MCP client such as Claude.

What happens to each part of the URL

LinkedIn's public search does not honour every filter the logged-in site shows, so the actor sends what works, checks some filters itself, and names the rest. Measured on 1 November 2026:

URL parameter What the run does
keywords, geoId, location, distance Sent. geoId wins over the location text: a UK geoId gave 10 of 10 UK listings.
f_TPR (date posted) Sent as-is, any r<seconds> window.
f_C (company), f_AL (Easy Apply), f_EA (under 10 applicants) Sent. f_C=1441 with no keywords gave 10 of 10 Google listings.
f_E (experience level) LinkedIn stopped applying it, so the "Seniority level" on each job page is checked instead.
f_JT, f_WT (job type, workplace) Ignored by LinkedIn, so they become search keywords; f_WT=2 on its own also keeps only cards whose title or location says remote.
f_PP (cities) Ignored by LinkedIn, so each city id is searched as its own geoId, up to 10 per URL, sharing the URL's maxItems.
sortBy Not used: the public search answers in relevance order either way.
f_SB2 (salary) and other f_ filters Not used, and named in the run's status message. Use the actor's minSalary instead.

Page and tracking parameters (currentJobId, start, refresh) are dropped, so a run always starts at the first result. A URL that is not a search (a single posting, a /jobs/collections/ page, an Indeed link) is not run, is named with a reason, and bills nothing.

The past-hour trick still works

The well-known LinkedIn URL trick is editing f_TPR=r86400 down to f_TPR=r3600 to see jobs posted in the last hour. The public search honours it: on 1 November 2026, r3600 returned 10 of 10 cards between 53 minutes and one hour old. Paste a URL with r3600 (or any other r<seconds> window) and put it on a schedule if you want to be early on new postings.

Several cities in one run

Two ways:

  1. Tick several cities in LinkedIn's location filter. They land in the URL as f_PP, which the public search ignores, so the actor runs one search per city (up to 10 per URL).
  2. Skip the URL and send a locations list (up to 25 places) with searchQueries.

Both also get you past the other wall: LinkedIn's public search serves at most 800 results per search, so a country-wide pull works better as one search per city. A job found under two places is delivered once.

What a real run looks like

A private test run on 2 November 2026, just before this feature shipped: one URL for data analyst with two f_PP cities, f_TPR=r604800 (past week), sortBy=DD and a salary filter, maxItems: 10, plus a second URL that points at a single job posting on purpose. It took 29 seconds. The status message, with the report link left out:

Done — 5 LinkedIn job listings delivered for your search URL ('data analyst' in 2 cities). Not run: URL 2 (a job posting). Not used from your URL: f_SB2 (use minSalary). Details: RUN_SUMMARY.search_urls. 1 of 2 search(es) could not be fetched ("URL 1: 'data analyst' in 2 cities / city 102277331"). Not used: sortBy=DD (LinkedIn's public search returns the most relevant jobs first, not the newest).

The second city hit LinkedIn's rate limit (HTTP 429), and the run says so instead of quietly returning fewer rows. The five New York rows:

Title Company Posted Applicants Seniority
Principal Data Analyst Capital One 2026-11-01 25 Not Applicable
Data Analyst Evlo AI 2026-11-01 - Mid-Senior level
Sr. Data Analyst Foot Locker 2026-10-29 200 Not Applicable
Data Analyst Wilson Elser 2026-10-28 - Not Applicable
Data Analyst RED Global 2026-10-30 200 Entry level

applicants is LinkedIn's bucket, not an exact count: "Over 200 applicants" becomes 200, "Be among the first 25 applicants" becomes 25, the original wording stays in applicantsText, and "-" means LinkedIn showed none. "Not Applicable" is LinkedIn's own text when the employer picked no seniority. Every row also carries descriptionText, employmentType, sector, jobUrl and a 0-100 job_score.

Turn the URL into a daily alert

Add onlyNewJobs and a webhookUrl, then schedule the input in Apify Console (Schedules):

{
  "searchUrls": ["https://www.linkedin.com/jobs/search/?keywords=data%20analyst&geoId=103644278&f_TPR=r86400"],
  "maxItems": 100,
  "onlyNewJobs": true,
  "webhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX"
}
Enter fullscreen mode Exit fullscreen mode

The first run is the baseline. Later runs deliver and bill only jobs this search has not delivered before (the memory lives in your own Apify account for 90 days), and a run with nothing new sends nothing and bills no jobs. The memory is keyed on the URL's search filters, not its tracking parameters, so re-copying the URL from your browser keeps it. A Slack incoming webhook gets a text message; any other URL gets JSON.

Cost

Pay per delivered job: $0.002 on Apify's free plan ($2 per 1,000), less on paid plans, plus $0.00005 per run start (read from the actor's live pricing record on 2 November 2026; the Pricing tab is authoritative). Reading each job page is included, and an empty run bills no jobs. The free plan's monthly $5 credit covers about 2,500 jobs.

What it can't do

  • It never logs in, so applyUrl is always null (an external Apply button is a sign-in prompt on the public page) and the job poster's name is not collected.
  • LinkedIn rate-limits. A search that gets HTTP 429 is skipped and named in the status message and in RUN_SUMMARY, and only delivered jobs are billed. In my measurements, retries 75-90 seconds later went through.
  • applicants shows on about 55% of postings, and salary only when the job page shows a base-pay range.
  • It covers LinkedIn only.
  • LinkedIn's terms restrict automated access. The actor reads only public, logged-out pages and never applies to jobs.

Try it with a pasted URL: LinkedIn job search URL example. The actor: LinkedIn Jobs Scraper.

Top comments (0)