Gem Jobs API.
Talent engagement platform with GraphQL batch API for job boards, requiring separate calls for full job descriptions.
Try the API.
Test Jobs, Feed, and Auto-Apply 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 Gem.
- GraphQL batch API
- Talent sourcing
- CRM features
- Pipeline management
- Engagement tools
- Analytics
- 01Enterprise talent sourcing
- 02Tech company job monitoring
- 03SaaS recruiting pipelines
How to scrape Gem.
Step-by-step guide to extracting jobs from Gem-powered career pages—endpoints, authentication, and working code.
import requests
board_id = "retool"
url = "https://jobs.gem.com/api/public/graphql/batch"
payload = [{
"operationName": "JobBoardList",
"variables": {"boardId": board_id},
"query": """query JobBoardList($boardId: String!) {
oatsExternalJobPostings(boardId: $boardId) {
jobPostings {
id
extId
title
locations { id name city isoCountry isRemote }
job { id department { id name } locationType employmentType }
}
}
jobBoardExternal(vanityUrlPath: $boardId) {
id teamDisplayName descriptionHtml pageTitle
}
}"""
}]
headers = {"Content-Type": "application/json", "batch": "true"}
response = requests.post(url, json=payload, headers=headers)
data = response.json()[0]["data"]
jobs = data["oatsExternalJobPostings"]["jobPostings"]
company_name = data["jobBoardExternal"]["teamDisplayName"]
print(f"Found {len(jobs)} jobs at {company_name}")for job in jobs:
locations = job.get("locations", [])
job_info = job.get("job", {})
print({
"id": job["id"],
"ext_id": job["extId"],
"title": job["title"],
"department": job_info.get("department", {}).get("name"),
"location_type": job_info.get("locationType"),
"employment_type": job_info.get("employmentType"),
"is_remote": any(loc.get("isRemote") for loc in locations),
"url": f"https://jobs.gem.com/{board_id}/{job['extId']}"
})import requests
def get_job_details(board_id: str, ext_id: str) -> dict:
url = "https://jobs.gem.com/api/public/graphql/batch"
payload = [{
"operationName": "ExternalJobPostingQuery",
"variables": {"boardId": board_id, "extId": ext_id},
"query": """query ExternalJobPostingQuery($boardId: String!, $extId: String!) {
oatsExternalJobPosting(boardId: $boardId, extId: $extId) {
id title descriptionHtml extId firstPublishedTsSec
locations { id name city isoCountry isRemote }
job {
id department { id name }
locationType employmentType requisitionId
}
jobPostSectionHtml { introHtml outroHtml }
compensationHtml
}
}"""
}]
headers = {"Content-Type": "application/json", "batch": "true"}
response = requests.post(url, json=payload, headers=headers)
return response.json()[0]["data"]["oatsExternalJobPosting"]
# Fetch details for a specific job
job_details = get_job_details("retool", "4003629005")
print(job_details["title"])def compose_full_description(job_details: dict) -> str:
sections = job_details.get("jobPostSectionHtml", {})
parts = []
if sections.get("introHtml"):
parts.append(sections["introHtml"])
if job_details.get("descriptionHtml"):
parts.append(job_details["descriptionHtml"])
if sections.get("outroHtml"):
parts.append(sections["outroHtml"])
return "\n".join(parts)
full_description = compose_full_description(job_details)
print(f"Full description length: {len(full_description)} characters")import time
import requests
def fetch_all_jobs_with_details(board_id: str) -> list:
# Step 1: Get listings
url = "https://jobs.gem.com/api/public/graphql/batch"
listings_query = [{
"operationName": "JobBoardList",
"variables": {"boardId": board_id},
"query": "query JobBoardList($boardId: String!) { oatsExternalJobPostings(boardId: $boardId) { jobPostings { id extId title } } jobBoardExternal(vanityUrlPath: $boardId) { teamDisplayName } }"
}]
headers = {"Content-Type": "application/json", "batch": "true"}
resp = requests.post(url, json=listings_query, headers=headers, timeout=30)
jobs = resp.json()[0]["data"]["oatsExternalJobPostings"]["jobPostings"]
# Step 2: Fetch details for each job
results = []
for i, job in enumerate(jobs):
try:
details = get_job_details(board_id, job["extId"])
results.append({**job, "details": details})
time.sleep(0.5) # Be respectful with rate limiting
except Exception as e:
print(f"Error fetching job {job['extId']}: {e}")
if (i + 1) % 10 == 0:
print(f"Processed {i + 1}/{len(jobs)} jobs")
return resultsAlways include the 'batch: true' header in your requests. The API requires this header for the batch GraphQL endpoint to work correctly.
The listings API only returns basic job info. You must make a separate API call using ExternalJobPostingQuery for each job to get descriptionHtml and compensationHtml.
Validate the boardId by checking if jobBoardExternal.teamDisplayName exists in the response. A null value indicates an invalid or non-existent board.
Gem uses both numeric IDs (e.g., '4003629005') and hash-like IDs (e.g., 'am9icG9zdDpKiCmaMgU6jDLo-wMv--D8'). Handle both formats in your implementation.
Note that firstPublishedTsSec is in seconds while startDateTs may be in milliseconds. Convert appropriately when processing timestamps.
- 1Always include 'batch: true' header for GraphQL requests
- 2Fetch listings first, then request job details only as needed
- 3Handle both numeric and hash-like extId formats
- 4Compose full descriptions from introHtml + descriptionHtml + outroHtml
- 5Add 500ms delay between job detail requests to be respectful
- 6Validate boardId by checking jobBoardExternal.teamDisplayName in response
One endpoint. All Gem jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://enterprise.jobo.world/api/jobs?sources=gem" \
-H "X-Api-Key: YOUR_KEY" Access Gem
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.