All platforms

Gem Jobs API.

Talent engagement platform with GraphQL batch API for job boards, requiring separate calls for full job descriptions.

Gem
Live
50K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Gem
RetoolFreshBooksWorkivaGustoZillow
Developer tools

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.

Data fields
  • GraphQL batch API
  • Talent sourcing
  • CRM features
  • Pipeline management
  • Engagement tools
  • Analytics
Use cases
  1. 01Enterprise talent sourcing
  2. 02Tech company job monitoring
  3. 03SaaS recruiting pipelines
Trusted by
RetoolFreshBooksWorkivaGustoZillow
DIY GUIDE

How to scrape Gem.

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

GraphQLintermediateNo official limit (recommend 500ms between requests)No auth

Fetch all job listings from the board

Use the GraphQL batch endpoint to retrieve all job postings for a company. The listings API returns job titles, locations, and department info, but not full descriptions.

Step 1: Fetch all job listings from the board
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}")

Parse job listings data

Extract relevant fields from each job posting. Note that full descriptions require a separate API call per job.

Step 2: Parse job listings data
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']}"
    })

Fetch full job details

Make a separate API call for each job to get the full description, compensation, and additional sections like intro and outro HTML.

Step 3: Fetch full job details
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"])

Compose full job description

Combine intro HTML, main description, and outro HTML to create the complete job posting content.

Step 4: Compose full job description
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")

Handle rate limiting and errors

Add delays between requests and handle errors gracefully. The API has no documented rate limits, but reasonable delays are recommended.

Step 5: Handle rate limiting and errors
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 results
Common issues
criticalMissing batch header causes 400 error

Always include the 'batch: true' header in your requests. The API requires this header for the batch GraphQL endpoint to work correctly.

highJob descriptions not returned in listings

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.

mediumInvalid boardId returns null data

Validate the boardId by checking if jobBoardExternal.teamDisplayName exists in the response. A null value indicates an invalid or non-existent board.

lowTwo different extId formats

Gem uses both numeric IDs (e.g., '4003629005') and hash-like IDs (e.g., 'am9icG9zdDpKiCmaMgU6jDLo-wMv--D8'). Handle both formats in your implementation.

lowTimestamp format inconsistency

Note that firstPublishedTsSec is in seconds while startDateTs may be in milliseconds. Convert appropriately when processing timestamps.

Best practices
  1. 1Always include 'batch: true' header for GraphQL requests
  2. 2Fetch listings first, then request job details only as needed
  3. 3Handle both numeric and hash-like extId formats
  4. 4Compose full descriptions from introHtml + descriptionHtml + outroHtml
  5. 5Add 500ms delay between job detail requests to be respectful
  6. 6Validate boardId by checking jobBoardExternal.teamDisplayName in response
Or skip the complexity

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

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

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.

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