SmartRecruiters Jobs API.
Pull structured job postings from a public, no-auth JSON API — complete with department, compensation ranges, and hybrid-work details for thousands of global employers.
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 SmartRecruiters.
- Full Job Descriptions
- Compensation Ranges
- Department & Function
- Hybrid & Remote Flags
- Experience & Employment Type
- Custom Metadata Fields
- 01Global Job Aggregation
- 02Salary Benchmarking
- 03Competitive Hiring Intelligence
- 04Talent Market Analysis
How to scrape SmartRecruiters.
Step-by-step guide to extracting jobs from SmartRecruiters-powered career pages—endpoints, authentication, and working code.
import re
def extract_company_id(url: str) -> str:
"""Extract company identifier from SmartRecruiters URL."""
patterns = [
r'jobs\.smartrecruiters\.com/([^/?]+)',
r'careers\.smartrecruiters\.com/([^/?]+)',
r'api\.smartrecruiters\.com/v1/companies/([^/?]+)',
]
for pattern in patterns:
match = re.search(pattern, url)
if match:
return match.group(1)
raise ValueError(f'Could not extract company ID from {url}')
# Example usage
url = "https://jobs.smartrecruiters.com/Visa"
company_id = extract_company_id(url)
print(f"Company ID: {company_id}") # Output: Visaimport requests
import time
def fetch_all_jobs(company_id: str, page_size: int = 100) -> list[dict]:
"""Fetch all job listings for a company with pagination."""
all_jobs = []
offset = 0
base_url = f"https://api.smartrecruiters.com/v1/companies/{company_id}/postings"
while True:
params = {"limit": page_size, "offset": offset}
response = requests.get(base_url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
jobs = data.get("content", [])
all_jobs.extend(jobs)
total_found = data.get("totalFound", 0)
print(f"Fetched {len(jobs)} jobs (total: {total_found}, offset: {offset})")
# Check if we've fetched all jobs
if len(all_jobs) >= total_found or len(jobs) < page_size:
break
offset += page_size
time.sleep(0.1) # Be respectful with rate limiting
return all_jobs
# Example usage
jobs = fetch_all_jobs("AFCA")
print(f"Total jobs found: {len(jobs)}")import requests
def fetch_job_details(company_id: str, job_id: str) -> dict:
"""Fetch full details for a specific job posting."""
url = f"https://api.smartrecruiters.com/v1/companies/{company_id}/postings/{job_id}"
response = requests.get(url, timeout=30)
response.raise_for_status()
job = response.json()
return {
"id": job.get("id"),
"title": job.get("name"),
"posting_url": job.get("postingUrl"),
"apply_url": job.get("applyUrl"),
"description": job.get("jobAd", {}).get("sections", {}).get("jobDescription", {}).get("text", ""),
"qualifications": job.get("jobAd", {}).get("sections", {}).get("qualifications", {}).get("text", ""),
"company_description": job.get("jobAd", {}).get("sections", {}).get("companyDescription", {}).get("text", ""),
"location": job.get("location", {}).get("fullLocation"),
"city": job.get("location", {}).get("city"),
"country": job.get("location", {}).get("country"),
"remote": job.get("location", {}).get("remote", False),
"hybrid": job.get("location", {}).get("hybrid", False),
"department": job.get("department", {}).get("label"),
"employment_type": job.get("typeOfEmployment", {}).get("label"),
"experience_level": job.get("experienceLevel", {}).get("label"),
"compensation": job.get("compensation"),
"posted_date": job.get("releasedDate"),
"custom_fields": job.get("customField", []),
}
# Example usage
job = fetch_job_details("AFCA", "744000107175576")
print(f"Job: {job['title']}")
print(f"Location: {job['location']}")import requests
def validate_company(company_id: str) -> dict:
"""Validate a company identifier and check for active jobs."""
url = f"https://api.smartrecruiters.com/v1/companies/{company_id}/postings"
params = {"limit": 1, "offset": 0}
try:
response = requests.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
total_found = data.get("totalFound", 0)
if total_found == 0:
return {"valid": False, "reason": "No jobs found or invalid company"}
# Get company name from first posting
company_name = None
if data.get("content"):
company_name = data["content"][0].get("company", {}).get("name")
return {
"valid": True,
"total_jobs": total_found,
"company_name": company_name,
}
except requests.RequestException as e:
return {"valid": False, "reason": str(e)}
# Example usage
result = validate_company("Visa")
print(result) # {'valid': True, 'total_jobs': 150, 'company_name': 'Visa'}
result = validate_company("InvalidCompany123")
print(result) # {'valid': False, 'reason': 'No jobs found or invalid company'}import requests
import time
from typing import Optional
def fetch_with_retry(
url: str,
params: dict = None,
max_retries: int = 3,
delay: float = 0.2
) -> Optional[dict]:
"""Fetch URL with retry logic and rate limiting."""
for attempt in range(max_retries):
try:
time.sleep(delay) # Rate limiting
response = requests.get(url, params=params, timeout=30)
if response.status_code == 404:
print(f"Resource not found: {url}")
return None
if response.status_code == 429:
# Rate limited - wait longer
wait_time = 2 ** attempt
print(f"Rate limited, waiting {wait_time}s...")
time.sleep(wait_time)
continue
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt == max_retries - 1:
raise
return None
# Example usage with caching via ETag
cached_etags = {}
def fetch_with_cache(url: str) -> dict:
"""Fetch with ETag-based caching."""
headers = {}
if url in cached_etags:
headers["If-None-Match"] = cached_etags[url]
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 304:
print("Using cached response")
return None # Return cached data
if response.ok:
if "ETag" in response.headers:
cached_etags[url] = response.headers["ETag"]
return response.json()
response.raise_for_status()SmartRecruiters publishes no official rate limit, so start conservative — roughly 100ms between requests with only a handful of concurrent detail fetches — and apply exponential backoff on any 429 or 403 response, both of which signal throttling.
Check the career-page URL structure. If a company embeds SmartRecruiters behind a custom domain, inspect network requests in browser DevTools to find the api.smartrecruiters.com call containing the identifier. Some companies use a name that differs from their brand (e.g. 'TheNielsenCompany' instead of 'Nielsen').
The listings endpoint returns summary data only. Always call /postings/{jobId} for the full jobAd.sections content (jobDescription, qualifications, companyDescription). A small number of postings omit descriptions upstream even from the details endpoint — keep the record with its other fields rather than dropping it.
These fields are optional in SmartRecruiters. Check the compensation object for min/max/currency and the customField array for salary or location details companies expose there. Not every employer publishes compensation.
A company with no active postings answers 200 OK with an empty content array and totalFound: 0. Validate that totalFound is greater than zero before starting a full crawl.
Job IDs are long numeric strings but some integrations return them as numbers. Always treat them as strings to avoid precision loss on large IDs.
- 1Use limit=100 for optimal pagination throughput
- 2Fetch job details separately — listings never include descriptions
- 3Cache with ETag headers to make re-scrapes cheap
- 4Validate company identifiers before launching a full crawl
- 5Read compensation and the customField array for salary ranges
- 6Capture hybrid work from location.hybrid and hybridDescription
One endpoint. All SmartRecruiters jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=smartrecruiters" \
-H "X-Api-Key: YOUR_KEY" Access SmartRecruiters
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.