- 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.
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.
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
- 01European Job Board Monitoring
- 02Salary Benchmarking
- 03Multilingual Job Aggregation
- 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.
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}")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")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)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']}")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
Best practices
- 1Cache the numeric company ID per slug — it is stable across crawls and avoids re-parsing the HTML page.
- 2Request pageSize=5 (the maximum join.com accepts) and iterate the page parameter to the pageCount.
- 3Space requests roughly 200ms apart and cap concurrent detail fetches to avoid 403/429 responses.
- 4Divide every salary amount by 100 to convert from cents to the stated currency.
- 5Always send locale=en-us so field values and descriptions stay consistent.
- 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 accesscURL
curl "https://connect.jobo.world/api/jobs?sources=join.com" \
-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 Join.com
Access Join.com
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