All platforms

TalNet (Oleeo) Jobs API.

Pull government and enterprise vacancies from Oleeo-powered tal.net boards by reading each tenant's advertised Atom feed, then enriching from server-rendered detail pages.

Get API access
TalNet (Oleeo)
Live
30K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using TalNet (Oleeo)
University of the Arts LondonKorn FerryEnvironment AgencyThe Royal HouseholdFCDO
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 TalNet (Oleeo).

Data fields
  • Native Vacancy IDs
  • Full Job Descriptions
  • Salary Ranges
  • City, Region & Country
  • Closing & Start Dates
  • Department & Section
Use cases
  1. 01Government Vacancy Tracking
  2. 02Enterprise Job Monitoring
  3. 03Public-Sector Recruitment Data
  4. 04Multi-Region Talent Feeds
Trusted by
University of the Arts LondonKorn FerryEnvironment AgencyThe Royal HouseholdFCDO
DIY GUIDE

How to scrape TalNet (Oleeo).

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

HybridadvancedNo published limit; the reference scraper paces ~500ms between requests and caps detail fetches at 3 concurrentNo auth

Discover the job board URL structure

TalNet URLs vary by company with different appcentre IDs, brand IDs, and board IDs. Start by navigating to the company's candidate page to identify the correct URL pattern.

Step 1: Discover the job board URL structure
import requests
from bs4 import BeautifulSoup
import re

company = "fcdo"
base_url = f"https://{company}.tal.net/candidate"

response = requests.get(base_url, timeout=15)
soup = BeautifulSoup(response.text, "html.parser")

# Look for job board links
job_links = soup.find_all("a", href=re.compile(r"/jobboard/vacancy/\d+/adv"))
if job_links:
    print(f"Found job board: {job_links[0]['href']}")

Resolve the tenant's Atom feed

TalNet exposes each tenant's listings as an Atom feed - the most reliable listings source. Discover it from the application/atom+xml link in the page head, or construct the feed URL from the tenant's appcentre, brand, and board IDs (the mobile-0 and brand- segments are required).

Step 2: Resolve the tenant's Atom feed
import requests
from bs4 import BeautifulSoup
import xml.etree.ElementTree as ET

company = "royalvacancies"
base_url = f"https://{company}.tal.net"

# Preferred: read the advertised Atom feed link from the candidate page.
discovery = requests.get(f"{base_url}/vx/appcentre-ext/candidate/", timeout=15)
soup = BeautifulSoup(discovery.text, "html.parser")
link = soup.find("link", attrs={"type": "application/atom+xml"})

if link and link.get("href"):
    feed_url = requests.compat.urljoin(base_url, link["href"])
else:
    # Fallback: build the feed URL from the tenant's appcentre, brand and board
    # IDs. The mobile-0 and brand- segments are required.
    feed_url = f"{base_url}/vx/mobile-0/appcentre-1/brand-2/candidate/jobboard/vacancy/1/feed"

resp = requests.get(feed_url, timeout=10)
if resp.status_code == 200 and "<feed" in resp.text:
    root = ET.fromstring(resp.content)
    entries = root.findall(".//{http://www.w3.org/2005/Atom}entry")
    print(f"Found {len(entries)} jobs in feed")

Parse job listings from HTML

If no Atom feed is exposed, fall back to scraping the job board HTML. Job links contain the /opp/ pattern, which encodes the job ID and title slug.

Step 3: Parse job listings from HTML
import requests
from bs4 import BeautifulSoup
import re

job_board_url = "https://fcdo.tal.net/vx/appcentre-ext/candidate/jobboard/vacancy/1/adv/"
response = requests.get(job_board_url, timeout=15)
soup = BeautifulSoup(response.text, "html.parser")

# Extract job links using the /opp/ pattern
job_links = soup.find_all("a", href=re.compile(r"/opp/\d+"))

jobs = []
for link in job_links:
    href = link.get("href", "")
    # Extract job ID from URL pattern: /opp/{job_id}-{slug}
    match = re.search(r"/opp/(\d+)", href)
    if match:
        jobs.append({
            "id": match.group(1),
            "title": link.get_text(strip=True),
            "url": href if href.startswith("http") else f"https://fcdo.tal.net{href}"
        })

print(f"Found {len(jobs)} jobs")

Handle pagination for large job boards

When scraping the HTML board directly, TalNet shows roughly 50 jobs per page. Use the ?start= parameter to walk through paginated results and stop when a short page signals the end.

Step 4: Handle pagination for large job boards
import requests
from bs4 import BeautifulSoup
import re
import time

base_url = "https://fcdo.tal.net/vx/appcentre-ext/candidate/jobboard/vacancy/1/adv/"
all_jobs = []
offset = 0

while True:
    url = f"{base_url}?start={offset}"
    response = requests.get(url, timeout=15)
    soup = BeautifulSoup(response.text, "html.parser")

    job_links = soup.find_all("a", href=re.compile(r"/opp/\d+"))

    if not job_links:
        break

    for link in job_links:
        match = re.search(r"/opp/(\d+)", link.get("href", ""))
        if match:
            all_jobs.append({
                "id": match.group(1),
                "title": link.get_text(strip=True),
                "url": link.get("href")
            })

    offset += 50
    time.sleep(1)  # Be respectful

    if len(job_links) < 50:  # Last page
        break

print(f"Total jobs found: {len(all_jobs)}")

Extract job details from individual pages

Current TalNet skins wrap each field in a .form-group block with a <label> and a .form-control-static value; the label-based regex below is a resilient fallback that also works on older text-based skins. Parse description, requirements, location, salary, and deadline, and handle missing fields gracefully.

Step 5: Extract job details from individual pages
import requests
from bs4 import BeautifulSoup
import re

def parse_job_details(job_url: str) -> dict:
    response = requests.get(job_url, timeout=15)
    soup = BeautifulSoup(response.text, "html.parser")

    details = {"url": job_url}

    # Extract title from h1
    h1 = soup.find("h1")
    if h1:
        title_text = h1.get_text(strip=True)
        details["title"] = title_text.replace("View Vacancy - ", "")

    # Extract sections by looking for label patterns
    page_text = soup.get_text()

    patterns = {
        "location": r"Location[^:]*:\s*([^\n]+)",
        "city": r"Location \(City\)[^:]*:\s*([^\n]+)",
        "salary": r"Salary[^:]*:\s*([^\n]+)",
        "deadline": r"(?:Application deadline|Closing Date)[^:]*:\s*([^\n]+)",
        "job_type": r"Type of Position[^:]*:\s*([^\n]+)",
        "region": r"Region[^:]*:\s*([^\n]+)",
        "grade": r"Grade[^:]*:\s*([^\n]+)",
    }

    for field, pattern in patterns.items():
        match = re.search(pattern, page_text, re.IGNORECASE)
        if match:
            details[field] = match.group(1).strip()

    # Extract full job description section
    desc_match = re.search(
        r"Job Description[^:]*:\s*(.+?)(?=Essential qualifications|Application deadline|$)",
        page_text,
        re.DOTALL | re.IGNORECASE
    )
    if desc_match:
        details["description"] = desc_match.group(1).strip()[:2000]

    return details

# Usage example with a real FCDO job URL
job = parse_job_details(
    "https://fcdo.tal.net/vx/lang-en-GB/mobile-0/appcentre-1/brand-2/"
    "candidate/so/pm/4/pl/1/opp/26301-example-job-title/en-GB"
)
print(job)

Extract embedded configuration for URL construction

TalNet pages embed JavaScript configuration containing the appcentre ID, brand ID, and CSRF tokens. Parse this to dynamically construct correct feed and detail URLs for each tenant.

Step 6: Extract embedded configuration for URL construction
import requests
from bs4 import BeautifulSoup
import re
import json

def extract_config(company: str) -> dict:
    url = f"https://{company}.tal.net/candidate"
    response = requests.get(url, timeout=15)
    soup = BeautifulSoup(response.text, "html.parser")

    config = {}

    # Find embedded WCN configuration
    scripts = soup.find_all("script")
    for script in scripts:
        if script.string and "WCN.global_config" in script.string:
            # Extract baseUrl
            base_match = re.search(r'baseUrl:\s*["']([^"']+)["']', script.string)
            if base_match:
                config["baseUrl"] = base_match.group(1)

            # Extract rootPath
            root_match = re.search(r'rootPath:\s*["']([^"']+)["']', script.string)
            if root_match:
                config["rootPath"] = root_match.group(1)

            # Extract appcentre ID from rootPath
            appcentre_match = re.search(r'appcentre-(\d+)', script.string)
            if appcentre_match:
                config["appcentreId"] = appcentre_match.group(1)

            # Extract brand ID
            brand_match = re.search(r'brand-(\d+)', script.string)
            if brand_match:
                config["brandId"] = brand_match.group(1)

            break

    return config

# Usage
config = extract_config("fcdo")
print(f"Appcentre ID: {config.get('appcentreId')}")
print(f"Brand ID: {config.get('brandId')}")
print(f"Base URL: {config.get('baseUrl')}")
Common issues
highResponses arrive truncated or malformed when gzip is advertised

Send the Accept-Encoding: identity header. TalNet's origin mis-frames compressed chunked responses; requesting an unencoded body returns clean HTML and Atom XML.

highBase URL redirects to a login page

Request the scoped board path https://{company}.tal.net/vx/appcentre-ext/candidate/ (or the tenant's Atom feed) directly instead of the bare subdomain, which some tenants (e.g. Evercore, Lazard) gate behind sign-in.

highURL structure varies significantly by company

appcentre, brand, and board IDs differ between tenants, so a single hard-coded URL will not work everywhere. Resolve the feed scope per tenant by reading the application/atom+xml link, or extract the appcentre/brand/board IDs from the page before building the feed URL.

mediumAPI endpoints return empty arrays

The observed /api/v1/cms/adv XHR endpoints are for tracking/analytics only and return empty data. Use the Atom feed for listings and the server-rendered detail pages for full vacancy data.

mediumAtom feed is not advertised via a link tag

Fall back to extracting the appcentre/brand/board IDs from the page HTML and construct the feed URL: /vx/mobile-0/appcentre-{id}/brand-{id}/candidate/jobboard/vacancy/{board}/feed (the mobile-0 and brand- segments are required).

mediumJob detail field labels vary between instances and languages

Field labels and markup differ across tenants and languages (English, Spanish, French, German, Portuguese, Italian, Dutch). Match on normalized, accent-stripped labels, read values from .form-control-static blocks, and fall back to case-insensitive text regex for older skins.

Best practices
  1. 1Send Accept-Encoding: identity - TalNet returns malformed chunked responses when gzip/deflate is advertised
  2. 2Resolve each tenant's Atom feed first (link[type=application/atom+xml] or the mobile-0/appcentre/brand/vacancy/feed pattern) before falling back to HTML
  3. 3Extract job IDs with the /opp/(\d+) or /vacancy/(\d+) URL patterns
  4. 4Space requests out (~500ms) and cap detail concurrency (about 3) to stay under the radar
  5. 5Parse detail fields from .form-group label / .form-control-static value pairs, tolerating multilingual labels
  6. 6Validate against several tenants (UAL, Korn Ferry, Environment Agency, The Royal Household) to cover URL and skin variations
Or skip the complexity

One endpoint. All TalNet (Oleeo) jobs. No scraping, no sessions, no maintenance.

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

Access TalNet (Oleeo)
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