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.
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#
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"{
"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.
# 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:
| Element | Holds |
|---|---|
<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 <p> and friends. Decode & first and
you corrupt everything downstream — &lt; becomes < becomes <, turning
text the employer escaped into markup. & 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.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key