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.

Get API access

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

  1. 01Hourly & Frontline Job Boards
  2. 02Restaurant & Retail Hiring Feeds
  3. 03Multi-Location Employer Tracking
  4. 04Local Labour Market Research

Trusted by

  • Mariane Inc.
  • Spirit Halloween
  • Rotolo's
DIY GUIDE

How to scrape TalentReef.

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

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

Read the numeric client ID from the board URL

Every TalentReef board is https://apply.jobappnetwork.com/clients/{clientId}, and job pages add /posting/{jobId}. Both identifiers are numeric on the vendor's own host, which is what makes the public URL sufficient proof of who the employer is.

Step 1: Read the numeric client ID from the board URL
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'}

Query the anonymous search API

The current apply bundle calls a public search proxy that exposes exact clientId and jobId filters plus authoritative totals. Send the apply site as the Origin header and filter on the keyword field clientId.raw so a numeric prefix cannot match a different client.

Step 2: Query the anonymous search API
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)

Page with search_after, not offsets

Offset paging breaks down on this data — the largest audited client publishes over 18,000 postings. Carry the last hit's sort values into the next request as search_after so paging stays deterministic and cannot loop over a rejected tail.

Step 3: Page with search_after, not offsets
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")

Map each posting into a job record

The search documents are already complete: no per-job detail request is needed. Each record carries the native IDs, the full description, a structured address, the application path, brand and category. Rebuild the canonical URL from the client and job IDs.

Step 4: Map each posting into a job record
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"])

Distinguish a dormant board from a failed query

An exact client filter that returns zero hits with a zero total is an authoritative empty board, and roughly 6% of audited clients are in that state at any time. A malformed response or an error is not the same thing and must never expire a board's jobs.

Step 5: Distinguish a dormant board from a failed query
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))
Common issues
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.
Best practices
  1. 1Take the employer identity from the numeric client ID in the board URL
  2. 2Filter on the clientId.raw keyword field, never on the analysed clientId
  3. 3Page with search_after on jobId plus document id instead of offsets
  4. 4Re-verify the clientId on every returned document before emitting it
  5. 5Treat a zero-hit, zero-total response as a dormant board rather than a dead client
  6. 6Skip per-job detail requests — the search documents already carry full descriptions
Or skip the complexity

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

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=talentreef" \
  -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 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.

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