All platforms

Teamtailor Jobs API.

Pull complete job listings—full HTML descriptions, structured locations, departments, and remote status—straight from each company's public Teamtailor RSS feed, no API key required.

Get API access
Teamtailor
Live
80K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Teamtailor
Polestarbunny.net
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 Teamtailor.

Data fields
  • Full HTML Job Descriptions
  • Structured Locations (City, Country, Zip)
  • Department & Role Tags
  • Remote Status (On-site / Hybrid / Remote)
  • Publication Dates
  • Stable Job IDs & GUIDs
Use cases
  1. 01European Job Market Tracking
  2. 02Remote & Hybrid Role Aggregation
  3. 03Multi-Location Job Feeds
  4. 04Startup Hiring Signals
Trusted by
Polestarbunny.net
DIY GUIDE

How to scrape Teamtailor.

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

RESTbeginnerNo published limit; space requests ~200ms apart. RSS responses appear cached (~5 min).No auth

Build the RSS feed URL

Every Teamtailor board exposes its jobs at /jobs.rss. Build the feed URL by keeping the original scheme and host and appending the path—this preserves regional hosts (.na.teamtailor.com) and numeric-suffix subdomains instead of breaking them.

Step 1: Build the RSS feed URL
from urllib.parse import urlparse

def build_rss_url(board_url: str) -> str:
    """Build the Teamtailor RSS feed URL from any board or job URL.

    Keeps the scheme + host and appends /jobs.rss, so regional hosts like
    cardioone.na.teamtailor.com and numeric-suffix subdomains like
    inlight-1733094855.teamtailor.com all resolve correctly.
    """
    parsed = urlparse(board_url)
    return f"{parsed.scheme}://{parsed.netloc}/jobs.rss"

print(build_rss_url("https://polestar.teamtailor.com/jobs"))
# https://polestar.teamtailor.com/jobs.rss
print(build_rss_url("https://cardioone.na.teamtailor.com"))
# https://cardioone.na.teamtailor.com/jobs.rss

Fetch the RSS feed

Request the public RSS feed, which returns every job with full descriptions, locations, and departments in a single call. Confirm the response is really RSS before parsing.

Step 2: Fetch the RSS feed
import requests

board_url = "https://polestar.teamtailor.com"
rss_url = build_rss_url(board_url)

headers = {
    "Accept": "application/rss+xml, application/xml, text/xml",
    "User-Agent": "JobScraper/1.0",
}

response = requests.get(rss_url, headers=headers, timeout=30)
response.raise_for_status()
rss_content = response.text

if "<rss" not in rss_content:
    print("RSS not available for this board; fall back to the /jobs HTML page")
else:
    print(f"Fetched {len(rss_content)} bytes from {rss_url}")

Parse the RSS items

Parse the RSS 2.0 XML to pull each job's title, description, URL, publication date, remote status, and company. Teamtailor sometimes double-encodes description HTML, so decode entities twice when any remain.

Step 3: Parse the RSS items
import xml.etree.ElementTree as ET
from html import unescape
import re

def decode_description(html: str) -> str:
    """Decode HTML entities; Teamtailor occasionally double-encodes descriptions."""
    decoded = unescape(html or "")
    if any(entity in decoded for entity in ("&lt;", "&gt;", "&amp;")):
        decoded = unescape(decoded)
    return decoded

def parse_rss_feed(rss_content: str) -> list[dict]:
    """Parse a Teamtailor RSS feed into job dicts."""
    root = ET.fromstring(rss_content)
    channel = root.find("channel")
    if channel is None:
        return []

    jobs = []
    for item in channel.findall("item"):
        link = item.findtext("link", "")

        # Job ID lives in the URL: /jobs/{id}-{slug}
        id_match = re.search(r"/jobs/(\d+)", link)

        jobs.append({
            "id": id_match.group(1) if id_match else None,
            "title": (item.findtext("title") or "").strip(),
            "url": link,
            "description_html": decode_description(item.findtext("description", "")),
            "published_at": item.findtext("pubDate"),
            "remote_status": item.findtext("remoteStatus", "none"),
            "company": item.findtext("company_name", ""),
        })

    return jobs

jobs = parse_rss_feed(rss_content)
print(f"Found {len(jobs)} jobs")

Extract locations and departments from the tt: namespace

Teamtailor puts structured location and department data in a custom XML namespace (xmlns:tt="https://teamtailor.com/locations"). Prefer tt:name for each location and fall back to tt:city + tt:country when it is missing—mirroring the scraper's mapping.

Step 4: Extract locations and departments from the tt: namespace
TT_NS = {"tt": "https://teamtailor.com/locations"}

def extract_namespaced_data(item: ET.Element) -> dict:
    """Pull department and structured locations from the tt: namespace."""
    department = item.findtext("tt:department", default=None, namespaces=TT_NS)

    locations = []
    for loc in item.findall("tt:locations/tt:location", TT_NS):
        name = loc.findtext("tt:name", namespaces=TT_NS)
        if name and name.strip():
            locations.append(name.strip())
            continue

        city = (loc.findtext("tt:city", namespaces=TT_NS) or "").strip()
        country = (loc.findtext("tt:country", namespaces=TT_NS) or "").strip()
        combined = ", ".join(part for part in (city, country) if part)
        if combined:
            locations.append(combined)

    return {
        "department": department.strip() if department else None,
        "locations": locations,
    }

# Merge namespaced data back onto the parsed jobs (same item order).
root = ET.fromstring(rss_content)
channel = root.find("channel")
for job, item in zip(jobs, channel.findall("item")):
    extra = extract_namespaced_data(item)
    job["department"] = extra["department"]
    job["locations"] = extra["locations"]

Handle errors and pace requests

Wrap fetching with error handling that mirrors the scraper: 404 means the board is gone, while 403 and 429 both signal blocking or rate limiting. Back off and retry those, and pace requests when scanning many boards.

Step 5: Handle errors and pace requests
import time
from typing import Optional

def fetch_teamtailor_jobs(board_url: str, max_retries: int = 3) -> Optional[list[dict]]:
    """Fetch and parse all jobs for a Teamtailor board with error handling."""
    rss_url = build_rss_url(board_url)

    for attempt in range(max_retries):
        try:
            response = requests.get(
                rss_url,
                headers={
                    "Accept": "application/rss+xml, application/xml, text/xml",
                    "User-Agent": "JobScraper/1.0",
                },
                timeout=30,
            )
            response.raise_for_status()

            if "<rss" not in response.text:
                print(f"No RSS feed for {board_url}; try the /jobs HTML page")
                return None
            return parse_rss_feed(response.text)

        except requests.HTTPError as e:
            status = e.response.status_code
            if status == 404:
                print(f"Board not found: {board_url}")
                return None
            if status in (403, 429):  # scraper treats both as blocked / rate limited
                wait = 2 ** attempt
                print(f"Blocked or rate limited ({status}); waiting {wait}s")
                time.sleep(wait)
                continue
            print(f"HTTP error {status}: {e}")
            return None

        except requests.RequestException as e:
            print(f"Request failed: {e}")
            if attempt < max_retries - 1:
                time.sleep(1)

    return None

# Pace requests (~200ms) when scanning multiple boards.
boards = [
    "https://polestar.teamtailor.com",
    "https://cardioone.na.teamtailor.com",
]
for board in boards:
    jobs = fetch_teamtailor_jobs(board)
    if jobs:
        print(f"{board}: {len(jobs)} jobs")
    time.sleep(0.2)
Common issues
mediumThe RSS feed responds with an HTML error page instead of XML

The scraper only accepts a response whose root is <rss> with a <channel>; if it isn't, the board likely has RSS disabled. Fall back to parsing the job cards on the /jobs HTML page.

highThe /api/v1/jobs JSON endpoint returns 404

Teamtailor's per-tenant JSON API is not publicly accessible. Use the public /jobs.rss feed instead—it returns every job with full descriptions in a single request.

mediumA company board returns HTTP 404

The subdomain is wrong or the tenant no longer exists. Verify the live careers URL (some tenants use numeric-suffix subdomains like inlight-1733094855.teamtailor.com) and build the feed URL from the resolved host.

lowLocation and department fields come back empty

Locations and departments live in the tt: namespace (https://teamtailor.com/locations). Resolve tt:location/tt:name and tt:department with the full namespace URI, and fall back to tt:city + tt:country when tt:name is absent.

lowDescriptions contain literal &lt;p&gt; tags

Teamtailor sometimes double-encodes description HTML. Run html.unescape() once, then again if &lt;, &gt;, or &amp; still remain—matching the scraper's two-pass decode.

lowRegional or numeric-suffix hosts break a hard-coded feed URL

Don't rebuild the URL from a bare subdomain—regional tenants use hosts like cardioone.na.teamtailor.com. Keep the original scheme + host and append /jobs.rss.

mediumRequests start returning HTTP 403 or 429

The scraper classifies both as rate limiting or blocking. Slow down (the platform paces requests ~200ms apart), lower concurrency, and retry 429s with exponential backoff.

Best practices
  1. 1Source jobs from /jobs.rss—it returns every listing with full descriptions in one request.
  2. 2Build the feed URL from the board's scheme + host so regional (.na) and numeric-suffix subdomains keep working.
  3. 3Decode descriptions twice when entities remain—Teamtailor occasionally double-encodes HTML.
  4. 4Resolve the tt: namespace (https://teamtailor.com/locations) for structured location and department data.
  5. 5Extract the numeric job ID from /jobs/{id}-{slug} for stable deduplication.
  6. 6Pace requests (~200ms apart) and back off on 403/429; RSS responses appear cached for a few minutes.
Or skip the complexity

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

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

Access Teamtailor
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