TalentTech Portals Jobs API.
Page a company's TalentTech Portals board through one public JSON endpoint, then enrich each role with the JobPosting JSON-LD embedded in its canonical page — no login or API key required.
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 TalentTech Portals.
- Full Job Descriptions
- City, State & Country
- Employment Type
- Hiring Organization Name
- Posted & Closing Dates
- Apply & Detail URLs
- 01Multi-Tenant Board Monitoring
- 02Careers Page Extraction
- 03Enterprise Hiring Tracking
- 04Recruiting Market Research
How to scrape TalentTech Portals.
Step-by-step guide to extracting jobs from TalentTech Portals-powered career pages—endpoints, authentication, and working code.
import requests
# TTC tenants are subdomains of ttcportals.com; the slug is the leftmost label
tenant = "meltwatercareers"
base_url = f"https://{tenant}.ttcportals.com"
headers = {
"Accept": "application/json",
"Referer": f"{base_url}/jobs",
}
resp = requests.get(
f"{base_url}/search/jobs.json",
params={"page": 1},
headers=headers,
timeout=15,
)
resp.raise_for_status()
data = resp.json()
print(f"{data['total_entries']} jobs, {data['per_page']} per page")def fetch_all_listings(base_url: str) -> list[dict]:
headers = {"Accept": "application/json", "Referer": f"{base_url}/jobs"}
listings, page = [], 1
while True:
resp = requests.get(
f"{base_url}/search/jobs.json",
params={"page": page},
headers=headers,
timeout=15,
)
resp.raise_for_status()
data = resp.json()
entries = data.get("entries") or []
for entry in entries:
# talemetry_job_id is the stable external id; skip rows without one
external_id = entry.get("talemetry_job_id")
if not external_id:
continue
permalink = entry.get("permalink") or f"/jobs/{external_id}"
listing_url = permalink if permalink.startswith("http") else base_url + "/" + permalink.lstrip("/")
listings.append({
"external_id": str(external_id),
"title": entry.get("title"),
"location": entry.get("location"),
"listing_url": listing_url,
})
# Stop once the collected count reaches the envelope total
seen = (data["current_page"] - 1) * data["per_page"] + len(entries)
if not entries or seen >= data["total_entries"]:
break
page += 1
return listingsimport json
from bs4 import BeautifulSoup
def fetch_job_detail(listing_url: str) -> dict | None:
resp = requests.get(listing_url, timeout=15)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
# The full posting exists only as JobPosting JSON-LD
for tag in soup.find_all("script", type="application/ld+json"):
try:
block = json.loads(tag.string or "")
except (json.JSONDecodeError, TypeError):
continue
if isinstance(block, dict) and block.get("@type") == "JobPosting":
org = block.get("hiringOrganization")
return {
"title": block.get("title"),
"description": block.get("description"),
"employment_type": block.get("employmentType"),
"company": org.get("name") if isinstance(org, dict) else None,
"posted_at": block.get("datePosted"),
"closes_at": block.get("validThrough"),
"url": block.get("url"),
"locations": block.get("jobLocation"),
}
# Fallback: older tenants omit JSON-LD; read the provider-owned HTML container
main = soup.select_one(".job-details__main")
if main:
title = main.select_one("h1")
body = main.select_one(".job-description")
if title and body:
return {
"title": title.get_text(strip=True),
"description": body.get_text(" ", strip=True),
}
return Noneimport time
def scrape_tenant(tenant: str) -> list[dict]:
base_url = f"https://{tenant}.ttcportals.com"
try:
listings = fetch_all_listings(base_url)
except requests.HTTPError as e:
status = e.response.status_code
if status == 404:
print(f"Tenant '{tenant}' not found — likely moved off ttcportals.com")
elif status in (403, 429):
# A non-browser TLS fingerprint often triggers Cloudflare 403s here
print(f"Blocked or throttled (HTTP {status}); slow down or use a browser TLS client")
raise
jobs = []
for listing in listings:
detail = fetch_job_detail(listing["listing_url"])
jobs.append({**listing, **(detail or {})})
time.sleep(2) # match the scraper's ~1 request / 2s throttle
return jobsTTC's Cloudflare edge fingerprints the TLS handshake and rejects non-browser signatures. If a standard requests client gets 403, switch to a browser-impersonating client (e.g. curl_cffi with a recent Chrome profile) and keep concurrency low.
The search/jobs.json endpoint returns only id, talemetry_job_id, title, permalink, and location. The description, company, employment type, and dates exist only in the JobPosting JSON-LD on the canonical /jobs/{id} page, so fetch each detail page.
Some employers front their board on a vanity domain (e.g. careers.example.com) that can't be identified by URL alone. Map those hosts to their ttcportals tenant explicitly rather than relying on host detection.
When the JSON-LD block is absent, fall back to the provider-owned HTML: read the h1 and .job-description inside .job-details__main, and reject text that is too short to be a real description.
Use talemetry_job_id as the stable external id and skip entries that lack it, rather than falling back to the transient row id, which is not a durable identifier across snapshots.
- 1Derive the tenant slug from the leftmost label of the *.ttcportals.com host
- 2Use talemetry_job_id as the stable external id; skip rows that lack it
- 3Throttle to ~1 request every 2 seconds at single concurrency to stay under Cloudflare limits
- 4Prefer the JobPosting JSON-LD for details; fall back to .job-description only when it is absent
- 5Send a browser-like TLS fingerprint (e.g. curl_cffi impersonation) if a plain client returns 403
- 6Map custom-domain boards to their ttcportals tenant explicitly — host detection alone won't catch them
One endpoint. All TalentTech Portals jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=talenttech portals" \
-H "X-Api-Key: YOUR_KEY" Access TalentTech Portals
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.