All platforms

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.

Get API access
Trakstar Hire
Live
20K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Trakstar Hire
MailchimpTerraPowerTrading Economics
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.

What's in every response.

Data fields, real-world applications, and the companies already running on Trakstar Hire.

Data fields
  • Full HTML Job Descriptions
  • City, State & Country
  • Department & Team
  • Employment Type
  • Application Close Dates
  • Publish Dates & Job IDs
Use cases
  1. 01Job Board Aggregation
  2. 02Company Hiring Trackers
  3. 03Recruiting Market Research
  4. 04Talent Sourcing Pipelines
Trusted by
MailchimpTerraPowerTrading Economics
DIY GUIDE

How to scrape Trakstar Hire.

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

HTMLbeginnerNo published limit; space requests ~200ms apart, one at a timeNo auth

Build the RSS feed URL

Trakstar Hire publishes one RSS job feed per company board. The company slug is the subdomain, and the same lowercase slug is reused in the feed path — so there is no capitalization or lookup to guess.

Step 1: Build the RSS feed URL
# 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/mailchimp

Fetch and parse the RSS feed

One GET returns every open role with full descriptions. Parse the XML and read the job: namespaced elements for structured metadata. Note the feed misspells the close-date tag as job:closeDte.

Step 2: Fetch and parse the RSS feed
import 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")

Normalize links and decode descriptions

Feed links come back as http:// with mixed-case hosts, and description markup is double-encoded. Normalize each URL, pull the job ID from the /jobs/<id> path, and decode HTML entities twice.

Step 3: Normalize links and decode descriptions
import re
from urllib.parse import urlsplit, urlunsplit

JOB_ID_RE = re.compile(r"/jobs/([A-Za-z0-9-]+)", re.IGNORECASE)

_ENTITIES = {"&lt;": "<", "&gt;": ">", "&amp;": "&", "&quot;": '"',
             "&#39;": "'", "&apos;": "'", "&nbsp;": " "}

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 ("&lt;", "&gt;", "&amp;")):
        decoded = once(decoded)
    return decoded

Structure the final job records

Map each item to a clean record: dedupe on the normalized link, keep only items with a real /jobs/<id> URL, join the location parts, and build the apply URL by appending /?apply=true.

Step 4: Structure the final job records
def 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"])

Classify errors and empty feeds

Handle the failure codes the way a production scraper does: 404 means the slug is wrong, 403/429 mean you are being blocked, and a live board with no openings still returns valid RSS with zero items.

Step 5: Classify errors and empty feeds
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")
Common issues
mediumClose dates never populate

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.

mediumDescriptions render as escaped &lt;p&gt; markup

Trakstar double-encodes the HTML inside <description>. Decode entities once to recover the tags, then decode again if &lt;, &gt; or &amp; still remain, before storing the description.

mediumDuplicate jobs slip past deduplication

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.

lowFeed returns valid RSS but zero jobs

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.

highNo public JSON API for job data

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.

Best practices
  1. 1Use the lowercase company slug for both the subdomain and the /jobfeeds/ path segment.
  2. 2Register the job: namespace (https://recruiterbox.com/rss/job/) and read job:closeDte, matching the feed's spelling.
  3. 3Decode HTML entities twice — Trakstar double-encodes the description markup.
  4. 4Normalize each item link to https + lowercase host and strip trailing slashes before deduplicating.
  5. 5Pace requests around 200ms apart, one at a time, and treat 403/429 as a signal to back off.
  6. 6One feed request returns every open role with full descriptions, so cache it and refresh on a schedule instead of crawling pages.
Or skip the complexity

One endpoint. All Trakstar Hire jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=trakstar hire" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

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.

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