How to scrape Google Jobs listings with Python
Company, location and apply links from Google Jobs as JSON, and the one response shape that breaks naive parsing code on a plain HTTP 200. Tested August 2026.

You want job listings for a role and a place, company name, location, a link to apply, at a scale beyond clicking through Google’s job cards one at a time: a niche job board, a market-intelligence feed, or a script that tracks who’s hiring for a title over the following weeks.
The request
A call to /v1/jobs takes a search query the same way you’d type it into Google, plus optional country, language and domain if you need results scoped to a specific market.
The response shape that will break your code if you skip it
Google doesn’t return a jobs module for every query, and when it doesn’t, /v1/jobs still answers with an HTTP 200. The body is {"jobs_results": ["There are no jobs for this query"]}, a list holding a single string instead of job objects. We reproduced this on August 27, 2026: the query “product manager” returned that message on every attempt, while “product manager jobs” returned ten real listings for what is functionally the same search. A loop that assumes jobs_results is a list of dicts, for job in results: print(job["title"]), throws a TypeError the moment it reaches the string, because you can’t index a string with another string. The fix is to check that each entry is a dict with a title before treating it as a job, not just that the list is non-empty:
import requests
API_KEY = "faf_live_..." # your FastAF key
resp = requests.get(
"https://api.fastaf.co/v1/jobs",
params={"query": "product manager jobs"},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
resp.raise_for_status()
raw_results = resp.json()["jobs_results"]
# Google doesn't always have a jobs module for a query. When it doesn't,
# jobs_results is ["There are no jobs for this query"], a string, not a job.
listings = [r for r in raw_results if isinstance(r, dict) and "title" in r]
if not listings:
print("No jobs module for this query.")
for job in listings:
apply_options = job.get("apply_options", [])
apply_via = apply_options[0]["title"] if apply_options else None
print(f'{job["title"]} @ {job["company_name"]} ({job["location"]})')
print(f' apply via {apply_via}, share link: {job["share_link"]}')
We ran this against a real “data engineer” search while writing the post (August 2026): all ten entries in jobs_results were proper job objects, so listings came out the same length as the raw list. The guard costs nothing when the module is present; it just stops the crash when it isn’t.
Apply links live in apply_options, not on the job itself
Every listing in our sample carried an apply_options list, pairs of {title, link} such as {"title": "LinkedIn", "link": "/goto?..."}, one entry per site that lists the posting: the company’s own careers page, then whichever of Indeed, LinkedIn, ZipRecruiter, Glassdoor and similar boards also picked it up. The link in each one is a relative path, not a ready-to-open URL, so store it alongside its title rather than assuming it works as a standalone href. Every listing also carried a separate share_link, a full absolute URL back to that same posting on Google’s own job search page. That’s the one field in the payload that’s a complete URL on its own, which makes it the natural choice for a “view online” link or a stable key for deduplicating a job across two search runs.
What else is on a listing
Beyond title, company and location, each entry carries via (the site Google attributes the listing to), extensions (short tags like “Full-time” or “20 days ago”), a full description, and job_highlights, a list of grouped bullets under headings such as “Qualifications”, “Responsibilities” and “Benefits” when Google’s parser found them in the posting. Not every listing has every group: in our sample, half the postings skipped “Benefits” entirely and one had only “Responsibilities”, so read job_highlights as a list to search by heading, not a fixed set of named fields.
What it costs
/v1/jobs is 5 credits per call, whether it returns ten listings or the no-results string. The free tier’s 500 credits cover 100 calls. On the Scale pack ($99 for 300,000 credits) that’s 60,000 calls, $1.65 per 1,000.
The short version
Build the isinstance and title check into your parsing from the start, because the no-jobs response is a normal, well-formed 200, not an error your try/except will ever see. Past that, apply_options gets you to the posting and share_link gets you a stable URL for the listing itself. Get 500 free credits, no card and run your own title-and-location combinations through it.