Pereless ATS OnDemand Jobs API.

Read hiring from restaurants, hotels and care providers on Pereless ATS OnDemand, where each employer's board is one server-rendered table keyed by a durable numeric company id.

Get API access

What's in every response.

Data fields, real-world applications, and the companies already running on Pereless ATS OnDemand.

Data fields

  • Complete Employer Board
  • Full Job Descriptions
  • Job Tracking IDs
  • Category and Keywords Columns
  • Job Location Fields
  • Native Apply Form URLs

Use cases

  1. 01Hospitality & Care Job Aggregation
  2. 02SMB Employer Monitoring
  3. 03Careers Page Extraction
  4. 04ATS Data Pipelines

Trusted by

  • 1606 Restaurant & Bar
  • Hay Creek Hotels
  • Heathwood Assisted Living
  • Boulder Medical Center
DIY GUIDE

How to scrape Pereless ATS OnDemand.

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

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

Key the employer on the numeric cid, not the hostname

Pereless boards are served from {name}.atsondemand.com and {name}.submit4jobs.com, and the same employer can appear on more than one host. The durable identity is the numeric cid query parameter, which stays constant across those aliases; a job adds a JID and a fuseaction of {cid}.viewjobdetail.

Step 1: Key the employer on the numeric cid, not the hostname
from urllib.parse import urlparse, parse_qs

SUFFIXES = (".atsondemand.com", ".submit4jobs.com")

def parse_url(url: str) -> tuple[str, str, str | None] | None:
    parsed = urlparse(url)
    host = parsed.netloc.lower()
    if not host.endswith(SUFFIXES) or parsed.path.lower() != "/index.cfm":
        return None
    query = {k.lower(): v[0] for k, v in parse_qs(parsed.query).items()}

    cid = query.get("cid", "")
    if not cid.isdigit():
        return None
    job_id = query.get("jid")
    action = query.get("fuseaction")
    if job_id is not None:
        if not job_id.isdigit() or action != f"{cid}.viewjobdetail":
            return None
    elif action is not None and action != f"{cid}.viewjobs":
        return None
    return host, cid, job_id

def board_url(host: str, cid: str) -> str:
    return f"https://{host}/index.cfm?cid={cid}"

print(parse_url(
    "https://1606restaurantbar.atsondemand.com/index.cfm"
    "?cid=512881&fuseaction=512881.viewjobdetail&JID=913337"
))

Bootstrap the board page before requesting the frame

The jobs table is served inside a frame that only answers after the outer board page has set its same-origin cookies. Fetch /index.cfm?cid={cid} first with a session, and confirm it is really that company's board — either the legacy iframe plus a companyimage/{cid}/ logo, or a branded theme whose assets live under /{cid}/website/images/.

Step 2: Bootstrap the board page before requesting the frame
import requests
from bs4 import BeautifulSoup

def has_company_proof(html: str, cid: str) -> bool:
    soup = BeautifulSoup(html, "html.parser")
    iframe = soup.select_one("iframe#myiframe[src]")
    legacy = (
        iframe is not None
        and f"cid={cid}" in (iframe.get("src") or "")
        and soup.select_one(f"img[src*='companyimage/{cid}/']") is not None
        and "iframeResizer" in html
    )
    branded = (
        soup.select_one(f"img[src^='/{cid}/website/images/']") is not None
        and any(f"cid={cid}" in (a.get("href") or "") for a in soup.select("a[href]"))
    )
    return legacy or branded

def bootstrap(session: requests.Session, host: str, cid: str) -> None:
    response = session.get(
        board_url(host, cid),
        headers={"Accept": "text/html,application/xhtml+xml"},
        timeout=30,
    )
    response.raise_for_status()
    if not has_company_proof(response.text, cid):
        raise RuntimeError("Pereless bootstrap page omitted its company proof")

session = requests.Session()
bootstrap(session, "1606restaurantbar.atsondemand.com", "512881")

Read the complete jobs table from the frame

With the cookie in place, request the listings frame. The whole board arrives in table#psjobstable — there is no pagination — and each row's second cell holds the a.joblink anchor, with category, location and keywords in the neighbouring cells. Rebuild the canonical detail URL rather than storing the frame link.

Step 3: Read the complete jobs table from the frame
def listings_frame_url(host: str, cid: str) -> str:
    return f"https://{host}/index.cfm?frame=1&cid={cid}&fuseaction={cid}.viewjobs&mybuid="

def job_url(host: str, cid: str, job_id: str) -> str:
    return f"https://{host}/index.cfm?cid={cid}&fuseaction={cid}.viewjobdetail&JID={job_id}"

def fetch_listings(session: requests.Session, host: str, cid: str) -> list[dict]:
    response = session.get(listings_frame_url(host, cid), timeout=30)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")
    if soup.select_one("table#psjobstable") is None:
        raise RuntimeError("Pereless listing frame omitted its jobs table")

    listings = []
    for row in soup.select("#psjobstable tbody tr"):
        cells = row.find_all("td", recursive=False)
        anchor = cells[1].select_one("a.joblink[href]") if len(cells) > 1 else None
        parsed = parse_url(requests.compat.urljoin(board_url(host, cid), anchor["href"])) if anchor else None
        title = " ".join(anchor.get_text().split()) if anchor else ""
        if not parsed or parsed[1] != cid or not parsed[2] or not title:
            continue
        listings.append({
            "id": parsed[2],
            "title": title,
            "category": " ".join(cells[2].get_text().split()) if len(cells) > 2 else None,
            "location": " ".join(cells[3].get_text().split()).replace(" / ", ", ") if len(cells) > 3 else None,
            "keywords": " ".join(cells[4].get_text().split()) if len(cells) > 4 else None,
            "listing_url": job_url(host, cid, parsed[2]),
        })
    return listings

listings = fetch_listings(session, "1606restaurantbar.atsondemand.com", "512881")
print(f"{len(listings)} vacancies")

Verify the Job Tracking ID before trusting a detail page

The detail frame prints a labelled Job Tracking ID of the form {cid}-{jid}, and that single value proves both the company and the job. Legacy themes also carry a hidden jobid input which must agree when present, while newer branded themes legitimately omit it — so the tracking id, not the hidden field, is the check to rely on.

Step 4: Verify the Job Tracking ID before trusting a detail page
def detail_frame_url(host: str, cid: str, job_id: str) -> str:
    return f"https://{host}/index.cfm?frame=1&cid={cid}&fuseaction={cid}.viewjobdetail&JID={job_id}"

def labelled_fields(soup) -> dict:
    fields = {}
    for item in soup.select("li"):
        # Only a DIRECT <strong> child is a label; a wrapping <li> would otherwise
        # swallow every nested field into one value.
        label = next((c for c in item.find_all("strong", recursive=False)), None)
        if not label:
            continue
        key = " ".join(label.get_text().split()).rstrip(":")
        text = item.get_text()
        value = text[text.find(":") + 1:].strip().lstrip("\u00a0")
        if key and value:
            fields.setdefault(key, value)
    return fields

def fetch_detail(session: requests.Session, host: str, cid: str, listing: dict) -> dict | None:
    response = session.get(detail_frame_url(host, cid, listing["id"]), timeout=30)
    if response.status_code in (404, 410):
        return None  # canonical removal
    response.raise_for_status()
    html = response.text

    # Pereless renders its own explicit closed-job document.
    if "We can't find the Job you are looking for" in html and "status might have changed or closed" in html:
        return None

    soup = BeautifulSoup(html, "html.parser")
    fields = labelled_fields(soup)
    hidden = soup.select_one("input[name='jobid']")
    if fields.get("Job Tracking ID") != f"{cid}-{listing['id']}":
        raise RuntimeError("Pereless detail frame contradicted its company/job proof")
    if hidden is not None and (hidden.get("value") or "").strip() != listing["id"]:
        raise RuntimeError("Pereless hidden job id disagreed with the requested job")

    heading = soup.select_one(".responsiveJobHeader h1")
    section = next((h for h in soup.select("h2")
                    if h.get_text().strip().rstrip(":").lower() == "job description"), None)
    container = section.parent if section else None
    if section:
        section.extract()
    apply_form = soup.select_one("form[name='applyonline']")

    return {
        **listing,
        "title": " ".join(heading.get_text().split()) if heading else listing["title"],
        "description_html": container.decode_contents().strip() if container else None,
        "location": fields.get("Job Location") or listing["location"],
        "posted_at": fields.get("Starting Date") or fields.get("Date Updated"),
        "apply_url": requests.compat.urljoin(
            job_url(host, cid, listing["id"]), apply_form.get("action")
        ) if apply_form and apply_form.get("action") else job_url(host, cid, listing["id"]),
    }

for listing in listings[:3]:
    job = fetch_detail(session, "1606restaurantbar.atsondemand.com", "512881", listing)
    print(job["title"] if job else f"{listing['id']} is closed")
Common issues
criticalThe listings frame returns nothing on its own
The frame only serves the jobs table once the outer board page has set its same-origin cookies. Request /index.cfm?cid={cid} first with a persistent session, then the frame URL; skipping the bootstrap yields an empty document that looks like a board with no jobs.
highOne employer is split across two hostnames
The same company can be published on both an atsondemand.com and a submit4jobs.com host, and public and internal boards may differ only by hostname. Key identity on the numeric cid, which collapses those aliases onto a single employer.
highA closed job still returns HTTP 200
Pereless renders its own explicit closed-job document rather than a 404. Detect that page and record it as the provider saying the role has ended, reserving 404 and 410 on the canonical frame for genuine HTTP removal.
mediumThe Job Tracking ID reads as every field concatenated
Some themes wrap the metadata list in an outer li element, so a naive search for the first strong tag inside each li returns the whole block. Only accept a strong element that is a direct child of the li you are reading.
Best practices
  1. 1Use the numeric cid as the employer identity, never the hostname
  2. 2Bootstrap the outer board page in a persistent session before requesting any frame
  3. 3Confirm the board's own company proof — the cid-scoped logo or asset root — before mapping rows
  4. 4Treat table#psjobstable as the complete board; there is no pagination to follow
  5. 5Validate the labelled Job Tracking ID against {cid}-{jid} on every detail page
  6. 6Record the applyonline form action as the apply URL when the theme provides one
Or skip the complexity

One endpoint. All Pereless ATS OnDemand jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=pereless ats ondemand" \
  -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 Pereless ATS OnDemand
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