- highThe application name must be lowercased for the feed
- Cadient URLs in the wild preserve mixed case, for example AztecShopsLtdKTMDReqExt, but the RSS sequence is addressed with the lowercase form. Lowercase before building the feed URL and before comparing a feed item's application name against the tenant you requested.
- highThe RSS description is only a summary
- Feed items carry a short teaser, not the posting body. Anything that stores the feed description as the job description ends up with truncated content across the whole board. Fetch each posting page and read the #jobDetails block for the real text.
- mediumDetail URLs use two different SEQ values
- Both SEQ=jobDetails and SEQ=positionDetails are live detail shapes and appear in feeds and inbound links alike. Accept either when parsing a URL, and key the record on the numeric POSTING_ID rather than on the sequence name.
- mediumClosed postings return HTTP 200
- A withdrawn posting frequently renders a normal page carrying Cadient's structured Job Not Found state instead of returning 404. Require both that marker and the 'no longer available' phrase before retiring a record, and treat any other empty page as inconclusive.
Cadient Talent Jobs API.
Cadient Talent is a high-volume hourly hiring platform used by retailers, transit agencies and health systems. Every tenant publishes a complete RSS inventory, and each posting page carries labelled requisition fields.
What's in every response.
Data fields, real-world applications, and the companies already running on Cadient Talent.
Data fields
- Full Job Descriptions
- Complete RSS Inventory
- Job Code & Department
- City & State Fields
- Work Location Type
- Posting Dates
Use cases
- 01Hourly & Retail Job Aggregation
- 02Public Sector Hiring Trackers
- 03Healthcare Recruitment Feeds
- 04Careers Page Monitoring
Trusted by
- Aztec Shops
- Metra
- Wayne Memorial Health System
How to scrape Cadient Talent.
Step-by-step guide to extracting jobs from Cadient Talent-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse, parse_qs
HOST = "cta.cadienttalent.com"
DETAIL_SEQUENCES = {"jobdetails", "positiondetails"}
def parse_cadient(url: str) -> dict | None:
parsed = urlparse(url)
if parsed.scheme != "https" or parsed.netloc.lower() != HOST:
return None
if parsed.path.lower() != "/index.jsp":
return None
query = {k.lower(): v[0] for k, v in parse_qs(parsed.query).items()}
application = (query.get("applicationname") or "").strip()
posting_id = (query.get("posting_id") or "").strip()
sequence = (query.get("seq") or "").strip().lower()
if not application.isalnum() or not 3 <= len(application) <= 120:
return None
if sequence in DETAIL_SEQUENCES and not posting_id.isdigit():
return None
return {
"application_name": application.lower(),
"posting_id": posting_id if sequence in DETAIL_SEQUENCES else None,
}
print(parse_cadient("https://cta.cadienttalent.com/index.jsp"
"?POSTING_ID=107287505627&SEQ=jobDetails"
"&applicationName=aztecshopsltdktmdreqext"))import requests
from xml.etree import ElementTree
def feed_url(application_name: str) -> str:
return (f"https://{HOST}/index.jsp"
f"?applicationName={application_name.lower()}"
"&locale=en_US"
"&seq=postingSearchResultsRss"
"&event=com.deploy.application.ca.plugin.PostingSearch.doSearch")
def fetch_listings(session, application_name: str) -> list[dict]:
resp = session.get(feed_url(application_name),
headers={"Accept": "application/rss+xml"}, timeout=30)
resp.raise_for_status()
channel = ElementTree.fromstring(resp.content).find("channel")
if channel is None:
raise RuntimeError("Cadient response contained no RSS channel")
listings = []
for item in channel.findall("item"):
link = (item.findtext("link") or item.findtext("guid") or "").strip()
identity = parse_cadient(link)
title = (item.findtext("title") or "").strip()
if not identity or not identity["posting_id"] or not title:
continue
if identity["application_name"] != application_name.lower():
continue
listings.append({
"id": identity["posting_id"],
"title": title,
"summary": (item.findtext("description") or "").strip(),
"posted_at": item.findtext("pubDate"),
"url": link,
})
return listings
session = requests.Session()
listings = fetch_listings(session, "AztecShopsLtdKTMDReqExt")
print(f"{len(listings)} open postings")from bs4 import BeautifulSoup
from urllib.parse import urljoin
import time
def fetch_detail(session, listing: dict) -> dict | None:
resp = session.get(listing["url"], headers={"Accept": "text/html"}, timeout=30)
if resp.status_code in (404, 410):
return None # posting removed
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
body = soup.select_one("#jobDetails")
heading = soup.select_one("#jobdetail h1") or soup.select_one("h1")
if not body or not heading:
return None
apply_anchor = soup.select_one("#jobdetail a.apply-now-btn[href]")
apply_url = (urljoin(listing["url"], apply_anchor["href"])
if apply_anchor else listing["url"])
return {
"id": listing["id"],
"title": heading.get_text(strip=True),
"description_html": body.decode_contents(),
"url": listing["url"],
"apply_url": apply_url,
}
for listing in listings[:3]:
print(fetch_detail(session, listing))
time.sleep(0.25)def read_fields(soup: BeautifulSoup) -> dict:
fields = {}
for row in soup.select("#jobdetail .formRow"):
label_node = row.select_one("[title]")
value_node = row.select_one("span.field")
if not label_node or not value_node:
continue
label = (label_node.get("title") or "").strip()
value = value_node.get_text(strip=True)
if not label or not value:
continue
fields.setdefault(label.lower().replace(" ", "_"), value)
return fields
def enrich(session, listing: dict) -> dict | None:
resp = session.get(listing["url"], headers={"Accept": "text/html"}, timeout=30)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
fields = read_fields(soup)
location = ", ".join(v for v in (fields.get("city"), fields.get("state")) if v)
return {
"id": listing["id"],
"job_code": fields.get("job_code"),
"department": fields.get("department"),
"category": fields.get("category"),
"work_location": fields.get("work_location"),
"location": location or None,
"posted_at": fields.get("date_posted") or listing.get("posted_at"),
}def classify(html_text: str) -> str:
lowered = html_text.lower()
if '"title":"job not found"' in lowered and "no longer available" in lowered:
return "removed"
if "id=\"jobdetails\"" in lowered:
return "active"
return "inconclusive"
def check_posting(session, url: str) -> str:
resp = session.get(url, headers={"Accept": "text/html"}, timeout=30)
if resp.status_code in (404, 410):
return "removed"
if not resp.ok:
return "inconclusive"
return classify(resp.text)- 1Lowercase the application name before building the RSS URL
- 2Use the RSS feed as the inventory and the posting page as the record
- 3Accept both jobDetails and positionDetails as valid detail sequences
- 4Read structured fields from the labelled .formRow pairs, not from free text
- 5Require the structured Job Not Found state before retiring a posting
- 6Throttle to ~250ms between requests with at most three concurrent detail fetches
One endpoint. All Cadient Talent jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=cadient talent" \
-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 Cadient Talent
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.