~ / guides / How to Scrape Google Jobs (Python Guide)

How to Scrape Google Jobs (Python Guide)

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • Google Jobs renders every listing with JavaScript. I fetched the results page on June 12, 2026: HTTP 200, ~91 KB of HTML, and 0 job fields in the static markup, so requests + BeautifulSoup alone parses no jobs.
  • The old ibp=htl;jobs URL now redirects no-cookie bots to consent.google.com. The current udm=8 URL returns 200 but the body is a JavaScript shell with an 'enable JavaScript' notice.
  • There is no official Google Jobs API: Google shut its Jobs API down in May 2021. Every method here is a scrape.
  • The realistic DIY route is Playwright: drive a headless browser, wait for the job cards, read fields from the rendered DOM. For bulk jobs without running browsers, a scraper API like ChocoData renders the JS and returns JSON.

Google Jobs (the “for jobs” panel that opens inside Google Search) aggregates postings from company career pages and job boards into one ranked list, which makes it the obvious target for recruiters, job-board startups, and labor-market analysts. This guide builds a real Python scraper, reports exactly what I got back from a live fetch, and is honest about where the simple approach dies. I ran the fetch on June 12, 2026 and pasted the real result below. The short version up front: a plain request returns the page but none of the jobs, so you either drive a headless browser or hand the JavaScript problem to an API. For the fundamentals, see my web scraping guide and the Python walkthrough.

You can fetch the public Google Jobs results with a script, and the legality depends on the method and what you keep. The technical reality first: the jobs panel runs on JavaScript, so a plain request gets the page shell and zero listings. The cards arrive after the page’s JavaScript fires, which means a headless browser or an API that renders JS. The legal part has two layers worth separating.

On US computer-access law, reading pages anyone can load without logging in sits on the safer side. In hiQ Labs v. LinkedIn, the Ninth Circuit held that scraping public data does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward traditional hacking. Querying a public jobs results page with a bot is not, by itself, a federal computer crime.

Contract law is the live risk. Google’s Terms of Service restrict automated access to its services, and hiQ is the cautionary tale: it won the CFAA question and still paid a $500,000 judgment for breaching LinkedIn’s user agreement. Two rules keep you on the lower-risk path: stay on public results, never script a logged-in Google account. Job postings are facts about openings, but some fields can include a named recruiter or contact, so under GDPR treat any personal data carefully and store the posting metadata rather than individuals’ details. I am a tester, not a lawyer, so get advice before any commercial scrape. My is web scraping legal pillar covers the full picture.

Is there an official Google Jobs API?

No. Google ran a Cloud Talent Solution “Jobs API” and shut it down in May 2021, and the consumer Google for Jobs panel has never had a public, documented API that returns listings as JSON. That single fact shapes this whole guide: there is no key to request and no contract-backed feed, so every method below is a scrape of the rendered results page. The structured data does exist upstream, in the schema.org JobPosting markup that employers publish on their own career pages and that Google ingests, which is why scraping a single company’s careers page is sometimes the easier path than rendering the aggregator.

What data can you extract from a Google Jobs listing?

A Google Jobs results page carries the fields a job tracker actually needs, once the JavaScript has rendered them. The field names below match what the major Google Jobs scrapers expose; knowing the structure tells you what to target and what an API should hand back as JSON.

FieldWhere it livesIn the static HTML?
Job titleRendered job cardNo (loads via JS)
Company nameRendered job cardNo
LocationRendered job cardNo
Source (“via Indeed”, “via LinkedIn”)Rendered job cardNo
Posted date (“3 days ago”)Detected extensionsNo
Schedule / employment type (Full-time)Detected extensionsNo
Salary range (when present)Detected extensionsNo
Job description (full text)Expanded panelNo
Apply links (per source)Apply options panelNo
Sharing link / job idExpanded panelNo

The pattern to internalize: every commercially useful field (title, company, salary, description, apply links) is painted into the DOM by JavaScript after load. The query and location you control through the URL, but the listing fields people want are not in the initial response.

How does Google Jobs block scrapers?

Google Jobs stacks four defenses, and the first one stops a naive requests scraper before it sees a single listing.

I tried requests + BeautifulSoup first (here is what came back)

I ran a plain fetch against the live page before reaching for anything heavier, because if the listings were in the HTML this would be a short script. They are not. I tested both URL styles: the current udm=8 results format and the legacy ibp=htl;jobs widget link. Here is exactly what I ran and the real result.

import requests

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      "(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")

urls = {
    "udm=8 (current)": "https://www.google.com/search?q=python+developer+jobs&udm=8&hl=en&gl=us",
    "ibp=htl;jobs (legacy)": "https://www.google.com/search?q=python+developer+jobs&ibp=htl;jobs&hl=en&gl=us",
}

for name, url in urls.items():
    r = requests.get(url, headers={"User-Agent": UA}, timeout=20)
    redirected = "consent.google.com" in r.url
    fields = sum(r.text.count(s) for s in
                 ("Full-time", "Part-time", "via ", "JobPosting", "datePosted", "salary"))
    print(f"[{name}] status={r.status_code} bytes={len(r.text)}")
    print(f"   consent redirect: {redirected} | job-field markers: {fields}")

Real output from my run on June 12, 2026:

[udm=8 (current)] status=200 bytes=91271
   consent redirect: False | job-field markers: 0
[ibp=htl;jobs (legacy)] status=200 bytes=634515
   consent redirect: True | job-field markers: 0

The udm=8 URL returns HTTP 200 and about 91 KB, with zero job-field markers: no Full-time, no via, no JobPosting schema, no salary strings. Stripping the tags, the visible text is a redirect notice (“Please click here if you are not redirected”) and the page carries an “enable JavaScript” message, which is the tell of a client-rendered shell. The legacy ibp=htl;jobs URL is worse: 634 KB that is entirely the consent.google.com interstitial. Either way, BeautifulSoup would parse the response cleanly and still hand you no jobs, because the listings are not there yet. I omitted the “tested” stamp on this article for that reason: I fetched the page successfully, but I could not parse real listings from a static response, and I will not paste job data I did not extract.

Method 1: scrape Google Jobs with Playwright (DIY)

Since the listings need JavaScript, the working DIY route is a headless browser. Playwright launches Chromium, runs the page’s JS, waits for the job cards, and reads fields from the rendered DOM. I am giving you the real pattern, not a tested job dump: the job-card class names on Google are obfuscated and change, so the selectors below are the part you will re-confirm in your browser’s devtools before this runs clean.

# pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright

URL = "https://www.google.com/search?q=python+developer+jobs&udm=8&hl=en&gl=us"

def scrape_jobs(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page(
            user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/126.0.0.0 Safari/537.36")
        )
        # Clear the consent gate, then load results.
        page.goto("https://consent.google.com/", wait_until="domcontentloaded")
        page.goto(url, wait_until="networkidle", timeout=60_000)

        # Each listing is a card with a role of listitem; confirm in devtools,
        # Google obfuscates and rotates the real class names.
        page.wait_for_selector("[role='listitem']", timeout=30_000)
        cards = page.query_selector_all("[role='listitem']")

        results = []
        for card in cards:
            lines = [ln for ln in card.inner_text().split("\n") if ln.strip()]
            if lines:                       # first lines are usually title, company, location
                results.append(lines[:4])
        browser.close()
        return results

for job in scrape_jobs(URL)[:5]:
    print(job)

What to expect and where it breaks:

Playwright is the honest DIY answer for Google Jobs. For a few queries checked occasionally, it works. The cost shows up at scale, in proxies, CAPTCHA solving, and selector maintenance. For the broader headless-browser playbook, see my notes on scraping without getting blocked, and the Scrapy guide if you want to wrap this in a crawler.

Method 2: scrape Google Jobs with a scraper API

A scraper API turns JS rendering, consent cookies, IP rotation, and CAPTCHA solving into one HTTP call that returns JSON. You skip the browser farm and the selector maintenance. ChocoData (chocodata.com) is one example I checked against its own docs. It exposes a universal endpoint (“one endpoint, any site”) plus dedicated per-site endpoints, and its documented Google pattern is GET /api/v1/google/{resource}:

import requests

API_KEY = "YOUR_KEY"

resp = requests.get(
    "https://chocodata.com/api/v1/google/jobs",
    params={
        "q": "python developer",
        "location": "New York",
        "api_key": API_KEY,
    },
    timeout=120,
)
data = resp.json()

for job in data.get("jobs", []):
    print(job["title"], "-", job["company_name"], "-", job["location"])

The endpoint pattern is GET /api/v1/{site}/{resource}?api_key=..., so swapping the path targets other sites (/api/v1/google/search for the SERP, for example). Per ChocoData’s site, the service handles residential proxy rotation, CAPTCHAs, retries, and JavaScript rendering, and returns structured JSON. I have not independently benchmarked their success rate or confirmed the exact jobs response schema, so treat the field names above as illustrative and verify against their docs on the free tier before you wire anything to them.

The reason this category exists for Google Jobs specifically: the two hard problems (JS rendering and the consent-plus-IP gauntlet) are exactly what an API absorbs. You send a query and a location, you get listings back, and the proxy pool and headless fleet are someone else’s operational problem.

DIY vs scraper API: which should you use?

The choice comes down to volume and how much browser-and-proxy plumbing you want to own.

FactorDIY (Playwright)Scraper API
Handles JS renderingYes (you run the browser)Yes (managed)
Consent wall / IP rotation / CAPTCHAYou build itIncluded
OutputRaw DOM, you parseStructured JSON
Cost modelYour CPU + proxy billPer request / plan
Breaks when Google changes classesYes, you fix itVendor’s problem
Good forA few queries, occasional checksBulk queries, scheduled tracking

Pick Playwright when you are pulling a handful of queries, you are comfortable maintaining selectors, and you do not want a vendor in the loop. Pick a scraper API when you need many queries on a schedule and the proxy-plus-CAPTCHA upkeep is not how you want to spend your week.

The deciding fact from my run: a plain request to the Google Jobs page returns HTTP 200, about 91 KB of HTML, and 0 job fields, because every listing is painted in by JavaScript afterward, and the legacy URL just bounces to a consent page. Closing the gap to reliable listing data means either running and babysitting headless browsers behind rotating proxies, or letting an API own that stack and hand you JSON. If you want to learn the parsing skills regardless, my BeautifulSoup guide covers extracting fields once you have rendered HTML in hand, and the Python walkthrough covers the request layer.

FAQ

Does the Google for Jobs widget still use the ibp=htl;jobs URL?

Not reliably. Google migrated the jobs experience to the udm=8 results format, and the legacy ibp=htl;jobs link now bounces no-cookie clients to a consent page in my June 2026 test. Both URLs ultimately render listings client-side, so neither returns job data in the raw HTML a plain GET receives.

Can I get Google Jobs data from the JobPosting schema on employer sites instead?

Often yes, and it is cleaner. Google populates its jobs index from schema.org JobPosting markup on company career pages and job boards. If you only need a specific company's roles, scraping that JobPosting JSON-LD straight from their careers page is simpler than rendering Google's aggregator. For broad cross-employer coverage in one query, the aggregator or an API saves the per-site work.

Why does my requests call to Google Jobs return no listings?

The listings are painted into the DOM by JavaScript after the initial HTML loads. A plain GET to the udm=8 URL returns a 200 with a page shell and an 'enable JavaScript' redirect notice, not job cards. BeautifulSoup parses that shell and finds zero titles, companies, or salaries.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.