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.

Get API access

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

  1. 01Hourly & Retail Job Aggregation
  2. 02Public Sector Hiring Trackers
  3. 03Healthcare Recruitment Feeds
  4. 04Careers Page Monitoring

Trusted by

  • Aztec Shops
  • Metra
  • Wayne Memorial Health System
DIY GUIDE

How to scrape Cadient Talent.

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

API type
Hybrid
Difficulty
intermediate
Rate limit
No published limit; ~250ms between requests, max 3 concurrent detail fetches
Authentication
No auth

Extract the application name

Every Cadient board is a query parameter, not a subdomain: cta.cadienttalent.com/index.jsp?applicationName={tenant}. The application name is alphanumeric and usually ends in a suffix like KTMDReqExt. Job pages add a numeric POSTING_ID and a SEQ of jobDetails or positionDetails.

Step 1: Extract the application name
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"))

Fetch the complete RSS inventory

Cadient's postingSearchResultsRss sequence returns every open posting for the tenant in one document. Build the URL with the lowercased application name; the feed has no pagination, so the item count is the whole board.

Step 2: Fetch the complete RSS inventory
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")

Parse the posting page

The feed's description is a short summary. The full body lives on the posting page under #jobDetails, with the title in the #jobdetail heading and the apply target on the apply-now button. Fetch each posting URL straight from the feed.

Step 3: Parse the posting page
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)

Read the labelled requisition fields

Structured fields — job code, department, category, city, state, work location type and posting date — are rendered as labelled rows in the posting page. Each .formRow pairs an element carrying a title attribute with a span.field value, so build a dictionary from those pairs.

Step 4: Read the labelled requisition fields
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"),
    }

Detect removed postings

Cadient publishes an explicit removal state rather than a 404 for many closed jobs. When the page carries both its structured Job Not Found title and the phrase 'no longer available', retire the posting; a generic empty or malformed page is inconclusive and should be retried.

Step 5: Detect removed postings
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)
Common issues
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.
Best practices
  1. 1Lowercase the application name before building the RSS URL
  2. 2Use the RSS feed as the inventory and the posting page as the record
  3. 3Accept both jobDetails and positionDetails as valid detail sequences
  4. 4Read structured fields from the labelled .formRow pairs, not from free text
  5. 5Require the structured Job Not Found state before retiring a posting
  6. 6Throttle to ~250ms between requests with at most three concurrent detail fetches
Or skip the complexity

One endpoint. All Cadient Talent jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=cadient talent" \
  -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 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.

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