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.
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 Recruitee.
- Full HTML Descriptions
- Structured Salary Ranges
- Remote & Hybrid Flags
- Department & Category Codes
- Multi-Language Translations
- Structured Location Fields
- 01Job Board Aggregation
- 02Salary Data Extraction
- 03Multi-Company Monitoring
- 04Startup Job Tracking
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 resultsRecruitee 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.
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.
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.
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.
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.
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.
- 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" Access Recruitee
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.