- highA valid company returns HTTP 404
- Recruitee has no public directory of tenants, so a wrong subdomain simply 404s. Confirm the exact subdomain from the company's live careers URL and treat 404 as a clean 'not found' rather than a retryable error.
- mediumCustom-domain boards are not recognized as Recruitee
- White-label boards drop the recruitee.com host. Probe the same /api/offers path and confirm the JSON contains an 'offers' array before treating a domain as Recruitee; also look for /o/{slug} links on the page.
- mediumThe English translation is missing
- The translations map may not include an 'en' key. Fall back to the first available locale so multi-language boards still yield a title, description, and requirements.
- lowDescription and requirements are raw HTML
- Both fields return HTML and are meant to be concatenated (Recruitee separates them logically). Join them and strip tags with BeautifulSoup when you need plain text, or sanitize before rendering.
- lowThe salary object is null
- Not every board publishes pay, and min/max arrive as strings. Null-check the salary field, cast to float, and fall back to parsing the description HTML when it is absent.
- mediumBulk requests get blocked (HTTP 403 or 429)
- Aggressive fan-out trips Recruitee's protection, which surfaces as 403 or 429. Serialize requests per host and add a short delay (~200ms) between calls when sweeping many boards.
Recruitee Jobs API.
Pull every open role - full HTML descriptions, salary bands, and location data - from a single unauthenticated JSON endpoint, with no per-job page fetches and no pagination to manage.
What's in every response.
Data fields, real-world applications, and the companies already running on Recruitee.
Data fields
- Full HTML Descriptions
- Structured Salary Ranges
- Remote & Hybrid Flags
- Department & Category Codes
- Multi-Language Translations
- Structured Location Fields
Use cases
- 01Job Board Aggregation
- 02Salary Data Extraction
- 03Multi-Company Monitoring
- 04Startup Job Tracking
Trusted by
- 1X Technologies AS
- 2am.tech
- 433
How to scrape Recruitee.
Step-by-step guide to extracting jobs from Recruitee-powered career pages—endpoints, authentication, and working code.
import requests
# Standard Recruitee subdomain
company_slug = "1x"
api_url = f"https://{company_slug}.recruitee.com/api/offers"
# Custom domain (e.g., careers.company.com)
custom_domain_url = "https://careers.company.com/api/offers"
response = requests.get(api_url, timeout=10)
response.raise_for_status()
data = response.json()
print(f"Found {len(data.get('offers', []))} jobs")for offer in data.get("offers", []):
# Extract English translation (default to first available)
translations = offer.get("translations", {})
en_translation = translations.get("en", next(iter(translations.values()), {}))
job = {
"id": offer["id"],
"title": offer["title"],
"slug": offer["slug"],
"company": offer.get("company_name"),
"location": offer.get("location"),
"remote": offer.get("remote", False),
"on_site": offer.get("on_site", True),
"employment_type": offer.get("employment_type_code"),
"category": offer.get("category_code"),
"description": en_translation.get("description", ""),
"requirements": en_translation.get("requirements", ""),
"url": offer.get("careers_url"),
"apply_url": offer.get("careers_apply_url"),
"created_at": offer.get("created_at"),
}
print(f"{job['title']} - {job['location']}")def parse_salary(offer: dict) -> dict | None:
"""Extract structured salary from Recruitee offer."""
salary_data = offer.get("salary")
if not salary_data:
return None
return {
"min": float(salary_data.get("min", 0)),
"max": float(salary_data.get("max", 0)),
"currency": salary_data.get("currency", "USD"),
"period": salary_data.get("period", "year"),
}
# Usage
for offer in data.get("offers", []):
salary = parse_salary(offer)
if salary:
print(f"{offer['title']}: {salary['currency']} {salary['min']:,} - {salary['max']:,} per {salary['period']}")import requests
from urllib.parse import urlparse
def is_recruitee_domain(domain: str) -> bool:
"""Check if a domain is running Recruitee."""
try:
# Check for Recruitee subdomain
if domain.endswith(".recruitee.com"):
return True
# Check for custom domain with /api/offers endpoint
api_url = f"https://{domain}/api/offers"
response = requests.get(api_url, timeout=10)
# Valid Recruitee API returns JSON with 'offers' key
if response.status_code == 200:
data = response.json()
return "offers" in data
except Exception:
pass
return False
def get_api_url(input_url: str) -> str | None:
"""Convert any Recruitee URL to the API endpoint."""
parsed = urlparse(input_url)
domain = parsed.netloc
# Handle job page URLs by extracting base domain
if "/o/" in input_url:
return f"https://{domain}/api/offers"
# Direct API URL
if input_url.endswith("/api/offers"):
return input_url
# Homepage URL
return f"https://{domain}/api/offers"import requests
import time
from datetime import datetime, timedelta
def fetch_recruitee_jobs(company_slug: str, use_cache: bool = True) -> dict:
"""Fetch all jobs from a Recruitee company with error handling."""
url = f"https://{company_slug}.recruitee.com/api/offers"
try:
response = requests.get(url, timeout=15)
if response.status_code == 404:
raise ValueError(f"Company '{company_slug}' not found on Recruitee")
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
raise Exception(f"Rate limited. Retry after {retry_after} seconds")
response.raise_for_status()
return response.json()
except requests.Timeout:
raise Exception(f"Request timed out for {company_slug}")
except requests.RequestException as e:
raise Exception(f"Failed to fetch jobs: {e}")
# Batch processing with rate limiting
def fetch_multiple_companies(slugs: list[str], delay: float = 0.5) -> dict:
"""Fetch jobs from multiple companies with rate limiting."""
results = {}
for slug in slugs:
try:
results[slug] = fetch_recruitee_jobs(slug)
print(f"Fetched {len(results[slug].get('offers', []))} jobs from {slug}")
except Exception as e:
print(f"Error fetching {slug}: {e}")
results[slug] = None
time.sleep(delay)
return results- 1Hit /api/offers once per board - it returns every job with full details and no pagination
- 2Read the structured salary object before parsing pay from the description HTML
- 3Pick the 'en' translation, then fall back to the first available locale
- 4Confirm a JSON 'offers' array before treating any host, including custom domains, as Recruitee
- 5Serialize per-host requests and space them ~200ms apart to avoid 403/429 responses
- 6Parse Recruitee's 'YYYY-MM-DD HH:MM:SS UTC' timestamps into ISO 8601 before storing
One endpoint. All Recruitee jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=recruitee" \
-H "X-Api-Key: YOUR_KEY"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.
Access Recruitee
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.