- highA shared jobs.jobaline.com link names no employer
- The global jobs.jobaline.com/ApplyForJob shape carries a job id but no tenant. Fetch the page and read the tenant out of its og:url meta tag, which is where Jobaline declares the owning employer board; treat a page with no og:url as unattributable rather than assigning it a guess.
- highThe tenant board returns only the first ten jobs
- Tenant boards are search surfaces, not lists. Request a wide radius and walk the start parameter, driving the loop from the HasMoreResults flag in the response body rather than from the number of rows returned, and stop at a page ceiling so a misbehaving board cannot spin forever.
- mediumDetail pages have no JSON-LD on some tenants
- Only part of the estate publishes JobPosting JSON-LD. Try the structured block first and fall back to the labelled HTML fields .job-title, .customer-name and .job-description-container, which are consistent across the tenants that omit it.
- mediumA search row is attributed to the wrong employer
- Require the row's data-id, the a.search_job_title link's jobid, and the link's own subdomain to agree with the board being crawled. A row that fails any of those checks belongs to a different tenant and should be dropped rather than merged into this employer.
Jobaline / Jobalign Jobs API.
Read hourly hiring from Jobaline (Jobalign) employer boards, where each tenant subdomain is a search surface and every posting resolves to a canonical apply page carrying the full description.
What's in every response.
Data fields, real-world applications, and the companies already running on Jobaline / Jobalign.
Data fields
- Full Job Descriptions
- Brand and Employer Names
- City, State and Postal Code
- Native Numeric Job IDs
- JobPosting JSON-LD On Many Tenants
- Multi-Location Employer Boards
Use cases
- 01Hourly & Shift Work Aggregation
- 02Restaurant and Retail Job Boards
- 03Multi-Site Employer Monitoring
- 04Local Labour Market Research
Trusted by
- Arctic Circle
- Anna's House
- Revlon
How to scrape Jobaline / Jobalign.
Step-by-step guide to extracting jobs from Jobaline / Jobalign-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse, parse_qs
SUFFIX = ".jobaline.com"
GLOBAL_HOST = "jobs.jobaline.com"
def parse_url(url: str) -> tuple[str, str | None] | None:
parsed = urlparse(url)
host = parsed.netloc.lower()
if not host.endswith(SUFFIX):
return None
tenant = host[: -len(SUFFIX)]
if tenant in {"jobs", "www"} or not tenant:
return None
path = parsed.path.rstrip("/")
if not path:
return (tenant, None) # the board itself
if path.lower() != "/applyforjob":
return None
job_id = (parse_qs(parsed.query).get("jobid") or [""])[0]
return (tenant, job_id) if job_id.isdigit() else None
def board_url(tenant: str) -> str:
return f"https://{tenant}{SUFFIX}"
print(parse_url("https://acr.jobaline.com/ApplyForJob?jobid=556078&s=12")) # ('acr', '556078')import re
import time
import requests
from bs4 import BeautifulSoup
HAS_MORE = re.compile(r'"HasMoreResults"\s*:\s*true', re.IGNORECASE)
MAX_PAGES = 500
def fetch_page(session: requests.Session, tenant: str, page: int) -> str:
# A very wide radius makes the tenant's search surface behave as a full board.
url = f"{board_url(tenant)}/?loc=Kansas%20City%2C%20MO&range=3000&start={page}"
response = session.get(
url, headers={"Accept": "text/html,application/xhtml+xml"}, timeout=30
)
response.raise_for_status()
if "Jobalign" not in response.text:
raise RuntimeError("Jobaline board omitted its first-party tenant proof")
return response.text
def crawl(session: requests.Session, tenant: str) -> list[dict]:
listings, seen, page = [], set(), 1
while page <= MAX_PAGES:
body = fetch_page(session, tenant, page)
for row in parse_rows(BeautifulSoup(body, "html.parser"), tenant):
if row["id"] not in seen:
seen.add(row["id"])
listings.append(row)
if not HAS_MORE.search(body):
return listings # the provider says this was the last page
page += 1
time.sleep(0.2)
raise RuntimeError("Jobaline pagination did not terminate within the page ceiling")from urllib.parse import urljoin
def text_of(row, selector: str) -> str | None:
node = row.select_one(selector)
return " ".join(node.get_text().split()) if node else None
def parse_rows(soup: BeautifulSoup, tenant: str) -> list[dict]:
rows = []
for row in soup.select("div.searchItem[data-id]"):
anchor = row.select_one("a.search_job_title[href]")
if not anchor:
continue
parsed = parse_url(urljoin(board_url(tenant), anchor["href"]))
data_id = (row.get("data-id") or "").strip()
title = " ".join(anchor.get_text().split())
# The anchor's tenant and job id must both match the row itself.
if not parsed or parsed[0] != tenant or parsed[1] != data_id or not title:
continue
city = text_of(row, "#DesktopBrand [data-ts='city']")
state = text_of(row, "#DesktopBrand [data-ts='state']")
rows.append({
"id": data_id,
"title": title,
"company": text_of(row, "#DesktopBrand [data-ts='brandname']"),
"location": ", ".join(p for p in [city and city.strip(" ,"), state] if p) or None,
"listing_url": f"{board_url(tenant)}/ApplyForJob?jobid={data_id}",
})
return rows
session = requests.Session()
listings = crawl(session, "acr")
print(f"{len(listings)} vacancies")import json
def canonical_detail_url(job_id: str) -> str:
return f"https://{GLOBAL_HOST}/ApplyForJob?jobid={job_id}&s=34"
def find_job_posting(soup) -> dict | None:
for tag in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "")
except (json.JSONDecodeError, TypeError):
continue
for node in (data if isinstance(data, list) else [data]):
if isinstance(node, dict) and node.get("@type") == "JobPosting":
return node
return None
def fetch_detail(session: requests.Session, listing: dict, tenant: str) -> dict | None:
response = session.get(canonical_detail_url(listing["id"]), timeout=30)
if response.status_code in (404, 410):
return None # canonical removal
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# The page must be a real detail render AND name this tenant and job.
required = (".job-title", ".customer-name", ".job-description-container")
og_url = soup.select_one("meta[property='og:url']")
proved = parse_url((og_url.get("content") if og_url else "") or "")
if any(soup.select_one(s) is None for s in required) or proved != (tenant, listing["id"]):
raise RuntimeError("Jobaline detail page contradicted its native tenant proof")
posting = find_job_posting(soup)
if posting:
return {
**listing,
"title": posting.get("title") or listing["title"],
"description_html": posting.get("description"),
"posted_at": posting.get("datePosted"),
"employment_type": posting.get("employmentType"),
"company": (posting.get("hiringOrganization") or {}).get("name") or listing["company"],
}
# Tenants without JSON-LD still expose the same fields as labelled HTML.
body = soup.select_one(".job-description-container")
return {
**listing,
"title": " ".join(soup.select_one(".job-title").get_text().split()),
"company": " ".join(soup.select_one(".customer-name").get_text().split()),
"description_html": body.decode_contents() if body else None,
"detail_format": "jobaline_html",
}
for listing in listings[:3]:
job = fetch_detail(session, listing, "acr")
if job:
print(job["title"], "-", job["company"])- 1Treat the bare {tenant}.jobaline.com host as the canonical board and exclude jobs and www
- 2Search with a wide radius so the tenant surface behaves as a complete board
- 3Drive pagination from the HasMoreResults flag and cap the page count defensively
- 4Cross-check data-id against the anchor's jobid before accepting a row
- 5Hydrate from the canonical jobs.jobaline.com apply page and verify og:url names the same tenant
- 6Fall back to the labelled HTML fields when a tenant publishes no JobPosting JSON-LD
One endpoint. All Jobaline / Jobalign jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=jobaline / jobalign" \
-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 Jobaline / Jobalign
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.