All platforms

TriNet Hire Jobs API.

Pull structured job listings from SMB and mid-market boards hosted on app.trinethire.com, where each company's roles render as a predictable HTML table you can parse without an API key.

Get API access
TriNet Hire
Live
<3haverage discovery time
1hrefresh interval
Companies using TriNet Hire
ARK Investment ManagementBrooklyn BreweryZymo Research
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 TriNet Hire.

Data fields
  • Job Titles & IDs
  • Full HTML Descriptions
  • Location Per Role
  • Employment Type & Category
  • Minimum Experience
  • Filled-Status Flag
Use cases
  1. 01SMB & Mid-Market Job Aggregation
  2. 02PEO-Hosted Board Monitoring
  3. 03Company Careers-Page Extraction
  4. 04Role & Category Analytics
Trusted by
ARK Investment ManagementBrooklyn BreweryZymo Research
DIY GUIDE

How to scrape TriNet Hire.

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

HTMLintermediateNo published limit; keep detail concurrency ~3 with ~300ms spacingNo auth

Build the company listings URL

Every TriNet Hire board lives under a company ID that combines a numeric prefix and a slug (e.g. 20533-all-weather-insulated-panels). Listings are served at /companies/{companyId}/jobs — the bare /companies/{companyId} path returns 404, so always append /jobs.

Step 1: Build the company listings URL
import requests

BASE_URL = "https://app.trinethire.com"

def listings_url(company_id: str) -> str:
    # company_id looks like "20533-all-weather-insulated-panels".
    # /companies/{id} without /jobs returns 404 — always append /jobs.
    return f"{BASE_URL}/companies/{company_id}/jobs"

url = listings_url("20533-all-weather-insulated-panels")
print(url)

Parse the server-rendered jobs table

There is no public JSON API, so the listings page is plain HTML. Each open role is a <tr class="job"> row containing a link to the detail page, a location cell, and a type cell. Extract the job ID from the /jobs/{jobId} path segment.

Step 2: Parse the server-rendered jobs table
import re
from urllib.parse import urljoin
from bs4 import BeautifulSoup

JOB_ID_RE = re.compile(r"/jobs/([^/?#]+)", re.IGNORECASE)

def _text(el):
    return " ".join(el.get_text().split()) if el else None

def parse_listings(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for row in soup.select("tr.job"):
        anchor = row.select_one("a[href*='/jobs/']")
        href = anchor.get("href") if anchor else None
        if not href:
            continue
        match = JOB_ID_RE.search(href)
        if not match:
            continue
        rows.append({
            "external_id": match.group(1),  # e.g. "116069-chemical-engineer"
            "title": _text(anchor),
            "url": urljoin(BASE_URL, href),
            "location": _text(row.select_one("td.location")),
            "employment_type": _text(row.select_one("td.type")),
        })
    return rows

Fetch and parse each detail page

Descriptions and structured meta live only on the per-job page. Pull the title from h3.job-name, the body from div.job-descr.content (with article fallbacks), and read location, category, type, and minimum experience from the ul.job-meta list. A body containing 'This position has been filled' marks a filled role.

Step 3: Fetch and parse each detail page
def parse_detail(html: str) -> dict:
    soup = BeautifulSoup(html, "html.parser")

    title = _text(soup.select_one("h3.job-name"))

    body = (soup.select_one("div.job-descr.content")
            or soup.select_one("article div.job-descr")
            or soup.select_one("article"))
    description_html = body.decode_contents().strip() if body else ""

    location = category = employment_type = min_experience = None
    for li in soup.select("ul.job-meta > li"):
        if li.select_one("span.map-pin-icon"):
            location = _text(li)
            continue
        text = _text(li) or ""
        if ":" not in text:
            continue
        label, _, value = text.partition(":")
        label, value = label.strip().lower(), value.strip()
        if not value:
            continue
        if label == "category":
            category = value
        elif label == "type":
            employment_type = value
        elif label in ("min. experience", "min experience"):
            min_experience = value

    is_filled = "this position has been filled" in soup.get_text().lower()

    return {
        "title": title,
        "location": location,
        "description_html": description_html,
        "category": category,
        "employment_type": employment_type,
        "min_experience": min_experience,
        "is_filled": is_filled,
    }

Scrape a whole board politely

TriNet Hire boards are not paginated, so a single listings fetch returns every role. Space detail requests ~300ms apart and handle the HTTP codes the board returns: 404 (unknown company or removed role), and 401/403/429 (auth or throttling).

Step 4: Scrape a whole board politely
import time

def scrape_company(company_id: str) -> list[dict]:
    session = requests.Session()
    session.headers["User-Agent"] = "Mozilla/5.0 (compatible; JoboBot/1.0)"

    resp = session.get(listings_url(company_id), timeout=30)
    if resp.status_code == 404:
        raise LookupError(f"Company '{company_id}' not found (did you append /jobs?)")
    if resp.status_code in (401, 403, 429):
        raise RuntimeError(f"Blocked or rate limited (HTTP {resp.status_code})")
    resp.raise_for_status()

    jobs = parse_listings(resp.text)  # single page — boards are not paginated

    detailed = []
    for job in jobs:
        detail = session.get(job["url"], timeout=30)
        if detail.status_code == 404:
            continue  # role pulled since the listing was read — skip it
        detail.raise_for_status()
        detailed.append({**job, **parse_detail(detail.text)})
        time.sleep(0.3)  # DelayBetweenRequestsMs = 300 from the scraper runtime config

    return detailed

roles = scrape_company("20533-all-weather-insulated-panels")
print(f"Scraped {len(roles)} roles")
Common issues
highThe company root path returns HTTP 404

Requesting /companies/{companyId} on its own always 404s — listings only exist at /companies/{companyId}/jobs. Append /jobs to every board URL before fetching.

mediumNo usable JSON API despite a /jobs.json reference

The page's JavaScript bundle references /jobs.json?status=open, but public requests return HTTP 401 (or HTML), so there is no structured endpoint. Scrape the server-rendered tr.job rows instead of chasing the JSON path.

mediumFilled roles still appear on the board

Positions that have been filled remain in the listings table and detail pages. Detect the phrase 'This position has been filled' in the detail body and flag or drop those roles instead of treating them as open.

mediumHTTP 403 or 429 on aggressive fetching

Bursting detail requests can return 403 (blocked) or 429 (rate limited). Keep detail concurrency at about 3, space requests ~300ms apart, and back off on 429 responses.

Best practices
  1. 1Always request /companies/{id}/jobs — the bare /companies/{id} path 404s
  2. 2Preserve the full slug IDs (e.g. 116069-chemical-engineer); the numeric prefix alone won't resolve
  3. 3Treat listings as a single page — TriNet Hire boards aren't paginated, so skip cursor logic
  4. 4Cap detail concurrency at ~3 and space requests ~300ms apart to avoid 403/429
  5. 5Flag roles whose body reads 'This position has been filled' instead of counting them as open
  6. 6Resolve relative /jobs/ hrefs against https://app.trinethire.com before fetching details
Or skip the complexity

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

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

Access TriNet 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