All platforms

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.

Get API access
TalentTech Portals
Live
<3haverage discovery time
1hrefresh interval
Companies using TalentTech Portals
MeltwaterTyler TechnologiesParkerGeneral Dynamics
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 TalentTech Portals.

Data fields
  • Full Job Descriptions
  • City, State & Country
  • Employment Type
  • Hiring Organization Name
  • Posted & Closing Dates
  • Apply & Detail URLs
Use cases
  1. 01Multi-Tenant Board Monitoring
  2. 02Careers Page Extraction
  3. 03Enterprise Hiring Tracking
  4. 04Recruiting Market Research
Trusted by
MeltwaterTyler TechnologiesParkerGeneral Dynamics
DIY GUIDE

How to scrape TalentTech Portals.

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

HybridadvancedNo published limit; TTC sits behind Cloudflare — the scraper throttles to ~1 request / 2s at single concurrencyNo auth

Build the listings endpoint for a tenant

Every TalentTech Portals tenant is a subdomain of ttcportals.com, and the slug is the leftmost host label. Hit the public paged JSON search endpoint — no auth or API key is needed, but send a JSON Accept header and a Referer pointing at the tenant's /jobs page.

Step 1: Build the listings endpoint for a tenant
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")

Page through every listing

Each entry carries id, talemetry_job_id, title, permalink, and location. Use talemetry_job_id as the stable external id and skip rows without one. Walk pages until the count you have collected reaches total_entries.

Step 2: Page through every listing
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 listings

Extract the JobPosting JSON-LD from each detail page

The listing feed returns only row metadata — the full description, employment type, hiring organization, and dates live only in the JobPosting JSON-LD block on the canonical /jobs/{id} page. Fetch each detail URL and read that block.

Step 3: Extract the JobPosting JSON-LD from each detail page
import 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 None

Throttle and classify Cloudflare responses

TTC sits behind Cloudflare, so keep single concurrency with ~2s between requests. Map a 404 on the listing endpoint to a tenant that moved off TTC, and treat 403/429 as blocking rather than a missing board.

Step 4: Throttle and classify Cloudflare responses
import 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 jobs
Common issues
highCloudflare returns HTTP 403 to plain HTTP clients

TTC'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.

highDetails are missing from the listing feed

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.

mediumCustom-domain boards are not on *.ttcportals.com

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.

mediumOlder tenants emit no JobPosting JSON-LD

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.

lowRows missing talemetry_job_id

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.

Best practices
  1. 1Derive the tenant slug from the leftmost label of the *.ttcportals.com host
  2. 2Use talemetry_job_id as the stable external id; skip rows that lack it
  3. 3Throttle to ~1 request every 2 seconds at single concurrency to stay under Cloudflare limits
  4. 4Prefer the JobPosting JSON-LD for details; fall back to .job-description only when it is absent
  5. 5Send a browser-like TLS fingerprint (e.g. curl_cffi impersonation) if a plain client returns 403
  6. 6Map custom-domain boards to their ttcportals tenant explicitly — host detection alone won't catch them
Or skip the complexity

One endpoint. All TalentTech Portals jobs. No scraping, no sessions, no maintenance.

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

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.

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