- criticalThe search endpoints return 403 or an empty payload
- MatchedJobs and ProcessSortAndShowMoreJobs both require session state from the HomeWithPreLoad bootstrap: the session cookies, the __RequestVerificationToken echoed in an RFT header, and the encrypted CookieValue in the request body. Skipping the bootstrap fails every time, and the token expires with the session.
- highPagination stops early or loops forever
- Page size is a per-tenant setting, so assuming a fixed count of rows per page either truncates a large board or overruns a small one. Track the number of rows you have actually consumed against JobsCount, and stop as soon as a page returns no rows.
- highA job returns Jobdetails: null
- That is BrassRing publishing a matching requisition ID with no record behind it, which means the posting has been withdrawn. Record it as removed rather than retrying it as a parse failure, otherwise closed requisitions stay live in your index indefinitely.
- mediumCustomer-owned domains are not recognised as BrassRing
- Large customers run the identical TGnewUI application on hosts such as jobs.ubs.com or applybsd.org, so a hostname allow-list limited to brassring.com misses them entirely. Match on the /TGnewUI/Search/Home path plus a numeric partnerid and siteid, and verify the returned bootstrap echoes both IDs before trusting an unfamiliar host.
IBM Kenexa BrassRing Jobs API.
BrassRing is IBM Kenexa's enterprise ATS, still running large retail, media and banking careers sites. Each board is a partner/site pair whose search endpoints return an authoritative requisition count and page-numbered results.
What's in every response.
Data fields, real-world applications, and the companies already running on IBM Kenexa BrassRing.
Data fields
- Full Job Descriptions
- Authoritative Requisition Counts
- Native Requisition IDs
- Department & Category Fields
- Multi-Location Requisitions
- White-Label Customer Domains
Use cases
- 01Enterprise Job Aggregation
- 02Retail & Media Hiring Trackers
- 03Talent Market Research
- 04Careers Page Monitoring
Trusted by
- AAFES
- Disney Worldwide Services
- Archer Daniels Midland
- UBS
How to scrape IBM Kenexa BrassRing.
Step-by-step guide to extracting jobs from IBM Kenexa BrassRing-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse, parse_qs
VENDOR_HOSTS = {
"sjobs.brassring.com",
"krb-sjobs.brassring.com",
"xjobs.brassring.com",
"krb-xjobs.brassring.com",
}
# Audited customer-owned deployments of the same application.
CUSTOMER_HOSTS = {
"jobs.ubs.com",
"jobs.aoshearman.com",
"jobs.peerpoint.com",
"applybsd.org",
"carrieres.hema-quebec.qc.ca",
}
def parse_board(url: str) -> dict | None:
parsed = urlparse(url)
host = parsed.netloc.lower()
if host not in VENDOR_HOSTS and host not in CUSTOMER_HOSTS:
return None
if parsed.path.lower() not in ("/tgnewui/search/home/home",
"/tgnewui/search/home/homewithpreload"):
return None
query = parse_qs(parsed.query)
partner = (query.get("partnerid") or [None])[0]
site = (query.get("siteid") or [None])[0]
if not (partner and site and partner.isdigit() and site.isdigit()):
return None
return {
"host": host,
"partner_id": partner,
"site_id": site,
"job_id": (query.get("jobid") or [None])[0],
}
print(parse_board(
"https://sjobs.brassring.com/TGnewUI/Search/Home/Home?partnerid=25212&siteid=5163"))import requests
from bs4 import BeautifulSoup
def start_session(board: dict) -> tuple[requests.Session, dict]:
session = requests.Session()
session.headers.update({
"Accept": "text/html,application/xhtml+xml",
"User-Agent": "Mozilla/5.0 (compatible; example-jobs-crawler/1.0)",
})
host, partner, site = board["host"], board["partner_id"], board["site_id"]
url = (f"https://{host}/TGnewUI/Search/Home/HomeWithPreLoad"
f"?partnerid={partner}&siteid={site}&PageType=SearchResults")
resp = session.get(url, timeout=30)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
def value(selector):
node = soup.select_one(selector)
return (node.get("value") or "").strip() if node else ""
# The bootstrap must prove it is the board we asked for.
if value("input#partnerId") != partner or value("input#siteId") != site:
raise RuntimeError("BrassRing bootstrap did not match the requested board")
state = {
"token": value("input[name='__RequestVerificationToken']"),
"encrypted_session": value("input#CookieValue"),
}
if not state["token"] or not state["encrypted_session"]:
raise RuntimeError("BrassRing bootstrap omitted its request token")
return session, stateimport time
def search_headers(board: dict, state: dict) -> dict:
host, partner, site = board["host"], board["partner_id"], board["site_id"]
return {
"Accept": "application/json,text/plain,*/*",
"Content-Type": "application/json; charset=UTF-8",
"Origin": f"https://{host}",
"Referer": (f"https://{host}/TGnewUI/Search/Home/Home"
f"?partnerid={partner}&siteid={site}"),
"RFT": state["token"],
"X-Requested-With": "XMLHttpRequest",
}
def fetch_all_rows(session, board: dict, state: dict) -> list[dict]:
host = board["host"]
headers = search_headers(board, state)
rows, consumed, page = [], 0, 1
while True:
if page == 1:
endpoint = "/TgNewUI/Search/Ajax/MatchedJobs"
body = {"partnerId": board["partner_id"], "siteId": board["site_id"],
"encryptedSessionValue": state["encrypted_session"]}
else:
endpoint = "/TgNewUI/Search/Ajax/ProcessSortAndShowMoreJobs"
body = {"partnerId": board["partner_id"], "siteId": board["site_id"],
"SortType": "LastUpdated", "pageNumber": page,
"encryptedSessionValue": state["encrypted_session"]}
resp = session.post(f"https://{host}{endpoint}", json=body,
headers=headers, timeout=30)
resp.raise_for_status()
payload = resp.json()
total = payload.get("JobsCount")
batch = ((payload.get("Jobs") or {}).get("Job")) or []
rows.extend(batch)
consumed += len(batch)
if total is None:
raise RuntimeError("BrassRing search response omitted JobsCount")
if consumed >= total or not batch:
return rows
page += 1
time.sleep(0.15)def read_questions(questions: list[dict], value_key: str) -> dict:
fields = {}
for question in questions or []:
value = (question.get(value_key) or "").strip()
if not value:
continue
for key in (question.get("QuestionName"), question.get("VerityZone")):
if key:
fields.setdefault(key.strip().lower(), value)
return fields
def map_row(row: dict, board: dict) -> dict | None:
fields = read_questions(row.get("Questions"), "Value")
job_id = fields.get("reqid")
title = fields.get("jobtitle")
# Reject rows that belong to a different customer or site.
if fields.get("clientid") != board["partner_id"]:
return None
if fields.get("siteid") != board["site_id"] or not job_id or not title:
return None
host, partner, site = board["host"], board["partner_id"], board["site_id"]
return {
"id": job_id,
"title": title,
"description": fields.get("jobdescription"),
"url": (f"https://{host}/TGnewUI/Search/Home/HomeWithPreLoad"
f"?partnerid={partner}&siteid={site}"
f"&PageType=JobDetails&jobid={job_id}"),
}
listings = [map_row(r, board) for r in rows]
listings = [row for row in listings if row]import json
def fetch_detail(session, board: dict, job_id: str) -> dict | str | None:
host, partner, site = board["host"], board["partner_id"], board["site_id"]
url = (f"https://{host}/TGnewUI/Search/Home/HomeWithPreLoad"
f"?partnerid={partner}&siteid={site}&PageType=JobDetails&jobid={job_id}")
resp = session.get(url, headers={"Accept": "text/html"}, timeout=30)
if resp.status_code in (404, 410):
return "removed"
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
node = soup.select_one("input#preLoadJSON")
if not node or not node.get("value"):
raise RuntimeError("BrassRing detail omitted its preLoadJSON state")
payload = json.loads(node["value"])
details = payload.get("Jobdetails")
if details is None:
return "removed" # structured unavailability, not a failure
if details.get("JobSiteId") != site:
raise RuntimeError("BrassRing Jobdetails belonged to a different site")
fields = read_questions(details.get("JobDetailQuestions"), "AnswerValue")
return {
"id": fields.get("reqid"),
"title": details.get("Title") or fields.get("jobtitle"),
"description": fields.get("jobdescription"),
"city": fields.get("city"),
"state": fields.get("state"),
"country": fields.get("country"),
"url": url,
}- 1Bootstrap HomeWithPreLoad once per board and reuse the session for every search call
- 2Send the anti-forgery token in the RFT header and the encrypted session value in the body
- 3Drive pagination off JobsCount and consumed rows, never off an assumed page size
- 4Flatten the Questions array once, then read reqid, jobtitle and jobdescription by key
- 5Treat Jobdetails: null as a withdrawn requisition, not a transient failure
- 6Verify the bootstrap echoes the partnerId and siteId before scraping a customer-owned host
One endpoint. All IBM Kenexa BrassRing jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=ibm kenexa brassring" \
-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 IBM Kenexa BrassRing
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.