TalentNest Jobs API.

Pull every posting from a TalentNest employer board by walking its server-rendered job rows and provider-owned next-page link, then reading JobPosting JSON-LD from each detail page.

Get API access

What's in every response.

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

Data fields

  • Full Job Descriptions
  • JobPosting JSON-LD
  • Numeric Posting IDs
  • Server-Rendered Pagination
  • Bilingual Board Locales
  • Canonical og:url Proof

Use cases

  1. 01Canadian Job Aggregation
  2. 02SMB Careers Page Monitoring
  3. 03Hospitality & Retail Hiring Feeds
  4. 04ATS Data Pipelines

Trusted by

  • 5 Corners
  • AAS
  • Alyeska Resort
DIY GUIDE

How to scrape TalentNest.

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

API type
HTML
Difficulty
intermediate
Rate limit
No published limit; ~150ms between requests, max 3 concurrent detail fetches
Authentication
No auth

Derive the employer from the subdomain

Every TalentNest board is https://{tenant}.talentnest.com, with the locale as the first path segment and jobs at /{locale}/posting/{numericId}. The tenant is the leading host label; anything else on the host is not a TalentNest board.

Step 1: Derive the employer from the subdomain
from urllib.parse import urlparse

HOST_SUFFIX = ".talentnest.com"

def parse_talentnest(url: str) -> dict:
    parsed = urlparse(url)
    host = parsed.netloc.lower()
    if not host.endswith(HOST_SUFFIX):
        raise ValueError("not a TalentNest host")

    tenant = host[: -len(HOST_SUFFIX)]
    if not tenant or "." in tenant:
        raise ValueError("unexpected TalentNest host shape")

    parts = [p for p in parsed.path.strip("/").split("/") if p]
    job_id = None
    if len(parts) == 3 and parts[1] == "posting" and parts[2].isdigit():
        job_id = parts[2]
    elif len(parts) == 2 and parts[0] == "posting" and parts[1].isdigit():
        job_id = parts[1]

    return {
        "tenant": tenant,
        "job_id": job_id,
        "board_url": f"https://{tenant}{HOST_SUFFIX}/en",
    }

print(parse_talentnest("https://5corners.talentnest.com/en/posting/239775"))
# {'tenant': '5corners', 'job_id': '239775', 'board_url': 'https://5corners.talentnest.com/en'}

Parse the board's job rows

The board is server-rendered HTML. Each posting is a .job-row element carrying a data-job-row-url attribute that points at the canonical detail page, so you never have to guess a URL from a title slug.

Step 2: Parse the board's job rows
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

session = requests.Session()
session.headers["Accept"] = "text/html,application/xhtml+xml"

def parse_rows(board_url: str, html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for row in soup.select(".job-row[data-job-row-url]"):
        href = row.get("data-job-row-url")
        detail = urljoin(board_url + "/", href)
        identity = parse_talentnest(detail)
        if not identity["job_id"]:
            continue                      # a row that does not resolve is a rejection
        rows.append({
            "job_id": identity["job_id"],
            "title": " ".join(row.get_text(" ", strip=True).split())[:120],
            "listing_url": detail,
        })
    return rows

board = parse_talentnest("https://5corners.talentnest.com/en")
first = session.get(board["board_url"], timeout=30)
first.raise_for_status()
print(len(parse_rows(board["board_url"], first.text)), "rows on page 1")

Follow the provider-owned next-page link

TalentNest publishes its own pagination as a.next_page[rel=next]. Follow it while it is present and not disabled, and confirm every next URL is still a page of the same employer board — a link that escapes the tenant is a failure, not a new page.

Step 3: Follow the provider-owned next-page link
def crawl_board(board: dict, max_pages: int = 100) -> list[dict]:
    collected, seen = [], set()
    page_url = board["board_url"]

    for _ in range(max_pages):
        resp = session.get(page_url, timeout=30)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.text, "html.parser")

        for row in parse_rows(board["board_url"], resp.text):
            if row["job_id"] not in seen:
                seen.add(row["job_id"])
                collected.append(row)

        nxt = soup.select_one("a.next_page[rel='next']:not(.disabled)")
        if nxt is None or not nxt.get("href"):
            return collected

        candidate = urljoin(board["board_url"] + "/", nxt["href"])
        if parse_talentnest(candidate)["tenant"] != board["tenant"]:
            raise RuntimeError("next-page link escaped the employer board")
        page_url = candidate

    raise RuntimeError("TalentNest pagination exceeded its page bound")

listings = crawl_board(board)
print(f"{len(listings)} postings")

Hydrate details from JobPosting JSON-LD

Each detail page carries a JobPosting JSON-LD block plus a canonical og:url. Prove both against the row you are hydrating: the og:url must name the same tenant and posting, and the JSON-LD identifier.value must equal the numeric job ID.

Step 4: Hydrate details from JobPosting JSON-LD
import json
import time

def find_job_posting(soup) -> dict | None:
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except (json.JSONDecodeError, TypeError):
            continue
        for node in (data if isinstance(data, list) else [data]):
            if isinstance(node, dict) and node.get("@type") == "JobPosting":
                return node
    return None

def hydrate(board: dict, row: dict) -> dict | None:
    canonical = f"{board['board_url']}/posting/{row['job_id']}"
    resp = session.get(canonical, timeout=30)
    if resp.status_code in (404, 410):
        return None                       # the only removal evidence TalentNest gives
    resp.raise_for_status()

    soup = BeautifulSoup(resp.text, "html.parser")
    og_url = (soup.select_one("meta[property='og:url']") or {}).get("content", "")
    if parse_talentnest(og_url)["job_id"] != row["job_id"]:
        raise RuntimeError("og:url contradicted the requested posting")

    posting = find_job_posting(soup)
    if not posting:
        raise RuntimeError("detail page carried no JobPosting JSON-LD")

    identifier = (posting.get("identifier") or {}).get("value")
    if str(identifier) != row["job_id"]:
        raise RuntimeError("JSON-LD identifier contradicted the requested posting")

    return {
        "job_id": row["job_id"],
        "title": posting.get("title"),
        "description_html": posting.get("description"),
        "posted_at": posting.get("datePosted"),
        "employment_type": posting.get("employmentType"),
        "listing_url": canonical,
    }

for row in listings[:3]:
    print(hydrate(board, row)["title"])
    time.sleep(0.15)
Common issues
highWhy does my crawler loop forever on a TalentNest board?
Drive pagination only from a.next_page[rel=next] and stop as soon as it is absent or carries the disabled class. Add a page bound and a seen-ID set as defensive guards so a board that keeps rendering the same next link cannot spin the crawler.
highWhy does a detail page belong to a different job than the row?
Confirm identity twice before storing anything: the page's og:url must name the same tenant and posting ID, and the JobPosting JSON-LD identifier.value must equal the numeric job ID from the row. A disagreement is a hard failure, not a field to overwrite.
mediumWhen is a TalentNest job actually removed?
Only a canonical HTTP 404 or 410 on the /{locale}/posting/{id} URL is removal evidence. A redirect, an empty board page, or a detail page missing JSON-LD is a scrape failure — expiring jobs from those states deletes postings that are still live.
lowWhy do some listing rows fail to resolve to a job ID?
Rows are located by .job-row[data-job-row-url], and a row whose URL does not match the /{locale}/posting/{numericId} shape cannot be attributed. Count those as rejections and mark the snapshot incomplete rather than dropping them quietly.
Best practices
  1. 1Take the employer identity from the leading host label, never from a job title
  2. 2Read detail URLs from data-job-row-url instead of rebuilding them from slugs
  3. 3Follow a.next_page[rel=next] and verify each next URL stays on the same tenant
  4. 4Require both og:url and the JSON-LD identifier to match before hydrating a job
  5. 5Treat only canonical 404/410 responses as removals
  6. 6Throttle to roughly 150ms between requests with at most three concurrent details
Or skip the complexity

One endpoint. All TalentNest jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=talentnest" \
  -H "X-Api-Key: YOUR_KEY"
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.

Ready to integrate

Access TalentNest
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.

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