All platforms

GR8 People Jobs API.

Pull complete enterprise job listings — full HTML descriptions, Schema.org pay ranges, and requisition data — from a single career-site GraphQL call, with no HTML parsing or browser rendering required.

Get API access
GR8 People
Live
<3haverage discovery time
1hrefresh interval
Companies using GR8 People
Lamb WestonT. Rowe PriceRandstad
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 GR8 People.

Data fields
  • Full HTML Job Descriptions
  • Structured Pay Ranges & Currency
  • Schema.org JobPosting Data
  • Requisition Numbers & Job Keys
  • Workplace & Position Types
  • Location & Remote Flags
Use cases
  1. 01Enterprise Job Aggregation
  2. 02Compensation Benchmarking
  3. 03Talent Market Intelligence
  4. 04Recruiting Data Pipelines
Trusted by
Lamb WestonT. Rowe PriceRandstad
DIY GUIDE

How to scrape GR8 People.

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

GraphQLintermediate1 request per 2 seconds, single-threaded, recommendedNo auth

Resolve the tenant host

Every GR8 People board is served from the customer's own career-site host. The GraphQL endpoint lives at /graphql on that same host. Treat any subdomain of gr8people.com as a tenant, plus the verified branded host gr8people.randstad.co.uk, but skip the vendor infrastructure hosts (www, api, assets, demo, help, status, support, t) which are not boards.

Step 1: Resolve the tenant host
# GR8 People tenants are the full career-site host itself.
# These vendor hosts are NOT tenants and must be skipped.
RESERVED_HOSTS = {
    "www.gr8people.com", "api.gr8people.com", "assets.gr8people.com",
    "demo.gr8people.com", "help.gr8people.com", "status.gr8people.com",
    "support.gr8people.com", "t.gr8people.com",
}

def is_tenant(host: str) -> bool:
    host = host.lower()
    if host == "gr8people.randstad.co.uk":  # verified branded host
        return True
    return host.endswith(".gr8people.com") and host not in RESERVED_HOSTS

tenant = "lambweston.gr8people.com"          # e.g. Lamb Weston, T. Rowe Price
assert is_tenant(tenant)
board_url = f"https://{tenant}/jobs"
graphql_url = f"https://{tenant}/graphql"
print(board_url, graphql_url)

POST the searchJobs trusted document

The board is powered by a persisted (trusted-document) GraphQL contract. Send operationName 'searchJobs' with the trusted-document id 'search-jobs' in extensions, and offset paging via start/first. Include browser-like Accept, Origin, and Referer headers so the endpoint treats you as the career site.

Step 2: POST the searchJobs trusted document
import requests

tenant = "lambweston.gr8people.com"
graphql_url = f"https://{tenant}/graphql"

payload = {
    "operationName": "searchJobs",
    "variables": {"query": "", "start": 0, "first": 100, "filters": {}},
    "extensions": {"trustedDocument": {"id": "search-jobs"}},
}

headers = {
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Origin": f"https://{tenant}",
    "Referer": f"https://{tenant}/jobs",
}

resp = requests.post(graphql_url, json=payload, headers=headers, timeout=30)
resp.raise_for_status()

body = resp.json()
if body.get("errors"):
    raise RuntimeError(f"GraphQL errors: {body['errors']}")

results = body["data"]["searchJobs"]["results"]
print("total:", results["totalCount"], "this page:", len(results["nodes"]))

Page through every result

Advance the start offset by the number of nodes returned and request 100 at a time. totalCount is authoritative for how many jobs exist; pageInfo.hasNextPage is a secondary hint. Stop only once you have reached totalCount, and treat an empty page below the total as a broken snapshot rather than the end.

Step 3: Page through every result
import requests
import time

tenant = "troweprice.gr8people.com"
graphql_url = f"https://{tenant}/graphql"
headers = {
    "Accept": "application/json",
    "Content-Type": "application/json",
    "Origin": f"https://{tenant}",
    "Referer": f"https://{tenant}/jobs",
}
PAGE_SIZE = 100

def fetch_page(start):
    payload = {
        "operationName": "searchJobs",
        "variables": {"query": "", "start": start, "first": PAGE_SIZE, "filters": {}},
        "extensions": {"trustedDocument": {"id": "search-jobs"}},
    }
    resp = requests.post(graphql_url, json=payload, headers=headers, timeout=30)
    resp.raise_for_status()
    return resp.json()["data"]["searchJobs"]["results"]

all_jobs, start = [], 0
while True:
    results = fetch_page(start)
    nodes, total = results["nodes"], results["totalCount"]
    if not nodes:
        if start < total:
            raise RuntimeError(f"Empty page at {start} while {total} jobs reported")
        break
    all_jobs.extend(nodes)
    start += len(nodes)
    if start >= total or not results["pageInfo"]["hasNextPage"]:
        break
    time.sleep(2)  # single in-flight request, ~2s apart

print(f"Fetched {len(all_jobs)} of {total} jobs")

Map each node into a job record

Each node already carries the full descriptionHTML and a Schema.org JobPosting in structuredDataJSON, so no detail fetch is needed. Use the numeric 'key' (not the requisition 'number') to build the canonical /jobs/{key}/{title-slug} URL and the /apply route, and prefer the structured data for company, dates, and locations.

Step 4: Map each node into a job record
import json

def slugify(title: str) -> str:
    cleaned = "".join(c if (c.isalnum() or c == " ") else " " for c in title.lower())
    return "-".join(cleaned.split()) or "job"

def parse_job(node, tenant):
    key = (node.get("key") or "").strip()
    title = (node.get("title") or "").strip()
    description = (node.get("descriptionHTML") or "").strip()
    # GR8 People routes only expose numeric job keys.
    if not key.isdigit() or not title or not description:
        return None

    listing_url = f"https://{tenant}/jobs/{key}/{slugify(title)}"

    structured = {}
    if node.get("structuredDataJSON"):
        try:
            structured = json.loads(node["structuredDataJSON"])
        except json.JSONDecodeError:
            structured = {}

    return {
        "external_id": key,                       # native route key
        "requisition_number": node.get("number"), # metadata only, NOT a URL key
        "title": title,
        "description_html": description,
        "listing_url": listing_url,
        "apply_url": f"{listing_url}/apply",
        "company": (structured.get("hiringOrganization") or {}).get("name"),
        "posted_at": structured.get("datePosted") or node.get("postedOn"),
        "closes_at": structured.get("validThrough"),
        "workplace_type": node.get("workplaceType"),
        "pay_range": {
            "low": node.get("payRangeLow"),
            "mid": node.get("payRangeMid"),
            "high": node.get("payRangeHigh"),
            "currency": node.get("currencyCode"),
        },
    }

jobs = [j for n in all_jobs if (j := parse_job(n, tenant))]
print(f"Mapped {len(jobs)} jobs")
Common issues
highVendor infrastructure hosts are mistaken for tenants

Hosts like www, api, assets, demo, help, status, support, and t under gr8people.com are marketing/infrastructure, not career boards. Only treat other *.gr8people.com subdomains (or the verified gr8people.randstad.co.uk host) as tenants; posting searchJobs to a reserved host returns no board.

highQuery rejected when the trusted document is omitted

The endpoint is a persisted-query contract, so a raw ad-hoc GraphQL body is refused. Always send operationName 'searchJobs' together with extensions.trustedDocument.id set to 'search-jobs'.

mediumAn empty page appears mid-crawl before totalCount is reached

A page returning zero nodes while start is still below results.totalCount signals an incomplete snapshot, not the end of the board. Only stop once start reaches totalCount, and retry the empty page rather than accepting a short result.

mediumHTTP 403 or 429 blocks the request

Some tenants block requests that do not look like the career site. Send the Accept, Origin, and Referer headers, keep a single in-flight request roughly 2 seconds apart, and back off on 403 (blocked) and 429 (rate limited).

lowRequisition number used as the URL key yields dead links

node.number is the requisition and differs from node.key, the numeric route key. Build /jobs/{key}/{slug} and apply URLs from node.key, and keep node.number as metadata only.

Best practices
  1. 1Use the numeric node.key for external IDs and URL building; store node.number as requisition metadata only.
  2. 2Always send operationName 'searchJobs' plus extensions.trustedDocument.id 'search-jobs' on every request.
  3. 3Page at first=100 and trust results.totalCount over pageInfo.hasNextPage when the two disagree.
  4. 4Throttle to a single in-flight request roughly 2 seconds apart and back off on 403 and 429.
  5. 5Prefer structuredDataJSON (Schema.org JobPosting) for company, dates, and locations, falling back to primaryPlace and places.
  6. 6Skip nodes missing a numeric key or descriptionHTML instead of emitting partial records.
Or skip the complexity

One endpoint. All GR8 People jobs. No scraping, no sessions, no maintenance.

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

Access GR8 People
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