All platforms

Join.com Jobs API.

Pull structured listings, EUR salary ranges, and full markdown descriptions from JOIN-powered European career pages using two public, unauthenticated REST endpoints.

Get API access
Join.com
Live
25K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
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 Join.com.

Data fields
  • Structured Salary Ranges
  • Full Markdown Descriptions
  • City, Region & Country
  • Workplace Type (Onsite/Remote/Hybrid)
  • Employment Type & Category
  • Recruiter Contact Details
Use cases
  1. 01European Job Board Monitoring
  2. 02Salary Benchmarking
  3. 03Multilingual Job Aggregation
  4. 04Startup & Scaleup Talent Sourcing
DIY GUIDE

How to scrape Join.com.

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

HybridintermediateNo documented limit; HTTP 403/429 returned under heavy loadNo auth

Extract the numeric company ID from the company page

The listings API is keyed by a numeric company ID that never appears in the URL. Fetch the company page and parse the __NEXT_DATA__ script tag to read it.

Step 1: Extract the numeric company ID from the company page
import requests
import json
from bs4 import BeautifulSoup

company_slug = "marswalk"
url = f"https://join.com/companies/{company_slug}"

response = requests.get(url, headers={"Accept": "text/html"})
soup = BeautifulSoup(response.text, "html.parser")

# The company ID is embedded in the Next.js __NEXT_DATA__ payload
script_tag = soup.find("script", id="__NEXT_DATA__")
next_data = json.loads(script_tag.string)

company_id = next_data["props"]["pageProps"]["initialState"]["company"]["id"]
print(f"Company ID: {company_id}")

Fetch job listings from the public API

Use the numeric company ID to call the listings endpoint. join.com validates pageSize against a small allow-list, so keep it at 5 and page through the rest.

Step 2: Fetch job listings from the public API
import requests

company_id = 98520  # From step 1
url = f"https://join.com/api/public/companies/{company_id}/jobs"

params = {
    "locale": "en-us",
    "page": 1,
    "pageSize": 5,  # join.com rejects pageSize >= 6 with HTTP 422
}

response = requests.get(url, params=params)
data = response.json()

jobs = data["items"]
pagination = data["pagination"]
print(f"Found {pagination['rowCount']} total jobs across {pagination['pageCount']} pages")

Parse listing fields

Each listing item carries title, location, category, employment and workplace type, plus salary. Salary amounts are in cents, so divide by 100.

Step 3: Parse listing fields
for job in jobs:
    # Salary values are returned in cents
    salary_from = job.get("salaryAmountFrom", {}).get("amount", 0) / 100
    salary_to = job.get("salaryAmountTo", {}).get("amount", 0) / 100
    currency = job.get("salaryAmountFrom", {}).get("currency", "EUR")

    job_info = {
        "id": job["id"],
        "idParam": job.get("idParam"),
        "title": job["title"],
        "location": job.get("city", {}).get("cityName"),
        "region": job.get("city", {}).get("regionName"),
        "country": job.get("city", {}).get("countryName"),
        "workplace_type": job.get("workplaceType"),  # ONSITE, REMOTE, HYBRID
        "category": job.get("category", {}).get("name"),
        "employment_type": job.get("employmentType", {}).get("name"),
        "salary_range": f"{salary_from:,.0f} - {salary_to:,.0f} {currency}" if salary_from else None,
        "created_at": job.get("createdAt"),
    }
    print(job_info)

Fetch the full job description

Call the details endpoint with a listing's numeric id. The description field may be null; fall back to the intro, tasks, requirements, benefits, and outro sections.

Step 4: Fetch the full job description
import requests

job_id = 15565770  # A listing's numeric "id" from step 2
url = f"https://join.com/api/public/jobs/{job_id}"

response = requests.get(url)
job = response.json()

# description may be null — assemble it from the section fields when it is
description = job.get("description")
if not description:
    sections = []
    if job.get("intro"): sections.append(job["intro"])
    if job.get("tasks"): sections.append(f"## Tasks\n\n{job['tasks']}")
    if job.get("requirements"): sections.append(f"## Requirements\n\n{job['requirements']}")
    if job.get("benefits"): sections.append(f"## Benefits\n\n{job['benefits']}")
    if job.get("outro"): sections.append(job["outro"])
    description = "\n\n".join(sections)

full_job = {
    "id": job["id"],
    "title": job["title"],
    "description": description,  # markdown
    "contact_name": job.get("contactName"),
    "contact_email": job.get("contactEmail"),
    "status": job.get("status"),  # ONLINE, OFFLINE, ARCHIVED
}
print(f"{full_job['title']}{full_job['status']}")

Page through the full job board

Loop over every page until the current page reaches pageCount, building the user-facing job URL from the company slug and each listing's idParam.

Step 5: Page through the full job board
import requests

def fetch_all_jobs(company_id: int, company_slug: str, page_size: int = 5) -> list:
    all_jobs = []
    page = 1

    while True:
        url = f"https://join.com/api/public/companies/{company_id}/jobs"
        params = {"locale": "en-us", "page": page, "pageSize": page_size}

        data = requests.get(url, params=params).json()
        for item in data["items"]:
            if not item.get("idParam"):
                continue
            item["job_url"] = f"https://join.com/companies/{company_slug}/jobs/{item['idParam']}"
            all_jobs.append(item)

        if page >= data["pagination"]["pageCount"]:
            break
        page += 1

    return all_jobs

jobs = fetch_all_jobs(98520, "marswalk")
print(f"Retrieved {len(jobs)} total jobs")
Common issues
highRequesting pageSize >= 6 returns HTTP 422

Since mid-2026 join.com validates pageSize against a small allow-list; values of 6 or more return 422 with `[{"param":"pageSize","msg":"Invalid value"}]`. Cap pageSize at 5 and paginate with the page parameter.

criticalCompany ID cannot be derived from the URL

The numeric company ID that the listings API requires only exists inside the __NEXT_DATA__ script tag on the company page. Fetch the HTML page first and read props.pageProps.initialState.company.id.

mediumSalary figures look inflated by 100x

salaryAmountFrom and salaryAmountTo are returned in cents. Divide the amount by 100 before formatting, and read the currency from the same object (usually EUR).

lowThe description field is empty on some jobs

When description is null, the content lives in the intro, tasks, requirements, benefits, and outro fields. Concatenate those sections to rebuild the full markdown body.

mediumRemoved jobs are hard to distinguish from errors

A job pulled from an expired listing returns HTTP 404 from /api/public/jobs/{id}. Treat that 404 as a removal signal, and use the details status field (ONLINE, OFFLINE, ARCHIVED) to filter inactive postings.

lowField values vary by locale

Always send locale=en-us on the listings request so category, employment type, and description language stay consistent across crawls.

Best practices
  1. 1Cache the numeric company ID per slug — it is stable across crawls and avoids re-parsing the HTML page.
  2. 2Request pageSize=5 (the maximum join.com accepts) and iterate the page parameter to the pageCount.
  3. 3Space requests roughly 200ms apart and cap concurrent detail fetches to avoid 403/429 responses.
  4. 4Divide every salary amount by 100 to convert from cents to the stated currency.
  5. 5Always send locale=en-us so field values and descriptions stay consistent.
  6. 6Treat an HTTP 404 from the details endpoint as a removed job rather than a hard failure.
Or skip the complexity

One endpoint. All Join.com jobs. No scraping, no sessions, no maintenance.

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

Access Join.com
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