Site scraper

Job Scraper: Public Job Listings via API

HTTP $0.50JS rendering $2.00 per 1,000 results or your own scraper from $1.95/GB

A job scraper that pulls many job boards and company careers pages into one feed is three parts today: the open-source JobSpy library for the boards, the public Greenhouse and Lever JSON feeds for careers pages (one request per company), and your own parser for every other page, all run through ProxyField residential proxies from $1.95/GB (start with the Easy Trial: 1 GB for $2.95). ProxyField's Web Scraping API, in development and reached through Contact sales, is priced to fetch those other public job pages for $0.50 per 1,000 successful results, or $0.20 from 10,000 results in one purchase.

Web Scraping API per 1,000 successful results
Web Scraping API prices per 1,000 successful results
FetchEntryVolume, from
HTTP$0.50$0.20
JS rendering$2.00$1.00

Billed on successful results only.

Your own scraper on residential proxies
from $1.95/GB Easy Trial: 1 GB for $2.95
gate.proxyfield.io:7000
SourceUseCost
Many job boards in one callJobSpy (MIT licence): LinkedIn, Indeed, Glassdoor, ZipRecruiter and 4 more boardsNo licence fee; proxies from $1.95/GB
Company careers pagesGreenhouse and Lever JSON feeds: every open role of a company in one request2,000 companies a day = 60,000 requests a month
Every other job pageYour scraper on residential proxies today; the Web Scraping API once it ships$0.50 per 1,000 pages, $0.20 from 10,000

Only want to search jobs, not build a feed? Indeed, the Google Jobs panel or a job-alert service such as JobLeads already combine many sources for job seekers.

Short answers:

Data

Job scraping tool for many job boards and careers pages in one feed

You wantUseWhat you getCost
To search and apply for jobs yourselfA job search site, such as Indeed or the Google Jobs panel, or a job-alert service such as JobLeadsListings from many boards and careers pages, with alertsFree for job seekers; alert services set their own plans
A finished job datasetA licensed job-data vendor, such as LinkUp, which sources jobs from employer websites (350,000,000 jobs indexed since 2007, 195 countries, per its site)Normalised records under the vendor's licenceSet by the vendor
Many boards in one call, as codeJobSpy, an open-source Python library (MIT licence)One table from LinkedIn, Indeed, Glassdoor, ZipRecruiter, Bayt, Naukri, BDJobs and HelloWork searches; about 1,000 jobs per search per boardNo licence fee (open source); proxies: ProxyField residential from $1.95/GB
Your own feed from the sources you choose, careers pages includedThe Web Scraping API and your parser, or your scraper on ProxyField residential proxiesThe pages; your code parses, deduplicates and stores them$0.50 per 1,000 pages, from $0.20; proxies from $1.95/GB
Read moreShow less

One feed, costed: 2,000 careers pages a day = 60,000 pages a month = $30 at $0.50 per 1,000, or $12 at $0.20 from 10,000 results in one purchase; a whole feed of 330,000 pages a month (board searches, careers pages and new job pages) is $345 at the entry price, $162 at the volume price (see Pricing below). Vendor and library figures are from their own pages.

Several boards in one call with JobSpy, through a rotating US residential IP (pip install -U python-jobspy, Python 3.10 or later):

Python
import csv
from jobspy import scrape_jobs

PROXY = "<login>-country-us:<password>@gate.proxyfield.io:7000"  # JobSpy's user:pass@host:port format

jobs = scrape_jobs(
    site_name=["indeed", "linkedin", "zip_recruiter", "glassdoor"],
    search_term="data engineer",
    location="Austin, TX",
    results_wanted=50,
    hours_old=72,
    country_indeed="USA",
    proxies=[PROXY],  # one line is enough: the gateway gives a new IP per request
)
print(f"Found {len(jobs)} jobs")
jobs.to_csv("jobs.csv", quoting=csv.QUOTE_NONNUMERIC, escapechar="\\", index=False)

What comes back: a pandas DataFrame, one row per job, with the site, title, company, location, job type, pay interval and range, job URL and description; a column stays empty when the board does not show it. JobSpy's README says most boards cap a search at about 1,000 jobs, LinkedIn answers bursts with a 429 that clears within about a minute, and Glassdoor limits traffic above about 2 requests a second, so keep results_wanted modest and pace the runs. It reads boards, not careers pages, and the same job comes back from several boards: merge them with the five tiers below. Public pages only, and follow each board's terms.

Careers pages come next, starting with the sources that cost one request per company: companies on Greenhouse and Lever publish all their open roles as one public JSON document:

FeedURLOne request returnsFields you get
Greenhousehttps://boards-api.greenhouse.io/v1/boards/<token>/jobsevery open job of the companytitle, location.name, absolute_url, updated_at (add ?content=true for the description)
Leverhttps://api.lever.co/v0/postings/<company>?mode=jsonevery published postingtext (the title), categories.location, hostedUrl, createdAt

So 2,000 companies on these two systems are 2,000 requests a day, or 60,000 a month: $30 at the entry price, $12 at the volume price. Job boards and other careers sites are fetched page by page.

How to scrape Google Jobs listings when they only show in Google's search results

Short answer: Google has no public API that returns the listings in its jobs panel (against Google's job posting documentation), so you read the employer and board pages the panel links to: 50,000 job pages a month cost $25 at the entry price, $10 at the volume price. To see how Google ranks jobs for a query, fetch the results page itself and read the panel from its HTML.

The routes, costed, with where each is weaker:

RouteWhat you getCost for a target sizeLimits
Google's own job toolsNothing to read: they publish your jobs to Google–Not a data source
The sources behind the panelEach listing's "apply on" link leads to the employer's or board's job page, usually with JobPosting markup, fetched by your scraper on residential proxies today or the Web Scraping API (in development)$0.50 per 1,000 pages, from $0.20: 50,000 job pages a month = $25 entry, $10 volume; 150,000 = $75 / $30Covers the sources you list; jobs Google takes from boards you don't read are missing
The Google results pageThe Google SERP API (in development, Contact sales) is designed to return the page as JSON with the AI Overview, People Also Ask and local pack parsed; with output: html it returns the page itself$1.20 per 1,000 pages, from $0.80: 500 job queries a day = 15,000 pages a month = $18 entry, $12 volumeThe jobs panel is not a parsed block: your code reads it from the HTML; the only route that shows how Google ranks jobs for a query
Read moreShow less

The Google SERP API does not parse the Google Jobs panel. It parses the AI Overview, People Also Ask and the local pack; the jobs panel on a results page is read from the HTML (output: html), and Google's separate Jobs view is not a page the API fetches.

Where each route is weaker:

RouteWeaker at
The sources behind the panelMisses jobs Google takes from boards you do not read; you need the list of sources first
The Google results pageThe panel is unparsed, so your code breaks when Google changes its markup; the page may show only some of the panel's listings; Google's terms restrict automated queries
Open-source scrapers that read Google JobsThey break with Google's page: JobSpy's README says its Google Jobs source is currently unavailable
Google's own job toolsThey publish your jobs to Google and return none of Google's

Google's own job tools work the other way round, for people who publish jobs, which is why none of them is a data source:

  • JobPosting structured data employers and boards add it to their job pages so Google can show those jobs. Required: title, description, datePosted, hiringOrganization, jobLocation. Recommended: baseSalary, validThrough, employmentType, directApply, jobLocationType (for remote jobs) and identifier (the employer's job ID).
  • Expiry Google expects a closed job to be marked by a validThrough date in the past, by removing the markup, or by removing the page so it returns 404 or 410. Your feed can read closed jobs the same way.
  • The Indexing API tells Google when a job page is added or removed. Its quickstart says it "can only be used to crawl pages with either JobPosting or BroadcastEvent embedded in a VideoObject".
  • Cloud Talent Solution job search over a corpus of jobs an employer or board supplies itself, not over Google's index (Cloud Talent Solution documentation).

From a results page to the job pages behind it, in code. It keeps every outbound link that looks like a job page and reads each page's JobPosting markup (the API's design, in development):

Python
import json, re, requests
from urllib.parse import urlsplit, parse_qs
from bs4 import BeautifulSoup

API, HEAD = "https://api.proxyfield.io/v1", {"Authorization": "Bearer <api_key>"}
JOB_LINK = re.compile(r"greenhouse\.io|lever\.co|myworkdayjobs\.com|/jobs?/|/careers?/")

def target(href):  # Google wraps some links as /url?q=<target>
    return parse_qs(urlsplit(href).query).get("q", [""])[0] if href.startswith("/url?") else href

serp = requests.post(f"{API}/google/search", headers=HEAD, timeout=180,
                     json={"q": "data engineer jobs austin", "gl": "us", "output": "html"}).text
links = {target(a["href"]) for a in BeautifulSoup(serp, "html.parser").find_all("a", href=True)}
links = sorted(u for u in links if u.startswith("http") and "google." not in u and JOB_LINK.search(u))

for url in links:
    page = requests.post(f"{API}/scrape", headers=HEAD, timeout=180,
                         json={"url": url, "render": False, "output": "html"}).text
    for tag in BeautifulSoup(page, "html.parser").find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except json.JSONDecodeError:
            continue
        for job in data if isinstance(data, list) else [data]:
            if isinstance(job, dict) and job.get("@type") == "JobPosting":
                print(url, "|", job.get("title"), "|", (job.get("hiringOrganization") or {}).get("name"))

Expect a line per job page with its title and company, like these (example companies):

https://boards.greenhouse.io/examplecompany/jobs/1234567 | Data Engineer | Example Inc.
https://jobs.lever.co/examplestartup/0b6c3e2a-5f1d-4c8e-9a7b-2d4f6e8a1c3b | Senior Data Engineer | Example Startup

A link that prints nothing had no JobPosting markup and needs a per-site parser. 20 job pages from one query cost $0.0012 for the results page plus $0.01 for the pages at the entry prices.

How JobPosting maps to your feed:

JobPosting propertyFeed fieldNote
titletitlenormalise before matching (below)
hiringOrganization.namecompanydrop Inc, LLC, Ltd, GmbH, Corp for matching
jobLocation.addresslocationcity and region; jobLocationType: TELECOMMUTE means remote
baseSalarysalarykeep the currency and unitText (HOUR, YEAR)
employmentTypeemployment_typeFULL_TIME, PART_TIME, CONTRACTOR …
identifierreq_idthe employer's job ID: a strong duplicate key
datePosted, validThroughdate_posted, valid_througha past validThrough means closed

For a job dataset, the sources route is the one to build on: the employer's page is the original, it carries the full description and it tells you when the job closes. Google's terms restrict automated queries: read Is Scraping Google Legal? before you collect results pages. The API is in development; its design is in the API documentation, and access is through Contact sales.

How to deduplicate job postings when the same job is listed on several job boards

Five tiers, strongest first; merge at the first match:

  1. the same canonical URL, tracking parameters removed;

  2. the same ATS job ID or requisition ID;

  3. the same hash of the cleaned description;

  4. the same company, title and city after normalising;

  5. near-matches (titles at least 90% alike, or descriptions with a 5-word-shingle Jaccard of 0.8 or more) go to a review list, never to an automatic merge.

Read moreShow less

The employer's own page is the master record, and every copy goes into sources. In detail:

The same job often sits on the employer's careers page, two boards and the Google Jobs panel. Match from the strongest signal to the weakest, merge at the first match, and run the merge on every daily run, not once: the script below reloads jobs.json and merges the new pages into it.

  1. Canonical URL

    Lowercase the host, drop the trailing slash and the tracking parameters: utm_*, Greenhouse's gh_src, Lever's lever-source and lever-origin, and the referral codes boards append to apply links. Two records with the same cleaned URL are one job. Leave source, ref and other parameters in place unless you have checked that a board uses them only for tracking: on some boards they identify the job.

  2. ATS key or requisition ID

    A Greenhouse URL carries the board token and the job ID (boards.greenhouse.io/acme/jobs/4012345), a Lever URL the company and the 36-character posting ID (jobs.lever.co/<company>/<posting-id>), and JobPosting.identifier the employer's own job ID. The same company and ID is one job, whichever board links to it.

  3. Description hash

    Strip the HTML, lowercase and collapse the whitespace, then hash the text (SHA-1 is enough). Boards often copy the employer's description word for word, so the same hash is the same job even when the title or URL differs. Skip very short descriptions, which many jobs share.

  4. Company, title and location after normalising

    Lowercase, drop punctuation and company suffixes, expand abbreviations (Sr. to senior) and compare city and state, not the full address. "Sr. Data Engineer, Acme Inc., Austin, TX" and "Senior Data Engineer, ACME, Austin, TX" then match. Exception: when both records carry an ATS job ID or a requisition ID and the IDs differ, they are two openings of one role in the same city, and the code keeps them apart.

  5. Review tier, never merged blind

    Same company and city, and either titles at least 90% alike (compared with their words sorted, so "Engineer, Data" and "Data Engineer" match) or descriptions with a word-shingle Jaccard similarity of 0.8 or more (sets of 5-word sequences; shared sequences divided by all sequences). These thresholds are starting points: tune them on a sample you check by hand. "Data Engineer" and "Senior Data Engineer" stay two jobs.

What goes wrong, and how the tiers handle it:

  • Two openings of one role in two cities are two jobs: the city is part of the key, so Austin and Denver never merge.
  • "Data Engineer" and "Senior Data Engineer" are two jobs: with their words sorted ("data engineer" against "data engineer senior"), Python's difflib.SequenceMatcher ratio is 0.79, under the 0.9 cut-off, so they are not even sent to review.
  • Two openings of one role in one city (two requisition IDs for "Data Engineer, Austin") stay two jobs: different IDs block the tier-4 merge.
  • A repost with a refreshed date keeps the old job if its text is unchanged (the same description hash); your first_seen shows how long it has been open.
  • Shared boilerplate: short descriptions ("Apply now to join our team") hash alike across many jobs, so texts under 300 characters are skipped for hashing.

The master record: the employer's own page (its ATS) first, then the most complete copy; every copy goes into sources. Tie-break: between two copies of equal rank, keep the one with more filled fields (salary, validThrough, full description), then the one you saw first. Dates are tiebreakers only: some boards refresh datePosted on every edit and reposts can keep an old one, so keep your own first_seen and last_seen from each run and use those. Closed jobs: a job gone from the employer's page, or with a past validThrough, is closed even if a board still lists it.

Before and after, with the code below:

SourceTitleCompanyCleaned URLMatched by
Employer (Greenhouse)Sr. Data EngineerAcme Inc.boards.greenhouse.io/acme/jobs/4012345 (gh_src dropped)master
Board ASenior Data EngineerAcmejobs.example-board.com/view/88812 (utm_source dropped)the same description hash
Board BSenior Data EngineerACME, Incwww.other-board.example/job/acme-senior-data-engineercompany, title and city
Feed1 job: the Greenhouse record as master, 3 URLs in sources

The merged record those three copies become in jobs.json (some fields left out, description shortened):

JSON
{
  "url": "https://boards.greenhouse.io/acme/jobs/4012345",
  "title": "Sr. Data Engineer",
  "company": "Acme Inc.",
  "location": "Austin, TX",
  "req_id": null,
  "description": "we are hiring a senior data engineer to build …",
  "first_seen": "2026-10-21",
  "last_seen": "2026-10-28",
  "sources": [
    "https://boards.greenhouse.io/acme/jobs/4012345",
    "https://jobs.example-board.com/view/88812",
    "https://www.other-board.example/job/acme-senior-data-engineer"
  ]
}

Where job data lives

Job listings are spread over general job boards, aggregators such as the Google Jobs panel, freelance marketplaces and thousands of company careers pages. A job scraper reads the public pages a job seeker sees. The Web Scraping API, in development, is designed to fetch any of those URLs and return the page; your code pulls the fields out. ProxyField sells no separate jobs API with pre-parsed fields.

SourceWhat its public pages showMode that usually works
Job board search (keyword + location)Title, company, location, salary when shown, posted date, link to the job pageJS rendering on boards with heavy bot protection
Job page on a boardFull description, job type, salary, benefits, apply linkPlain HTTP or JS, per board
Company careers page on an ATS (Greenhouse, Lever, Workday)Open roles, departments, locations, descriptionsPlain HTTP for most; JS for Workday
Greenhouse and Lever public job feedsEvery open role of one company as JSON: boards-api.greenhouse.io/v1/boards/<token>/jobs, api.lever.co/v0/postings/<company>?mode=jsonPlain HTTP
Upwork public job pagesTitle, budget or hourly range, skills, client countryOnly the public part; most search is behind a login
Fiverr gig pagesGig title, seller level, starting price, rating, review countJS rendering

As designed, the API returns the page itself as HTML, Markdown or plain text, or a JSON envelope with the status code, the final URL, the headers, the cookies and the content; it has no parsed job fields, so your parser reads them. Guides for single boards: Indeed scraper, Glassdoor scraper and LinkedIn scraper; none of them is a separate API.

The pipeline: from many sites to one feed

  1. Discover run keyword and location searches on each board, and keep a list of company careers URLs and their ATS feeds.

  2. Fetch send each URL to the Web Scraping API, plain HTTP first, JS rendering only where a source needs it.

  3. Parse many job pages carry schema.org JobPosting markup in a JSON-LD script (title, hiringOrganization, jobLocation, baseSalary, datePosted, validThrough), so one parser reads many sites. Fall back to per-site selectors where the markup is missing.

  4. Deduplicate collapse the copies of one job into one record (the five tiers above).

  5. Store and track keep first_seen and last_seen per job; a job you stop seeing is closed, and one that comes back with a new ID is a repost.

One record in the feed, after the merge:

JSON
{
  "url": "https://boards.greenhouse.io/examplecompany/jobs/1234567",
  "title": "Sr. Data Engineer",
  "company": "Example Inc.",
  "location": "Austin, TX",
  "salary": {"currency": "USD", "value": {"minValue": 150000, "maxValue": 180000, "unitText": "YEAR"}},
  "employment_type": "FULL_TIME",
  "req_id": "R-2041",
  "date_posted": "2026-10-20",
  "valid_through": "2026-12-01",
  "first_seen": "2026-10-21",
  "last_seen": "2026-10-27",
  "sources": [
    "https://boards.greenhouse.io/examplecompany/jobs/1234567",
    "https://jobs.example-board.com/view/88812",
    "https://www.other-board.example/job/example-senior-data-engineer"
  ]
}

Build the feed in Python, through ProxyField proxies

This script fetches job pages through a rotating US residential IP, reads their JobPosting markup, merges duplicates across sources by the five tiers above (URL, ATS key, requisition ID, description hash, normalised company, title and city), lists near-matches for review and writes jobs.json; run it daily and it keeps first_seen and last_seen for every job and flags jobs not seen today or past their validThrough. ProxyField gives you a new IP on every request or a sticky session from 30 s to 36 h, with 412M+ residential IPs in 195+ countries and targeting by country, region (state) and city.

Read moreShow less
Python
import hashlib, json, re
from datetime import date
from difflib import SequenceMatcher
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
import requests
from bs4 import BeautifulSoup

PROXY = "http://<login>-country-us:<password>@gate.proxyfield.io:7000"
TODAY = date.today().isoformat()
TRACKING = re.compile(r"^(utm_\w+|gh_src|lever-source(\[\])?|lever-origin)$")  # add source, ref ... only where a board uses them for tracking
COMPANY_SUFFIX = re.compile(r"\b(inc|llc|ltd|gmbh|corp)\b")
ABBR = {"sr": "senior", "jr": "junior", "mgr": "manager", "eng": "engineer"}

def canonical_url(url):
    p = urlsplit(url)
    query = [(k, v) for k, v in parse_qsl(p.query) if not TRACKING.match(k)]
    return urlunsplit((p.scheme, p.netloc.lower(), p.path.rstrip("/"), urlencode(query), ""))

def norm(text, company=False):
    text = (text or "").lower()
    if company:
        text = COMPANY_SUFFIX.sub(" ", text)
    return " ".join(ABBR.get(w, w) for w in re.sub(r"[^a-z0-9]+", " ", text).split())

def clean_description(html):
    return " ".join(BeautifulSoup(html or "", "html.parser").get_text(" ").lower().split())

def ats_key(url):
    m = (re.search(r"greenhouse\.io/([^/]+)/jobs/(\d+)", url)
         or re.search(r"jobs\.lever\.co/([^/]+)/([0-9a-f-]{36})", url))
    return ("ats", m.group(1).lower(), m.group(2)) if m else None

def postings(url):
    """Fetch one job page through a rotating residential IP; yield its JobPosting records."""
    html = requests.get(url, proxies={"http": PROXY, "https": PROXY},
                        headers={"User-Agent": "Mozilla/5.0"}, timeout=30).text
    for tag in BeautifulSoup(html, "html.parser").find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except json.JSONDecodeError:
            continue
        for item in data if isinstance(data, list) else [data]:
            if not (isinstance(item, dict) and item.get("@type") == "JobPosting"):
                continue
            org = item.get("hiringOrganization") or {}
            place = item.get("jobLocation") or {}
            place = place[0] if isinstance(place, list) and place else place
            addr = place.get("address") or {} if isinstance(place, dict) else {}
            if not isinstance(addr, dict):
                addr = {"addressLocality": str(addr)}
            ident = item.get("identifier")
            u = canonical_url(url)
            yield {"url": u, "title": item.get("title"),
                   "company": org.get("name") if isinstance(org, dict) else org,
                   "location": ", ".join(filter(None, [addr.get("addressLocality"), addr.get("addressRegion")])),
                   "salary": item.get("baseSalary"), "employment_type": item.get("employmentType"),
                   "req_id": ident.get("value") if isinstance(ident, dict) else ident,
                   "date_posted": item.get("datePosted"), "valid_through": item.get("validThrough"),
                   "description": clean_description(item.get("description")),
                   "first_seen": TODAY, "last_seen": TODAY, "sources": [u]}

def keys(rec):  # strongest signal first
    yield ("url", rec["url"])
    if ats_key(rec["url"]):
        yield ats_key(rec["url"])
    if rec.get("req_id"):
        yield ("req", norm(rec["company"], True), str(rec["req_id"]))
    if len(rec.get("description", "")) >= 300:  # short texts are shared by many jobs
        yield ("desc", hashlib.sha1(rec["description"].encode()).hexdigest())
    yield ("job", norm(rec["company"], True), norm(rec["title"]), norm(rec["location"]))

def conflict(a, b):
    """Two different openings: both carry an ATS job ID or a requisition ID, and the IDs differ."""
    ka, kb = ats_key(a["url"]), ats_key(b["url"])
    if ka and kb and ka != kb:
        return True
    return bool(a.get("req_id") and b.get("req_id") and str(a["req_id"]) != str(b["req_id"]))

def merge(records):
    feed, index = [], {}
    for rec in records:
        ks = list(keys(rec))
        hit = next((index[k] for k in ks if k in index and not conflict(index[k], rec)), None)
        if hit is None:
            feed.append(rec)
            hit = rec
        else:
            seen = (min(hit["first_seen"], rec["first_seen"]), max(hit["last_seen"], rec["last_seen"]))
            sources = sorted(set(hit["sources"]) | set(rec["sources"]))
            if ats_key(rec["url"]) and not ats_key(hit["url"]):
                hit.update(rec)  # the employer's own page becomes the master record
            hit.update(first_seen=seen[0], last_seen=seen[1], sources=sources)
        for k in ks:
            index[k] = hit
    return feed

def shingles(text, n=5):
    words = text.split()
    return {" ".join(words[i:i + n]) for i in range(max(1, len(words) - n + 1))}

def jaccard(a, b):
    sa, sb = shingles(a), shingles(b)
    return len(sa & sb) / len(sa | sb) if sa | sb else 0.0

def maybe_same(a, b):
    """Review tier: same company and city, and similar titles or descriptions. Flag, never merge blind."""
    if (norm(a["company"], True), norm(a["location"])) != (norm(b["company"], True), norm(b["location"])):
        return False
    ta, tb = (" ".join(sorted(norm(r["title"]).split())) for r in (a, b))
    return (SequenceMatcher(None, ta, tb).ratio() >= 0.9
            or jaccard(a.get("description", ""), b.get("description", "")) >= 0.8)

urls = [  # replace with real job pages from boards and careers sites
    "https://boards.greenhouse.io/examplecompany/jobs/1234567",
    "https://www.example.com/careers/data-engineer",
]
try:
    with open("jobs.json") as f:
        old = json.load(f)
except FileNotFoundError:
    old = []
feed = merge(old + [rec for url in urls for rec in postings(url)])
review = [(a["url"], b["url"]) for i, a in enumerate(feed) for b in feed[i + 1:] if maybe_same(a, b)]
with open("jobs.json", "w") as f:
    json.dump(feed, f, indent=2)
closed = [job for job in feed if job["last_seen"] < TODAY or (job["valid_through"] or "9999") < TODAY]
print(len(feed), "jobs,", len(review), "pairs to review,", len(closed), "possibly closed")
# on the three copies in the before-and-after table: 1 jobs, 0 pairs to review, 0 possibly closed

Access opens once the payment is confirmed; the gateway, credentials or allowlist are in your account. The username format is <login>-country-us-city-new_york; other tokens: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default) after the location, as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600.

Residential proxies cost $2.95/GB for 1 GB and from $1.95/GB at volume, with no subscription; the traffic never expires, and all purchases are final. For boards that need a browser, use Playwright or Puppeteer with the same gateway. To run the script on a timetable, see How to Schedule a Web Scraper.

What you send

As designed (in development, access through Contact sales), you send a job page or search URL to the Web Scraping API and get the page back. The API takes a POST to https://api.proxyfield.io/v1/scrape with your API key in an Authorization: Bearer <api_key> header and a JSON body: url, render (false for plain HTTP, true for JS rendering), country and output (html, markdown, text or json). For boards that build their pages with scripts, wait_for takes a CSS selector to wait for, with rendering. Parameter reference: API documentation.

Shell
# The request the Web Scraping API takes (access through Contact sales)
curl https://api.proxyfield.io/v1/scrape \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://boards.greenhouse.io/examplecompany", "render": false, "country": "us", "output": "json"}'

Example response (shortened; content is the page's HTML):

JSON
{
  "metadata": {"id": "req_74d92acd17e7e780", "api": "web_scrape", "status": "success", "billed": true},
  "status_code": 200,
  "final_url": "https://boards.greenhouse.io/examplecompany",
  "content_type": "text/html; charset=utf-8",
  "content": "<!DOCTYPE html><html lang=\"en\">…</html>"
}
Pricing

Pricing: $0.50 per 1,000 pages, from $0.20

You pay per successful result, and the price depends only on the mode: plain HTTP $0.50 per 1,000, or $0.20 from 10,000 results in one purchase; JS rendering $2.00 per 1,000, from $1.00. Billed: a page the site answered with a 2xx status; a target's 404 or 410, and a sign-in page the site serves with a 200 status, also count as billed results. Not billed: blocked requests, timeouts and errors on our side; a blocked request is retried on new IPs, and the retries are not billed. Every response says whether it was billed.

Payment and balance for the API: the same crypto balance as proxies, which never expires; there is no subscription or monthly fee. For availability and access, contact sales through the sales team on our 24/7 live chat. All prices: pricing.

Through the sales team on our 24/7 live chat

Workload (30-day month)
Workload (30-day month), Mode, Pages a month, Entry price, Volume price
Workload (30-day month)ModePages a monthEntry priceVolume price
2,000 company careers pages, dailyPlain HTTP60,000$30$12
500 searches on 4 boards, 2 result pages each, dailyJS rendering120,000$240$120
5,000 new job pages, dailyPlain HTTP150,000$75$30
All three330,000$345$162

When a job board refuses a request

Large job boards guard their search pages and answer bursts of automated traffic with a 403, a 429 or a challenge page. With the Web Scraping API you pay only for successful results. With your own scraper, keep a low rate per site, rotate IPs, cache job pages you already have, and render JavaScript only where the data needs it. A careers page on an ATS is usually far less guarded than a board's search, which is another reason to collect from employers first.

Use cases

What teams use job data for

  • Job aggregators and niche boards that list roles from many sources in one place.
  • Labour-market research: demand for skills, roles and locations over time.
  • Salary benchmarking from the pay ranges that postings show.
  • Competitive hiring intelligence: which teams a competitor is growing.
  • Sales signals: a company hiring for a role often needs the tools that role uses; see lead scraping.

Collect public postings only: candidate profiles behind a login, fake accounts and mass applications are out of bounds under ProxyField's acceptable use policy.

More scrapers

More scrapers

FAQ

Job Scraper FAQ

24/7 support by live chat, including help with setup and integration.

What's a good job scraping tool that pulls listings from many job boards and company careers pages into one feed?

ProxyField's route today is three free or low-cost parts run through its residential proxies (from $1.95/GB, Easy Trial 1 GB for $2.95): the open-source JobSpy library searches LinkedIn, Indeed, Glassdoor, ZipRecruiter and 4 more boards in one call, Greenhouse and Lever return each company's open roles as JSON in one request, and your own parser reads every other job page's JobPosting markup, then deduplicates and stores first-seen and last-seen dates. ProxyField's Web Scraping API, in development through Contact sales, is priced to fetch those other pages at $0.50 per 1,000 successful results ($0.20 from 10,000): a full feed of 330,000 pages a month would cost $345 at the entry price, $162 at the volume price. If you only want to search jobs, Indeed already aggregates many sources for free, and a vendor such as LinkUp sells finished job data.

Is there a job scraper tool that searches several job boards at once?

Yes: JobSpy, an open-source Python library under the MIT licence, searches LinkedIn, Indeed, Glassdoor, ZipRecruiter, Bayt, Naukri, BDJobs and HelloWork in one scrape_jobs() call and returns one table, about 1,000 jobs per search per board at most. It takes a proxy list in user:pass@host:port form, so <login>-country-us:<password>@gate.proxyfield.io:7000 gives every request a new US residential IP. It does not read company careers pages, and its README says its Google Jobs source is currently unavailable.

How much does it cost to scrape job boards?

ProxyField's Web Scraping API (in development, Contact sales) is priced at $0.50 per 1,000 successful results over plain HTTP, from $0.20 at volume, and $2.00 per 1,000, from $1.00, with JS rendering; your own scraper on residential proxies starts at 1 GB for $2.95. Reading 2,000 company careers pages a day is 60,000 pages a month: $30 at the entry price. You pay only for successful results.

What is job scraping?

Job scraping is the automated collection of public job postings from job boards, aggregators and company careers pages. A script or API fetches the pages and turns each posting into fields such as title, company, location, salary and posted date.

Is there a job scraper API?

Some providers sell parsed job APIs for particular boards. ProxyField's route is the Web Scraping API, in development through Contact sales, which is designed to fetch any public job page for $0.50 per 1,000 successful results; parsing the fields is up to your code, or the JobPosting markup many pages carry.

How do I scrape job boards?

Run keyword and location searches, collect the job page links from each results page, then fetch every job page and parse it. Pace the requests per board, and render JavaScript only on boards whose results load with scripts.

How do I build a job scraper?

Start with a list of sources and search URLs, a fetcher (your own code on rotating proxies, or the Web Scraping API) and a parser that reads JobPosting JSON-LD first. Add deduplication, a store with first-seen and last-seen dates, and a daily schedule, as in the Python on this page.

How do I deduplicate job postings scraped from several job boards when the same job is listed on all of them?

Match from the strongest signal down: the same canonical URL with tracking parameters removed, then the same ATS key or requisition ID (Greenhouse job ID, Lever posting ID, JobPosting.identifier), then the same hash of the cleaned description, then the same company, title and city after normalising ("Sr." to "senior", no "Inc" or "LLC"). Keep the employer's page as the master record and list every copy in sources; pairs with titles at least 90% alike or descriptions with a 5-word-shingle Jaccard similarity of 0.8 or more go to review, not to an automatic merge. Run the merge on every daily run, and use your own first-seen and last-seen dates, not the boards' posted dates, which some boards refresh.

How do I scrape Google Jobs listings when they only show up inside Google's search results?

Google has no public API that returns the Google Jobs panel, so collect the jobs from the pages it links to: each listing's "apply on" link leads to the employer's or board's job page, which usually carries JobPosting markup with the title, company, location, salary and validThrough. ProxyField's Web Scraping API fetches those pages for $0.50 per 1,000 successful results, or $0.20 from 10,000 results: 50,000 job pages a month cost $25 at the entry price, $10 at the volume price. To see which jobs Google shows for a query, the Google SERP API returns the results page as HTML for $1.20 per 1,000; it does not parse the jobs panel, so your code reads the panel and its outbound job links from the HTML. Both APIs are in development, through Contact sales.

Does Google have a Jobs API?

Not one that returns Google's job listings. Google's job tools are for publishing: JobPosting markup, the Indexing API (for job and livestream pages only) and Cloud Talent Solution, which searches jobs a company supplies itself. To read the jobs, fetch the employer and board pages, or the Google results page.

Show 16 more questionsShow fewer questions

What fields does Google need in JobPosting markup?

Required: title, description, datePosted, hiringOrganization and jobLocation (or applicantLocationRequirements with jobLocationType for remote jobs). Recommended: baseSalary, validThrough, employmentType, directApply and identifier. The same fields are what a job scraper reads.

What is a Google Jobs scraper API?

A service that runs job searches on Google and returns the listings from the jobs panel as data. Google itself offers no public API for reading those results; its job tools (JobPosting markup, the Indexing API, Cloud Talent Solution) are for publishing jobs, not reading Google's.

How do I scrape jobs from company careers pages?

Find which applicant tracking system hosts the page: Greenhouse, Lever and Workday boards each follow one layout, so one parser covers every company on that system. Greenhouse and Lever also publish public job feeds as JSON (boards-api.greenhouse.io/v1/boards/<token>/jobs, api.lever.co/v0/postings/<company>?mode=json), which are easier to read than the HTML.

Can I scrape CareerBuilder job listings?

Public pages only, and follow CareerBuilder's terms of use. Check first that the search results and job pages you plan to collect are still public and live, and test whether a page loads over plain HTTP before paying for JS rendering.

How do I scrape Upwork job listings?

Only Upwork's public job pages are in scope; most of its job search needs a freelancer login, and data behind a login you are not entitled to is out of bounds. For full access, apply for Upwork's own API, which serves approved applications.

Is there an Upwork scraper API?

ProxyField has no Upwork API. Upwork offers its own API to approved applications, which is the route for anything beyond its public pages; for public pages, follow Upwork's terms.

Can I scrape Fiverr gigs?

Public pages only, and follow Fiverr's terms. Its public category and gig pages show titles, seller levels, starting prices, ratings and review counts, and Fiverr builds much of each page with scripts, so plan for JS rendering ($2.00 per 1,000 pages on the Web Scraping API's price list) and a low request rate.

What data can you scrape from job postings?

Usually the job title, company, location or remote status, salary range when shown, employment type, description, posted date, expiry date and apply link. Pages with JobPosting markup carry most of these as named fields (title, hiringOrganization, jobLocation, baseSalary, datePosted, validThrough).

How do I extract structured data from job pages?

Look for a <script type="application/ld+json"> block with "@type": "JobPosting" and read it as JSON, as in the Python on this page. Where a page has none, fall back to CSS selectors for that site.

How often should you scrape job postings?

Daily suits most boards and careers pages, since postings change by the day. Fast-moving searches can run several times a day; long-open roles need only a weekly check of their job page.

Can I scrape job postings for a specific city?

Yes: most boards take the city as a search parameter, so the location is part of the URL. With your own scraper, ProxyField residential proxies also target country, region and city, for boards that change results with the visitor's location.

How do you track job postings that have been removed?

Store the date you first and last saw each posting, and recheck its URL on your schedule. A job that is gone from the employer's page, or returns an error or an "expired" page, is closed, even if a board still lists it.

How can I handle dynamic content when scraping job postings?

Check the page source first: many boards ship the listings as JSON inside a script tag or call a JSON endpoint you can read directly. Where nothing loads without scripts, use JS rendering in the Web Scraping API or a browser such as Playwright.

What do recruiting teams use job scraping for?

Agencies and in-house recruiters track which companies are hiring for which roles, benchmark salaries and spot new openings before competitors. The data comes from public postings, not candidate profiles.

Should I use a job scraping service or build my own?

A service saves the fetching and anti-bot work and bills per page; your own scraper on proxies costs less per page at high volume but needs maintenance. Many teams fetch through an API and keep the parsing and deduplication in-house.

What kinds of job board scraping software are there?

Open-source libraries such as Scrapy, Beautiful Soup and Playwright, no-code scraping tools with ready templates, scraper APIs that fetch pages for you, and data vendors that sell finished job datasets. They differ in cost, control and how much upkeep falls on you.

Start collecting public pages with the Web Scraping API

Billed on successful results only. Contact sales for the Web Scraping API and the volume you need, or run your own scraper on residential proxies. 24/7 live chat helps with setup and integration.