- 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.
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.
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
- 01Hospitality & Care Job Aggregation
- 02SMB Employer Monitoring
- 03Careers Page Extraction
- 04ATS Data Pipelines
Trusted by
- 1606 Restaurant & Bar
- Hay Creek Hotels
- Heathwood Assisted Living
- Boulder Medical Center
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.
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"
))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")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")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")- 1Use the numeric cid as the employer identity, never the hostname
- 2Bootstrap the outer board page in a persistent session before requesting any frame
- 3Confirm the board's own company proof — the cid-scoped logo or asset root — before mapping rows
- 4Treat table#psjobstable as the complete board; there is no pagination to follow
- 5Validate the labelled Job Tracking ID against {cid}-{jid} on every detail page
- 6Record the applyonline form action as the apply URL when the theme provides one
One endpoint. All Pereless ATS OnDemand jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=pereless ats ondemand" \
-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 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.