- highWhy does offset pagination miss jobs on large clients?
- Some TalentReef clients publish tens of thousands of postings — one audited board carries over 18,000. Sort on jobId plus the document id and page with search_after, carrying the previous hit's sort values forward, so results stay stable while the index changes underneath you.
- highWhy does filtering on clientId return other employers' jobs?
- The analysed clientId field tokenises, so a plain match can hit neighbouring values. Filter on the keyword sub-field clientId.raw with a term query, and re-check the clientId on every returned document before emitting it.
- mediumDoes an empty result mean the client left TalentReef?
- No. Around 43 of 727 audited clients were dormant boards with valid tenant URLs and no current postings. Treat an exact query that returns zero hits and a zero total as an authoritative empty snapshot, and keep the employer resolvable.
- mediumWhy are some jobs missing from the English index?
- TalentReef maintains separate search indices per language, including French-Canadian and Spanish. A client hiring outside the US may publish only into a non-English index, so query the language indices you care about rather than assuming the English one is complete.
TalentReef Jobs API.
Pull frontline and hourly openings from any TalentReef client board through the anonymous search API its own apply site calls, with complete descriptions and structured store addresses.
What's in every response.
Data fields, real-world applications, and the companies already running on TalentReef.
Data fields
- Full Job Descriptions
- Structured Store Addresses
- Brand & Category Fields
- Authoritative Result Totals
- Multi-Language Indices
- Deterministic Cursor Paging
Use cases
- 01Hourly & Frontline Job Boards
- 02Restaurant & Retail Hiring Feeds
- 03Multi-Location Employer Tracking
- 04Local Labour Market Research
Trusted by
- Mariane Inc.
- Spirit Halloween
- Rotolo's
How to scrape TalentReef.
Step-by-step guide to extracting jobs from TalentReef-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse
PUBLIC_HOST = "apply.jobappnetwork.com"
def parse_talentreef(url: str) -> dict:
parsed = urlparse(url)
if parsed.netloc.lower() != PUBLIC_HOST:
raise ValueError("not a TalentReef board host")
parts = [p for p in parsed.path.strip("/").split("/") if p]
if len(parts) < 2 or parts[0] != "clients" or not parts[1].isdigit():
raise ValueError("expected /clients/{numericClientId}")
job_id = parts[3] if len(parts) >= 4 and parts[2] == "posting" and parts[3].isdigit() else None
return {"client_id": parts[1], "job_id": job_id}
print(parse_talentreef("https://apply.jobappnetwork.com/clients/10043/posting/11052368"))
# {'client_id': '10043', 'job_id': '11052368'}import requests
SEARCH = ("https://prod-kong.internal.talentreef.com"
"/apply/proxy-es/search-en-us/posting/_search")
PAGE_SIZE = 100
session = requests.Session()
session.headers.update({
"Content-Type": "application/json",
"Origin": "https://apply.jobappnetwork.com",
})
def search_body(client_id: str, size: int = PAGE_SIZE, after: list | None = None) -> dict:
body = {
"size": size,
"query": {"bool": {"filter": [{"term": {"clientId.raw": client_id}}]}},
# Sorting on jobId plus the document id makes the cursor deterministic.
"sort": [{"jobId": "asc"}, {"_id": "asc"}],
}
if after:
body["search_after"] = after
return body
first = session.post(SEARCH, json=search_body("10043"), timeout=30)
first.raise_for_status()
payload = first.json()
total = payload["hits"]["total"]
print("authoritative total:", total["value"] if isinstance(total, dict) else total)def fetch_all(client_id: str) -> list[dict]:
postings, after, seen = [], None, set()
while True:
resp = session.post(SEARCH, json=search_body(client_id, after=after), timeout=30)
resp.raise_for_status()
hits = resp.json()["hits"]["hits"]
if not hits:
return postings
for hit in hits:
source = hit["_source"]
job_id = str(source.get("jobId"))
# Reject anything that is not this client, then dedupe on the native id.
if str(source.get("clientId")) != client_id or job_id in seen:
continue
seen.add(job_id)
postings.append(source)
last = hits[-1].get("sort")
if not last:
raise RuntimeError("hit omitted its sort values — cannot page deterministically")
after = last
postings = fetch_all("10043")
print(f"{len(postings)} postings")def to_job(source: dict) -> dict:
client_id = str(source.get("clientId"))
job_id = str(source.get("jobId"))
address = source.get("address") or {}
return {
"id": job_id,
"client_id": client_id,
"title": source.get("title") or source.get("jobTitle"),
"description_html": source.get("description"),
"brand": source.get("brand"),
"category": source.get("category"),
"city": address.get("city"),
"state": address.get("state"),
"postal_code": address.get("postalCode"),
"listing_url": f"https://apply.jobappnetwork.com/clients/{client_id}/posting/{job_id}",
}
for source in postings[:3]:
job = to_job(source)
print(job["title"], "-", job["city"], job["state"])def board_state(client_id: str) -> str:
resp = session.post(SEARCH, json=search_body(client_id, size=2), timeout=30)
if resp.status_code in (403, 429):
return "rate_limited" # back off; the board is not empty
resp.raise_for_status()
hits = resp.json().get("hits")
if not isinstance(hits, dict) or "hits" not in hits:
return "malformed" # inconclusive — do not expire anything
total = hits.get("total")
count = total.get("value") if isinstance(total, dict) else total
if count == 0 and not hits["hits"]:
return "empty" # dormant board, tenant identity still valid
return "active"
for client in ("10043", "10129", "10233"):
print(client, board_state(client))- 1Take the employer identity from the numeric client ID in the board URL
- 2Filter on the clientId.raw keyword field, never on the analysed clientId
- 3Page with search_after on jobId plus document id instead of offsets
- 4Re-verify the clientId on every returned document before emitting it
- 5Treat a zero-hit, zero-total response as a dormant board rather than a dead client
- 6Skip per-job detail requests — the search documents already carry full descriptions
One endpoint. All TalentReef jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=talentreef" \
-H "X-Api-Key: YOUR_KEY"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.
Access TalentReef
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.