Job postings live in two places. Job boards collect them, and companies publish them at the source, usually through the hiring system behind their careers page. Most big boards forbid scraping. Several hiring systems publish every open posting through a public API on purpose.
On October 12, 2026, we read the robots.txt files of 15 job sites and the terms of Indeed and LinkedIn. Then we collected postings through two hiring systems' public APIs. Every request ran from our own server, and we fetched no page that a robots.txt file forbids.
The short answer
- Collect at the source first. Greenhouse's and Lever's public APIs returned 18 and 10 postings in one request each, with no key.
- Use official job APIs where they exist. USAJOBS and Adzuna give keys to anyone who registers.
- Read a board's robots.txt and terms before anything else. LinkedIn and ZipRecruiter disallow every crawler they do not name. Indeed's terms bar automated access without written permission.
- Read robots.txt with a parser written to the standard. Python's built-in reader got 4 of 26 job addresses wrong in our test.
- A proxy changes your address, not a site's rules.
What 15 job sites' robots.txt files say
For each site we asked about two addresses in the site's own format, a job search and a single job. We asked for a crawler without its own rules and for Googlebot. We fetched only the robots.txt files.
Six of the 13 files we could read restrict job addresses.
| Site | Job search | Single job | Rule that decides |
|---|---|---|---|
| disallowed | disallowed | Disallow: / for crawlers it does not name | |
| ZipRecruiter | disallowed | disallowed | Disallow: / for crawlers it does not name |
| Indeed | allowed | disallowed | Disallow: /viewjob |
| CareerBuilder | allowed | disallowed | Disallow: /job/, Googlebot too |
| Dice | disallowed | allowed | Disallow: /jobs?q*, Googlebot too |
| Welcome to the Jungle | disallowed | allowed | Disallow: */jobs?query=*, Googlebot too |
Both addresses allowed
Glassdoor
Monster
Wellfound
USAJOBS
Germany's Federal Employment Agency
Greenhouse
the job pages it hosts for its customers
Lever
the job pages it hosts for its customers
Job addresses disallowed
LinkedIn
both, for every crawler it does not name
ZipRecruiter
both, for every crawler it does not name
Indeed
the single job
CareerBuilder
the single job, Googlebot too
Dice
the job search, Googlebot too
Welcome to the Jungle
the job search, Googlebot too
LinkedIn's file is 120,190 bytes long, with 76 groups of rules. Googlebot may crawl LinkedIn's and ZipRecruiter's job pages, and other crawlers may not. Glassdoor, Monster, Wellfound, USAJOBS and the job search of Germany's Federal Employment Agency allowed both addresses. So did the job pages that Greenhouse and Lever host for their customers. SimplyHired answered our request for its robots.txt with 403, and StepStone did not answer within 60 seconds.
An allowed address is not permission. The robots.txt standard, RFC 9309, says it plainly. "These rules are not a form of access authorization". A site's terms can forbid what its robots.txt allows.
What the terms say
Indeed's terms tie the two together. They bar "any automated system (bots, scrapers, spiders, AI or Agentic AI)" from the site without Indeed's "express written permission". In brackets they add "we conditionally grant permission to crawl the Site solely as outlined in our robots.txt file". The next sentence is broader. "You may not crawl, scrape, extract data from, reproduce, duplicate, copy, sell, exploit, trade or resell any part of the Site".
LinkedIn's User Agreement bars software, scripts and robots "to scrape or copy the Services, including profiles and other data from the Services". Glassdoor's and ZipRecruiter's terms pages answered our server with 403, so we could not read them.
Our pages on Indeed, LinkedIn and Google Jobs go through each site's rules. They also cover the official data each one offers.
Collect at the source: public job APIs
Companies that hire through Greenhouse or Lever publish their open jobs through public APIs. Greenhouse's documentation says "Job Board data is publicly available, so authentication is not required for any GET endpoints". Lever's says "all job postings in the published state are publicly viewable". It adds "These jobs may be scraped by third parties".
import requests
HEADERS = {"User-Agent": "job-collector/1.0 (+https://example.com/contact)"}
board = "greenhouse" # from the board's address: job-boards.greenhouse.io/greenhouse
r = requests.get(f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs",
params={"content": "true"}, headers=HEADERS, timeout=30)
r.raise_for_status()
jobs = r.json()["jobs"]
print(len(jobs), "postings")
for job in jobs[:3]:
print(job["title"], "|", job["location"]["name"], "|", job["updated_at"][:10])
18 postings
Commercial Counsel | Eastern Timezone | 2026-10-09
Enterprise Sales Development Representative | Anywhere in the United States | 2026-10-09
Growth Account Executive | Anywhere in Ireland | 2026-09-02
Greenhouse's own board came back in one response of 220,522 bytes. Each posting carried its full description, location, departments and dates. The board name comes from the board's address, so a company's board at job-boards.greenhouse.io/name is read with the name name.
Lever works the same way. The site name below is the demo site that Lever's own documentation uses.
site = "leverdemo" # the demo site Lever's own documentation uses
r = requests.get(f"https://api.lever.co/v0/postings/{site}", params={"mode": "json"}, headers=HEADERS, timeout=30)
r.raise_for_status()
postings = r.json()
print(len(postings), "postings")
for p in postings[:3]:
pay = p.get("salaryRange")
pay_text = f"{pay['min']} to {pay['max']} {pay['currency']}" if pay else "no pay range"
print(p["text"], "|", p["categories"].get("location"), "|", pay_text)
10 postings
Customer Success Manager | Atlanta, Georgia | 85000 to 175000 USD
Dentist | Houston, TX | 150000 to 185000 USD
Director of Diversity, Equity, and Inclusion | London, UK | no pay range
Lever's own site returned an empty list that day, so the demo site stands in. Neither API sent rate-limit headers. Lever's job pages ask for a Crawl-delay of 1 second, and we kept it for its API too.
Job postings inside the page: JobPosting data
Google shows job postings in its job search when a page carries schema.org JobPosting data. Its guide lists five required properties: datePosted, description, hiringOrganization, jobLocation and title. Where a page carries that block, a parser can read it without guessing at the layout.
import json
import time
from bs4 import BeautifulSoup
time.sleep(1) # jobs.lever.co asks for a Crawl-delay of 1 second
page = requests.get(postings[0]["hostedUrl"], headers=HEADERS, timeout=30)
soup = BeautifulSoup(page.text, "lxml")
for tag in soup.find_all("script", type="application/ld+json"):
data = json.loads(tag.string)
if data.get("@type") == "JobPosting":
print(data["title"], "|", data["datePosted"], "|", data["hiringOrganization"]["name"])
print(sorted(data))
Customer Success Manager | 2026-08-31 | Lever Demo 2
['@context', '@type', 'datePosted', 'description', 'employmentType', 'hiringOrganization', 'jobLocation', 'title']
Lever's job page carried all five required properties. The page Greenhouse hosts for its own job carried no JobPosting block, even after its scripts ran in Chrome. So read the API where a company has one, and use JobPosting data where a page carries it.
Read robots.txt the way the standard does
Python's built-in urllib.robotparser and Protego disagreed on 4 of our 26 job addresses. Protego is the "Pure-Python robots.txt parser with support for modern conventions" that Scrapy uses.
import urllib.robotparser
from protego import Protego
robots = requests.get("https://www.indeed.com/robots.txt", headers=HEADERS, timeout=30).text
url = "https://www.indeed.com/viewjob?jk=1a2b3c4d5e6f7a8b"
builtin = urllib.robotparser.RobotFileParser()
builtin.parse(robots.splitlines())
print("urllib.robotparser:", builtin.can_fetch("job-collector", url))
print("protego (RFC 9309):", Protego.parse(robots).can_fetch(url, "job-collector"))
urllib.robotparser: True
protego (RFC 9309): False
Indeed's rules for all crawlers start with Allow: /, and Disallow: /viewjob comes later. The standard says "The most specific match found MUST be used", so the longer rule wins and the job page is off limits. Python's reader returns the first rule that matches, and its own source compares plain prefixes with filename.startswith(self.path).
The other three disagreements followed from the same two habits. Python allowed CareerBuilder's job pages and Welcome to the Jungle's search, whose rule uses * wildcards. It refused Dice's job pages, which the standard allows. If your crawler checks robots.txt, check it with a parser like Protego.
Official job APIs with a key
USAJOBS is "The Federal Government's official employment site". Its developer guide says "Accessing the various USAJOBS APIs will require an API Key", and it wants your email address as the User-Agent. Adzuna's API asks you to "Register to receive an app_key and app_id". We did not register for either, so we did not test them.
Where a proxy fits
Public APIs, and pages a robots.txt file allows, need no proxy. A proxy changes the address a site sees and nothing in its terms. Our page on whether web scraping is legal covers the court cases behind these rules.
A proxy helps in one case: a site that allows the visit shows different listings to different countries or cities. Then an exit in that place shows the local listings.
What are the best residential proxies for job data in 2026?
For job data, the best fit is a small purchase with country or city targeting, since a run on allowed pages moves little traffic. We read each seller's own pricing page on October 26, 2026:
| Seller | Smallest buy, Oct 26 | Why it fits job data |
|---|---|---|
| HProxy Lite | 1 GB at $0.50/GB | country, never expires |
| Rainproxy | 1 GB at $0.99, $0.60 at 10 GB | traffic never expires |
| DataImpulse | 5 GB for $5 | free country targeting |
| MarsProxies | 1 GB at $4.99 | city targeting, no expiry |
| Evomi | 15 GB at $0.99 per GB | city and ISP included |
| HProxy Premium | 1 GB at $1.49/GB | state and city |
The smallest purchase differs more than the rate per GB suggests:
City listings cost more at the cheapest sellers: Rainproxy sells city and ASN targeting on a separate pool from $2.98 per GB. Our best residential proxies guide compares ten sellers in full.
What this page could not check
- Each robots.txt file was read once. SimplyHired refused our server and StepStone did not answer.
- The test addresses follow each site's own link format. A site may use other formats as well.
- We read the terms of Indeed and LinkedIn. Glassdoor and ZipRecruiter refused our server.
- Two hiring systems were tested. The documentation of four more (Ashby, SmartRecruiters, Recruitee and Personio) asked our server to prove it was human, so we left them out.
- The seller table repeats each pricing page as published on October 26, 2026, without a purchase from any of them.
- This page is not legal advice.
- We will check this page again by January 12, 2027.
Sources
- Greenhouse, Job Board API: developers.greenhouse.io.
- Lever, Postings API README: github.com.
- Indeed, Terms of Service: indeed.com.
- LinkedIn, User Agreement: linkedin.com.
- The robots.txt files of the 15 sites in the table and the capture, read on our server.
- RFC 9309, Robots Exclusion Protocol: rfc-editor.org.
- CPython 3.12,
Lib/urllib/robotparser.py: github.com. - Protego 0.7.0 on PyPI: pypi.org.
- Google Search Central, job posting structured data: developers.google.com.
- schema.org, JobPosting: schema.org.
- USAJOBS developer guide, authentication: developer.usajobs.gov.
- Adzuna API overview: developer.adzuna.com.
- Rainproxy, DataImpulse, MarsProxies and Evomi, residential pricing pages, read October 26, 2026.
- Our own tests on our server, October 12, 2026: boards_check.py at 08:33 UTC, robots_parsers.py at 08:34 UTC, ats_apis.py at 08:25 UTC and the code on this page at 08:32 UTC.



