Trakstar Hire Jobs API.
Pull an entire company's open roles — full HTML descriptions, locations, teams and close dates — from a single RSS feed with no API key and no pagination to manage.
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.
What's in every response.
Data fields, real-world applications, and the companies already running on Trakstar Hire.
- Full HTML Job Descriptions
- City, State & Country
- Department & Team
- Employment Type
- Application Close Dates
- Publish Dates & Job IDs
- 01Job Board Aggregation
- 02Company Hiring Trackers
- 03Recruiting Market Research
- 04Talent Sourcing Pipelines
How to scrape Trakstar Hire.
Step-by-step guide to extracting jobs from Trakstar Hire-powered career pages—endpoints, authentication, and working code.
# Board lives at https://{slug}.hire.trakstar.com
company_slug = "mailchimp"
rss_url = f"https://{company_slug}.hire.trakstar.com/jobfeeds/{company_slug}"
print(rss_url)
# -> https://mailchimp.hire.trakstar.com/jobfeeds/mailchimpimport requests
import xml.etree.ElementTree as ET
# The feed binds the "job" prefix to this namespace URI.
JOB_NS = {"job": "https://recruiterbox.com/rss/job/"}
def _text(node):
return node.text.strip() if node is not None and node.text else None
def parse_feed(xml_bytes: bytes) -> list[dict]:
root = ET.fromstring(xml_bytes)
items = []
for item in root.findall(".//item"):
items.append({
"title": _text(item.find("title")),
"link": _text(item.find("link")),
"description": _text(item.find("description")),
"pub_date": _text(item.find("pubDate")),
"guid": _text(item.find("guid")),
"location_city": _text(item.find("job:locationCity", JOB_NS)),
"location_state": _text(item.find("job:locationState", JOB_NS)),
"location_country": _text(item.find("job:locationCountry", JOB_NS)),
"position_type": _text(item.find("job:positionType", JOB_NS)),
"team": _text(item.find("job:team", JOB_NS)),
# NB: the feed misspells this tag as "closeDte", not "closeDate".
"close_date": _text(item.find("job:closeDte", JOB_NS)),
})
return items
resp = requests.get(rss_url, timeout=30)
resp.raise_for_status()
items = parse_feed(resp.content)
print(f"{len(items)} jobs in feed")import re
from urllib.parse import urlsplit, urlunsplit
JOB_ID_RE = re.compile(r"/jobs/([A-Za-z0-9-]+)", re.IGNORECASE)
_ENTITIES = {"<": "<", ">": ">", "&": "&", """: '"',
"'": "'", "'": "'", " ": " "}
def normalize_url(url: str) -> str:
# Force https + lowercase host, strip any trailing slash.
p = urlsplit(url)
return urlunsplit(("https", p.netloc.lower(), p.path, "", "")).rstrip("/")
def extract_job_id(url: str):
m = JOB_ID_RE.search(url or "")
return m.group(1) if m else None
def decode_entities(html: str) -> str:
# Trakstar double-encodes the description, so decode a second pass
# when encoded entities still remain.
if not html:
return ""
def once(s: str) -> str:
for enc, dec in _ENTITIES.items():
s = s.replace(enc, dec)
return s
decoded = once(html)
if any(e in decoded for e in ("<", ">", "&")):
decoded = once(decoded)
return decodeddef to_jobs(items: list[dict]) -> list[dict]:
jobs, seen = [], set()
for item in items:
link = item["link"]
if not link:
continue
listing_url = normalize_url(link)
if listing_url in seen: # drop duplicate links
continue
seen.add(listing_url)
job_id = extract_job_id(link)
if not job_id: # skip items without a /jobs/<id> link
continue
location = ", ".join(
part for part in (
item["location_city"], item["location_state"], item["location_country"]
) if part
)
jobs.append({
"external_id": job_id,
"title": item["title"],
"listing_url": listing_url,
"apply_url": f"{listing_url}/?apply=true",
"description_html": decode_entities(item["description"] or ""),
"location": location or None,
"department": item["team"],
"employment_type": item["position_type"],
"posted_at": item["pub_date"],
"closes_at": item["close_date"],
})
return jobs
jobs = to_jobs(items)
for job in jobs[:3]:
print(job["external_id"], job["title"], "-", job["location"])def fetch_feed_safe(company_slug: str) -> bytes | None:
"""GET the feed and classify the common failure codes before parsing."""
rss_url = f"https://{company_slug}.hire.trakstar.com/jobfeeds/{company_slug}"
try:
resp = requests.get(rss_url, timeout=30)
except requests.RequestException as exc:
print(f"network error: {exc}")
return None
if resp.status_code == 404:
print("slug not found (404) — verify the subdomain")
return None
if resp.status_code in (403, 429):
print("blocked / rate limited — back off and retry later")
return None
resp.raise_for_status()
if b"<rss" not in resp.content.lower():
print("unexpected body — not an RSS feed")
return None
return resp.content
xml_bytes = fetch_feed_safe("mailchimp")
# A live board with no openings still returns valid RSS with zero <item>s —
# that yields an empty list, which is a valid result, not a failure.
jobs = to_jobs(parse_feed(xml_bytes)) if xml_bytes else []
print(f"{len(jobs)} jobs")The feed misspells the close-date element as job:closeDte (not job:closeDate). Read the job: namespaced tag exactly as job:closeDte, or every closes_at value comes back empty.
Trakstar double-encodes the HTML inside <description>. Decode entities once to recover the tags, then decode again if <, > or & still remain, before storing the description.
Item links are returned as http:// with mixed-case hostnames, so naive string comparison misses repeats. Normalize each link to https, lowercase the host and strip trailing slashes before deduping.
A live board with no open roles still serves a well-formed feed with an empty <item> set. Treat an empty result as 'no jobs', not a failure, so monitoring does not false-alarm.
There is no /api/jobs endpoint — those paths return 404. The RSS feed at /jobfeeds/{slug} is the only structured source, and it already returns full descriptions in a single request.
- 1Use the lowercase company slug for both the subdomain and the /jobfeeds/ path segment.
- 2Register the job: namespace (https://recruiterbox.com/rss/job/) and read job:closeDte, matching the feed's spelling.
- 3Decode HTML entities twice — Trakstar double-encodes the description markup.
- 4Normalize each item link to https + lowercase host and strip trailing slashes before deduplicating.
- 5Pace requests around 200ms apart, one at a time, and treat 403/429 as a signal to back off.
- 6One feed request returns every open role with full descriptions, so cache it and refresh on a schedule instead of crawling pages.
One endpoint. All Trakstar Hire jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=trakstar hire" \
-H "X-Api-Key: YOUR_KEY" Access Trakstar Hire
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.