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.
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.
- Job Titles & IDs
- Full HTML Descriptions
- Location Per Role
- Employment Type & Category
- Minimum Experience
- Filled-Status Flag
- 01SMB & Mid-Market Job Aggregation
- 02PEO-Hosted Board Monitoring
- 03Company Careers-Page Extraction
- 04Role & Category Analytics
How to scrape TriNet Hire.
Step-by-step guide to extracting jobs from TriNet Hire-powered career pages—endpoints, authentication, and working code.
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)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 rowsdef 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,
}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")Requesting /companies/{companyId} on its own always 404s — listings only exist at /companies/{companyId}/jobs. Append /jobs to every board URL before fetching.
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.
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.
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.
- 1Always request /companies/{id}/jobs — the bare /companies/{id} path 404s
- 2Preserve the full slug IDs (e.g. 116069-chemical-engineer); the numeric prefix alone won't resolve
- 3Treat listings as a single page — TriNet Hire boards aren't paginated, so skip cursor logic
- 4Cap detail concurrency at ~3 and space requests ~300ms apart to avoid 403/429
- 5Flag roles whose body reads 'This position has been filled' instead of counting them as open
- 6Resolve relative /jobs/ hrefs against https://app.trinethire.com before fetching details
One endpoint. All TriNet Hire jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=trinet hire" \
-H "X-Api-Key: YOUR_KEY" 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.