- highA closed vacancy still returns HTTP 200
- iRecruit answers a stale RequestID with the organization page rather than a 404, so status codes alone cannot tell you the role has gone. Require the page to carry hidden OrgID and RequestID inputs matching the request; a first-party page without that native form is the provider's own 'no longer posted' answer.
- highDropping the category parameters empties the board
- Boards addressed with olnew/slnew or level/menulist publish only that slice of the vacancies. Keeping the OrgID but discarding the selection can turn a board with dozens of postings into an empty landing page, so treat the selection as part of the board's address.
- mediumDetail links appear in two different shapes
- Some boards link vacancies through index.php with a RequestID and others through jobRequest.php?source=XML. Both are first-party and both publish the full body and application form, so accept either rather than filtering to a single route and losing whole tenants.
- lowThe description swallows the apply controls
- The vacancy body and the application buttons share a container. Cut the markup at the reqbuttons element before storing the description, otherwise every posting ends with a block of form controls and boilerplate button labels.
iRecruit US Jobs API.
Read vacancies from the transit authorities, hospitals and clinics that run iRecruit, where a whole organization's board is one server-rendered page addressed by an eight-digit OrgID.
What's in every response.
Data fields, real-world applications, and the companies already running on iRecruit US.
Data fields
- Complete Board In One Request
- Full Vacancy Bodies
- Category & Level Sub-Boards
- Location Lines
- Provider-Issued Request IDs
- Native Application Forms
Use cases
- 01Healthcare & Transit Job Aggregation
- 02Regional Employer Monitoring
- 03Careers Page Extraction
- 04ATS Data Pipelines
Trusted by
- Greater Bridgeport Transit
- Newman Regional Health
- HealthCore Clinic
How to scrape iRecruit US.
Step-by-step guide to extracting jobs from iRecruit US-powered career pages—endpoints, authentication, and working code.
import re
from urllib.parse import urlparse, parse_qs
HOST = "www.irecruit-us.com"
ORG_ID = re.compile(r"^I\d{8}$")
REQUEST_ID = re.compile(r"^[A-Za-z0-9]{10,64}$")
def parse_board(url: str) -> tuple[str, str] | None:
parsed = urlparse(url)
if parsed.netloc.lower() != HOST or parsed.path.lower() != "/index.php":
return None
query = parse_qs(parsed.query)
org = (query.get("OrgID") or [""])[0].upper()
if not ORG_ID.match(org):
return None
# Category / level selections are part of the board identity, not noise.
suffix = ""
if "olnew" in query and "slnew" in query:
suffix = f"&olnew={query['olnew'][0]}&slnew={query['slnew'][0]}"
elif "level" in query:
suffix = f"&level={query['level'][0]}"
if "menulist" in query:
suffix += f"&menulist={query['menulist'][0]}"
return org, f"https://{HOST}/index.php?OrgID={org}&navpg=listings{suffix}"
org, board_url = parse_board("https://www.irecruit-us.com/index.php?OrgID=I20100401&navpg=listings")
print(board_url)import requests
from bs4 import BeautifulSoup
def fetch_board(session: requests.Session, org: str, board_url: str) -> BeautifulSoup:
response = session.get(
board_url,
headers={"Accept": "text/html,application/xhtml+xml"},
timeout=30,
)
response.raise_for_status()
# iRecruit prints its own organization id into the page script.
if not re.search(rf"var\s+OrgID\s*=\s*['\"]{org}['\"]", response.text, re.IGNORECASE):
raise RuntimeError("iRecruit page omitted its first-party organization proof")
if parse_board(response.url) != (org, board_url):
raise RuntimeError("iRecruit board redirected outside the requested organization")
return BeautifulSoup(response.text, "html.parser")
session = requests.Session()
soup = fetch_board(session, org, board_url)from urllib.parse import urljoin
def collect_listings(soup: BeautifulSoup, org: str, board_url: str) -> list[dict]:
listings, seen = [], set()
for anchor in soup.select("a[href*='RequestID=']"):
target = urljoin(board_url, anchor["href"])
parsed = urlparse(target)
query = parse_qs(parsed.query)
request_id = (query.get("RequestID") or [""])[0]
if (query.get("OrgID") or [""])[0].upper() != org:
continue
if not REQUEST_ID.match(request_id) or request_id in seen:
continue
bold = anchor.find("b")
title = " ".join((bold or anchor).get_text().split())
if not title:
continue
seen.add(request_id)
listings.append({"id": request_id, "title": title, "listing_url": target})
return listings
listings = collect_listings(soup, org, board_url)
print(f"{len(listings)} vacancies on {org}")def fetch_detail(session: requests.Session, org: str, listing: dict) -> dict | None:
response = session.get(listing["listing_url"], timeout=30)
if response.status_code in (404, 410):
return None # canonical removal
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
def has_hidden(name: str, value: str) -> bool:
return any(
(field.get("value") or "").upper() == value.upper()
for field in soup.select(f"input[name='{name}']")
)
# A live request keeps its native application form. Without it the server has
# returned the organization surface for a request that no longer exists.
if not has_hidden("OrgID", org) or not has_hidden("RequestID", listing["id"]):
return None
container = soup.select_one("#reqbuttons")
container = container.parent if container else soup.select_one("#page-wrapper")
body = container.decode_contents() if container else ""
body = body.split('<div id="reqbuttons"', 1)[0] # drop the apply controls
location = re.search(r"<b>\s*Location:\s*</b>\s*([^<]+)", body, re.IGNORECASE)
meta_title = soup.select_one("meta[property='og:title']")
return {
**listing,
"title": (meta_title.get("content") if meta_title else None) or listing["title"],
"description_html": body.strip() or None,
"location": " ".join(location.group(1).split()) if location else None,
}
for listing in listings[:3]:
job = fetch_detail(session, org, listing)
print(job["title"], "-", job["location"] if job else "no longer posted")- 1Normalise the OrgID to uppercase and validate the I plus eight digits shape before requesting
- 2Preserve olnew/slnew and level/menulist selections as part of the board identity
- 3Verify the page's own var OrgID declaration before mapping any vacancy
- 4Treat the selected board as a complete snapshot — there is no pagination to follow
- 5Accept both the index.php and jobRequest.php?source=XML detail routes
- 6Use the presence of the native OrgID/RequestID form, not the HTTP status, to decide a vacancy is still open
One endpoint. All iRecruit US jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=irecruit us" \
-H "X-Api-Key: YOUR_KEY"Try the API.
Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.
Access iRecruit US
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.