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.

Get API access

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

  1. 01Healthcare & Transit Job Aggregation
  2. 02Regional Employer Monitoring
  3. 03Careers Page Extraction
  4. 04ATS Data Pipelines

Trusted by

  • Greater Bridgeport Transit
  • Newman Regional Health
  • HealthCore Clinic
DIY GUIDE

How to scrape iRecruit US.

Step-by-step guide to extracting jobs from iRecruit US-powered career pages—endpoints, authentication, and working code.

API type
HTML
Difficulty
intermediate
Rate limit
No published limit; ~100ms between requests and at most 3 concurrent detail fetches
Authentication
No auth

Read the OrgID and keep the board selection

Every iRecruit board is www.irecruit-us.com/index.php with an OrgID of the form I followed by eight digits. Some employers publish category or level sub-boards through olnew/slnew or level/menulist — those parameters are part of the board's address, and dropping them can turn a board with dozens of vacancies into an empty category landing page.

Step 1: Read the OrgID and keep the board selection
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)

Fetch the board and prove it belongs to that organization

The selected board is a complete snapshot: there is no pagination control and no anonymous JSON endpoint. Confirm the page really belongs to the OrgID you asked for by looking for the first-party 'var OrgID' declaration it prints, so a redirect onto a different organization cannot be filed as this employer's jobs.

Step 2: Fetch the board and prove it belongs to that organization
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)

Collect the vacancy links

Vacancies are ordinary anchors carrying a RequestID query parameter, and the visible title sits inside a bold element within the anchor. Resolve each link, require the same OrgID and the same selection as the board you fetched, and treat an alphanumeric RequestID of ten characters or more as the job identifier.

Step 3: Collect the vacancy links
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}")

Hydrate the vacancy and detect stale requests

Detail pages come in two shapes: the ordinary index.php route and a jobRequest.php?source=XML variant, and both publish the body plus a native application form. iRecruit answers a closed request with HTTP 200 and the plain organization page, so the real liveness test is whether the page still carries hidden OrgID and RequestID inputs for the job you asked for.

Step 4: Hydrate the vacancy and detect stale requests
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")
Common issues
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.
Best practices
  1. 1Normalise the OrgID to uppercase and validate the I plus eight digits shape before requesting
  2. 2Preserve olnew/slnew and level/menulist selections as part of the board identity
  3. 3Verify the page's own var OrgID declaration before mapping any vacancy
  4. 4Treat the selected board as a complete snapshot — there is no pagination to follow
  5. 5Accept both the index.php and jobRequest.php?source=XML detail routes
  6. 6Use the presence of the native OrgID/RequestID form, not the HTTP status, to decide a vacancy is still open
Or skip the complexity

One endpoint. All iRecruit US jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=irecruit us" \
  -H "X-Api-Key: YOUR_KEY"
Developer tools

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.

Ready to integrate

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.

99.9%API uptime
<200msAvg response
50M+Jobs processed