All platforms

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.

Get API access
Homerun
Live
25K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Homerun
Woonstad RotterdamAppSignalSecfiThoughtly
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.

What's in every response.

Data fields, real-world applications, and the companies already running on Homerun.

Data fields
  • Full HTML job descriptions
  • Department & team names
  • Employment type
  • Salary indication
  • Location names
  • Feed updated timestamps
Use cases
  1. 01European Job Board Aggregation
  2. 02Dutch & EU Company Monitoring
  3. 03Scaleup Hiring Tracking
  4. 04Multi-Language Job Extraction
Trusted by
Woonstad RotterdamAppSignalSecfiThoughtly
DIY GUIDE

How to scrape Homerun.

Step-by-step guide to extracting jobs from Homerun-powered career pages—endpoints, authentication, and working code.

HybridbeginnerNo published limit. The reference scraper spaces requests ~300ms apart and caps at 3 concurrent detail fetches.No auth

Discover jobs using the sitemap

Discover every open role from one sitemap.xml request per company — enumerate job URLs with no HTML parsing before fetching details.

Step 1: Discover jobs using the sitemap
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 entries

The sitemap mixes the homepage, job pages, and apply pages. Keep only the job posting links, and capture lastmod dates so you can run cheap incremental refreshes later.

Step 2: Filter job URLs from sitemap entries
# 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}")

Parse job details from HTML

Fetch each job page and extract details using BeautifulSoup. All job content is server-side rendered, so no JavaScript execution is required.

Step 3: Parse job details from HTML
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']}")

Use the Atom feed for richer structured data

Homerun advertises a per-tenant Atom feed at feed.homerun.co, and it is the most reliable structured source. Each entry carries the native job ID, department, location, employment type, and salary indication that are awkward to parse out of the HTML.

Step 4: Use the Atom feed for richer structured data
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']}")

Handle rate limiting and errors

Implement respectful rate limiting and robust error handling when scraping multiple job pages. Add delays between requests and handle network failures gracefully.

Step 5: Handle rate limiting and errors
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")
Common issues
highProbing for a JSON or GraphQL API returns the homepage instead of job data

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.

highSitemap or feed returns 404 for an unknown company slug

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.

mediumNo master directory to enumerate every Homerun company

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.

mediumA tenant subdomain 301-redirects away to the company's own careers domain

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.

lowLanguage variants create duplicate job entries

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.

mediumAtom feed missing for a company that has a sitemap

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.

Best practices
  1. 1Prefer the Atom feed at feed.homerun.co for the richest structured fields, then fall back to sitemap + HTML
  2. 2Use sitemap.xml as your discovery backbone - one request enumerates every job URL
  3. 3Drive incremental refreshes from sitemap lastmod and feed updated timestamps instead of re-scraping everything
  4. 4Normalize away the trailing language code and deduplicate on the job slug to collapse locale variants
  5. 5Verify a company subdomain returns HTTP 200 before bulk scraping to avoid wasted 404s
  6. 6Keep request pacing modest (a few hundred ms between calls, low concurrency) to stay a polite crawler
Or skip the complexity

One endpoint. All Homerun jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=homerun" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

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.

99.9%API uptime
<200msAvg response
50M+Jobs processed