All platforms

Teamtailor Jobs API

Query jobs from Teamtailor boards through one API — and how its RSS feed, tt: namespace and separate JSON feed for salary fit together.

?source=teamtailorsource reference

Teamtailor is an employer-branding platform with an ATS attached, popular with Nordic and European companies that care about how their careers page looks. Boards live at <slug>.teamtailor.com. There's no JSON API in the usual sense — the public route is an RSS feed, which sounds like a downgrade and mostly isn't: it carries full descriptions and a set of custom tt: namespaced elements with real structured metadata. What it doesn't carry is salary, and that lives in a second feed.

Try it#

BASH
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=teamtailor" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
Illustrative — the shape, not a live response
{
  "data": [
    {
      "id": "teamtailor:1847263",
      "title": "Backend Developer",
      "company": {
        "slug": "saltbox",
        "name": "Saltbox",
        "domain": "saltbox.se",
        "website": "https://saltbox.se",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Software Development",
        "employee_count": 120
      },
      "location": {
        "remote": false,
        "countries": ["Sweden"],
        "cities": ["Stockholm"],
        "regions": null,
        "raw": ["Stockholm, Sweden"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "Hybrid",
      "salary": {
        "min": 55000,
        "max": 70000,
        "currency": "SEK",
        "unit": "MONTH",
        "annual_min": 660000,
        "annual_max": 840000,
        "summary": null
      },
      "skills": ["go", "postgresql"],
      "taxonomies": ["Software"],
      "apply_url": "https://saltbox.teamtailor.com/jobs/1847263-backend-developer",
      "url": "https://saltbox.teamtailor.com/jobs/1847263-backend-developer",
      "source": "teamtailor",
      "posted_at": "2026-07-09T11:00:00.000Z",
      "expires_at": "2026-08-15T00:00:00.000Z",
      "updated_at": "2026-07-14T05:30:12.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}

How Teamtailor boards actually work#

Two feeds, and you need both.

BASH
# 1. RSS — descriptions, locations, department, remote status. Paginated.
curl "https://<slug>.teamtailor.com/jobs.rss?per_page=100&offset=0"
 
# 2. JSON Feed — structured salary and expiry. Paginated by page, not offset.
curl "https://<slug>.teamtailor.com/jobs.json?page=1"

Note the two feeds paginate differently: the RSS takes offset, the JSON feed takes page. That's not a typo in your code.

The tt: namespace is the good part#

Inside each <item>, Teamtailor ships custom elements a normal RSS reader would ignore entirely:

ElementHolds
<tt:department>Department
<tt:role>A finer grouping — team
<tt:location>A block, repeatable, with <tt:city> and <tt:country> inside
<remoteStatus>fully, hybrid, temporary, none

<remoteStatus> is a four-value enum, so hybrid survives — but temporary means temporarily-remote, which is not remote. We map fully to remote, none and temporary to not-remote, and let hybrid fall through to the location parser.

The job id is the numeric segment in /jobs/<id> from the <link>.

Salary is in the other feed, keyed by id#

The RSS has no salary. The JSON feed's items carry a _jobposting object — a schema.org JobPosting with a baseSalary MonetaryAmount and a validThrough. You merge the two feeds on the numeric id: identifier.value on the JSON side, the /jobs/<id> segment on the RSS side.

baseSalary.value is a MonetaryAmount, which means it's either a range (minValue / maxValue) or a single value, and you have to handle both. Its unitText is the period. Swedish and Norwegian boards quote monthly, so a min of 55000 is a normal salary, not an extraordinary one.

validThrough is where Teamtailor's expires_at comes from — one of the few platforms in this pool that declares expiry at all.

What bites you#

`<tt:location>` repeats, and reading the first one loses the rest

A multi-site job carries several <tt:location> blocks. Reading <tt:city> and <tt:country> off the item directly grabs one office and silently drops the others — measured at roughly 14% of multi-site jobs losing a location. You have to find the blocks first, then read the city and country inside each.

RSS entities are single-encoded, and you have to decode in the right order. The description comes back with &lt;p&gt; and friends. Decode &amp; first and you corrupt everything downstream — &amp;lt; becomes &lt; becomes <, turning text the employer escaped into markup. &amp; is decoded last.

<description> is CDATA-wrapped, sometimes. Not consistently. The parser has to unwrap CDATA when present and not choke when it's absent.

pubDate is RFC-822, not ISO. Wed, 09 Jul 2026 11:00:00 +0200. It parses, but it isn't the ISO string the rest of your pipeline expects.

The JSON feed can be absent or fail while the RSS works. Salary and expiry are best-effort here: a board whose JSON feed 404s still gives you every job from the RSS, just without pay. Treating the second fetch as mandatory turns a partial success into zero jobs.

Or use ours#

Teamtailor is two feeds, two pagination schemes, an XML namespace, an entity decode with an ordering trap, and a join on a numeric id you extract from a URL. None of it is hard. All of it is fiddly, and the failure modes are quiet — a dropped second office, a mis-decoded description, a monthly SEK band read as annual.

You get it merged: descriptions decoded, every <tt:location> kept, salary and validThrough joined in from the JSON feed, monthly bands normalised to comparable annual figures via annual_min/annual_max, and the company join. We track 1,010 Teamtailor boards.

Teamtailor is in the fast refresh tier, attempted every cycle. See coverage.

Quickstart · Pricing

Every source above normalises onto the same schema, with the hiring company already joined onto each posting.

Get an API key