Homerun Jobs API.
Pull richly structured vacancies from modern, design-forward European career sites without any API key. Each tenant advertises an Atom feed carrying titles, departments, locations, salary indications, and full HTML descriptions.
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 Homerun.
- Full HTML job descriptions
- Department & team names
- Employment type
- Salary indication
- Location names
- Feed updated timestamps
- 01European Job Board Aggregation
- 02Dutch & EU Company Monitoring
- 03Scaleup Hiring Tracking
- 04Multi-Language Job Extraction
How to scrape Homerun.
Step-by-step guide to extracting jobs from Homerun-powered career pages—endpoints, authentication, and working code.
import requests
import xml.etree.ElementTree as ET
company_slug = "woonstad-rotterdam"
sitemap_url = f"https://{company_slug}.homerun.co/sitemap.xml"
response = requests.get(sitemap_url, timeout=10)
response.raise_for_status()
# Parse XML sitemap
root = ET.fromstring(response.content)
namespace = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
urls = [loc.text for loc in root.findall(".//sm:loc", namespace)]
print(f"Found {len(urls)} URLs in sitemap")# Filter job URLs from sitemap
job_urls = [
url for url in urls
if not url.endswith("/apply") # Exclude apply pages
and url.split("/")[-1] != "" # Exclude homepage
and len(url.split("/")) > 4 # Has job slug in path
]
# Also extract lastmod dates for incremental updates
job_data = []
for url_elem in root.findall(".//sm:url", namespace):
loc = url_elem.find("sm:loc", namespace)
lastmod = url_elem.find("sm:lastmod", namespace)
if loc is not None and not loc.text.endswith("/apply"):
job_data.append({
"url": loc.text,
"lastmod": lastmod.text if lastmod is not None else None
})
print(f"Found {len(job_urls)} job URLs")
for url in job_urls[:5]:
print(f" - {url}")from bs4 import BeautifulSoup
def parse_job_details(url):
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, "html.parser")
# Extract job title from h1 in main content
title_elem = soup.select_one("main h1")
title = title_elem.get_text(strip=True) if title_elem else "Unknown"
# Extract full job description from main element
main_content = soup.select_one("main")
description_html = str(main_content) if main_content else ""
description_text = main_content.get_text(strip=True) if main_content else ""
# Extract any metadata available
employment_type = None
location = None
# Look for common patterns in job cards
for tag in soup.select("main a, main div"):
text = tag.get_text(strip=True).lower()
if any(t in text for t in ["fulltime", "parttime", "full-time", "part-time"]):
employment_type = text.title()
break
return {
"url": url,
"title": title,
"description_text": description_text[:500],
"description_html": description_html,
"employment_type": employment_type,
"location": location,
}
# Parse first job as example
if job_urls:
job = parse_job_details(job_urls[0])
print(f"Title: {job['title']}")
print(f"Type: {job['employment_type']}")def fetch_atom_feed(company_slug):
feed_url = f"https://feed.homerun.co/{company_slug}"
try:
response = requests.get(feed_url, timeout=10)
response.raise_for_status()
except requests.RequestException:
print(f"Atom feed not available for {company_slug}")
return []
soup = BeautifulSoup(response.text, "xml")
jobs = []
for entry in soup.find_all("entry"):
title_elem = entry.find("title")
summary_elem = entry.find("summary")
link_elem = entry.find("link")
updated_elem = entry.find("updated")
job = {
"title": title_elem.get_text(strip=True) if title_elem else None,
"summary": summary_elem.get_text(strip=True)[:300] if summary_elem else None,
"url": link_elem.get("href") if link_elem else None,
"updated": updated_elem.get_text(strip=True) if updated_elem else None,
}
# Extract custom namespace elements if available
# Homerun may include department, location, salary in extensions
jobs.append(job)
return jobs
# Example usage
atom_jobs = fetch_atom_feed("breeze")
print(f"Found {len(atom_jobs)} jobs from Atom feed")
for job in atom_jobs[:3]:
print(f" - {job['title']}")import time
def scrape_all_jobs(job_urls, delay=1.0):
"""Scrape all jobs with rate limiting and error handling."""
jobs = []
for i, url in enumerate(job_urls, 1):
try:
print(f"Scraping {i}/{len(job_urls)}: {url}")
job = parse_job_details(url)
jobs.append(job)
time.sleep(delay) # Respectful rate limiting
except requests.RequestException as e:
print(f"Network error on {url}: {e}")
continue
except Exception as e:
print(f"Parse error on {url}: {e}")
continue
return jobs
# Scrape with 1 second delay between requests
all_jobs = scrape_all_jobs(job_urls, delay=1.0)
print(f"Successfully scraped {len(all_jobs)} jobs")Homerun exposes no public JSON API. Guessed paths like https://{company}.homerun.co/api/v1/jobs return the homepage HTML via SPA routing fallback (not a 404), and https://api.homerun.co/... returns 404. Ignore these and pull jobs from the per-tenant Atom feed or the sitemap.
Not every slug is a live tenant. Confirm the board exists by checking that the homepage (https://{company}.homerun.co/) returns HTTP 200 before you crawl, and treat a 404 as 'company not found' rather than a transient error.
Homerun publishes no central sitemap of all tenants. Seed your slug list from web searches (site:*.homerun.co), the Homerun customer showcase, or DNS enumeration, since discovery must happen outside the platform.
Some boards (e.g. ghost.homerun.co, nomod.homerun.co, dopper.homerun.co) 301 away from Homerun to the company's own domain such as careers.ghost.org or jobs.dopper.com. Follow redirects, and when the final host is no longer *.homerun.co, treat that tenant as no longer served by Homerun.
The same posting is served at /en, /nl and other locale suffixes. Normalize each URL by stripping the trailing two-letter language code and deduplicate on the resulting job slug so one posting is stored once.
Not every tenant exposes feed.homerun.co. Check the response status before parsing and fall back to sitemap discovery plus HTML parsing whenever the feed is unavailable.
- 1Prefer the Atom feed at feed.homerun.co for the richest structured fields, then fall back to sitemap + HTML
- 2Use sitemap.xml as your discovery backbone - one request enumerates every job URL
- 3Drive incremental refreshes from sitemap lastmod and feed updated timestamps instead of re-scraping everything
- 4Normalize away the trailing language code and deduplicate on the job slug to collapse locale variants
- 5Verify a company subdomain returns HTTP 200 before bulk scraping to avoid wasted 404s
- 6Keep request pacing modest (a few hundred ms between calls, low concurrency) to stay a polite crawler
One endpoint. All Homerun jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=homerun" \
-H "X-Api-Key: YOUR_KEY" Access Homerun
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.