Lumesse TalentLink Jobs API.
Pull structured vacancies from enterprise career sites running Lumesse TalentLink (formerly Infinite Talent) through its public guest Front Office API — full adverts, salary and locations returned as clean JSON.
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 Lumesse TalentLink.
- Full Job Descriptions
- Structured Salary Text
- Contract Type & Schedule
- Department & Team
- Posting & Closing Dates
- Location & Region Labels
- 01Enterprise Vacancy Aggregation
- 02Careers Site Monitoring
- 03Salary & Benefits Benchmarking
- 04Job Board Syndication
How to scrape Lumesse TalentLink.
Step-by-step guide to extracting jobs from Lumesse TalentLink-powered career pages—endpoints, authentication, and working code.
import re
import requests
# The site technical ID and regional API host come from the TalentLink
# widget embedded on the corporate careers page.
CAREERS_URL = "https://www.unitedutilities.com/corporate/careers/jobs/"
html = requests.get(CAREERS_URL, timeout=30).text
match = re.search(r'data-talentlink-fo-site-tech-id="([A-Z0-9]{20,})"', html)
SITE_ID = match.group(1) if match else "PGLFK026203F3VBQB688MF6CR"
# Regional host varies per tenant, e.g. emea3.recruitmentplatform.com or
# emea5-foc.lumessetalentlink.com. Confirm it from the widget config.
API_HOST = "https://emea3.recruitmentplatform.com"
LANGUAGE = "UK" # the board language passed in the lumesse-language headerdef fetch_page(offset=0, page_size=12):
url = f"{API_HOST}/fo/rest/jobs"
params = {
"firstResult": offset,
"maxResults": page_size,
"sortBy": "DPOSTINGSTART",
"sortOrder": "desc",
}
headers = {
"Accept": "application/json",
"username": f"{SITE_ID}:guest:FO",
"password": "guest",
"lumesse-language": LANGUAGE,
"Referer": CAREERS_URL,
}
resp = requests.post(
url, params=params, headers=headers,
json={"searchCriteria": {}}, timeout=30,
)
resp.raise_for_status()
return resp.json()
data = fetch_page()
total = data["globals"]["jobsCount"]
print(f"{total} vacancies on this board")def strip_html(text):
return re.sub(r"<[^>]+>", "", text or "").strip()
def first(fields, *names):
for name in names:
value = fields.get(name)
if value:
return value
return None
def parse_job(row):
fields = row.get("jobFields", {})
sections = []
for cf in row.get("customFields", []):
content = strip_html(cf.get("content"))
if content:
heading = (cf.get("title") or "").strip()
sections.append(f"{heading}\n{content}" if heading else content)
return {
"id": row.get("id"),
"title": first(fields, "jobTitle", "sJobTitle", "SJOBTITLE"),
"apply_url": fields.get("applicationUrl"),
"location": first(fields, "SLOCATION", "SLOVLIST2", "REGLABEL"),
"salary": fields.get("SALARY"),
"contract_type": first(fields, "CONTRACTTYPLABEL", "SLOVLIST208"),
"department": first(fields, "SDPTNAMELEVEL1", "SDPTNAME"),
"posted_at": first(fields, "DPOSTINGSTART", "postingStartDate"),
"closes_at": first(fields, "DPOSTINGEND", "postingEndDate"),
"description": "\n\n".join(sections),
}def scrape_all():
jobs, seen, offset = [], set(), 0
while True:
data = fetch_page(offset)
total = data["globals"]["jobsCount"]
rows = data.get("jobs", [])
if not rows:
break
for row in rows:
job = parse_job(row)
if job["id"] and job["id"] not in seen and len(job["description"]) >= 40:
seen.add(job["id"])
jobs.append(job)
offset += len(rows)
if offset >= total:
break
return jobs
all_jobs = scrape_all()
print(f"Collected {len(all_jobs)} vacancies")Read the correct host from the TalentLink widget config on the careers page instead of hardcoding one. The /fo/rest/jobs path is stable, but the origin must match the tenant's region.
Extract the site technical ID from the widget marker data-talentlink-fo-site-tech-id embedded on the corporate careers page, then reuse it in the username header and pagination calls.
Send the public guest identity on every request: username set to {siteId}:guest:FO, password set to guest, the board language in lumesse-language, and the corporate board URL as the Referer.
Read each value through an ordered fallback across the known key variants rather than a single property, so a missing key on one board does not blank the field.
Concatenate each customFields entry (title plus HTML-stripped content) into the description and skip rows whose combined text is below a small length threshold.
- 1Read the site technical ID and regional host from the widget config on each careers page before calling the API.
- 2Send the full guest identity on every request: username {siteId}:guest:FO, password guest, lumesse-language and the board Referer.
- 3Treat globals.jobsCount as the authoritative total and page with firstResult until the offset reaches it.
- 4Resolve each field through ordered fallbacks (jobTitle/sJobTitle, SLOCATION/SLOVLIST2/REGLABEL) so tenant-specific keys never blank a value.
- 5Rebuild descriptions from customFields[], strip the embedded HTML, and skip thin placeholder rows.
- 6Dedupe on the numeric job id and pace requests (~150ms) to stay well within the guest API's tolerance.
One endpoint. All Lumesse TalentLink jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=lumesse talentlink" \
-H "X-Api-Key: YOUR_KEY" Access Lumesse TalentLink
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.