# 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: https://hyperjobs.io/ats/teamtailor

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"
```

```json title="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:

| 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

<Warning title="`<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.
</Warning>

**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`](/docs/objects/job), and
the [company join](/docs/objects/company). We track 1,010 Teamtailor boards.

Teamtailor is in the fast refresh tier, attempted every cycle. See
[coverage](/docs/reference/coverage).

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
