# Teamtailor API: Get Jobs From the jobs.json Feed

> Every Teamtailor career site has a public jobs.json feed: full job descriptions, 100 per page, no API key. How to find it, paginate and parse it.

Source: https://cleanjobdata.com/articles/how-to-scrape-teamtailor-job-listings
Published: 2026-09-19 · Updated: 2026-11-01

TL;DR: Every Teamtailor career site publishes a JSON Feed at /jobs.json with the full job content (content_html) and a schema.org _jobposting object for each item, 100 jobs per page. Follow next_url until it is empty, use the numeric _jobposting.identifier.value as the job ID, and expect no salary, employment type or remote flag in the feed.

To get a company's jobs from Teamtailor, fetch `https://{host}/jobs.json` and follow `next_url` until it's empty — no API key, and each item already includes the full description. Teamtailor is popular with European employers, and its career sites are among the easier ones to scrape. Each site publishes a JSON Feed at a predictable path, and every item already includes the full job description, so there's no second request per job and no API key.

That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a Teamtailor scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the [Jobs API docs](https://cleanjobdata.com/docs/api/jobs).

## At a Glance

| | |
|---|---|
| Endpoint | `GET https://{career-site-host}/jobs.json` |
| API key | None |
| Pagination | Follow `next_url` until it's empty, 100 jobs per page |
| Full description | In the feed (`content_html`) |
| Salary field | No |
| Remote | No flag |

## Public jobs.json Feed vs the Official Teamtailor API

Teamtailor's official API needs an API key created by an admin in the employer's Teamtailor account, so it's meant for integrations the employer sets up. The jobs.json feed is public on every Teamtailor career site and needs no key. For reading published jobs from companies you don't work with, the feed is the one to use.

## Finding the Career Site Host

A Teamtailor career site lives either on a Teamtailor subdomain, such as `acme.teamtailor.com` (some regions use a variant like `acme.na.teamtailor.com`), or on a custom domain such as `careers.acme.com` that Teamtailor serves for the company. Either way, the feed is on that same host, so what you need is the full hostname, not just a short name.

To find it, open the company's careers page and read the address. If it's a custom domain, the page source and network requests usually still show Teamtailor assets. There's no public directory of Teamtailor customers, so this only helps for companies you already know about.

## The Endpoint

```bash
curl -H "accept: application/json" "https://acme.teamtailor.com/jobs.json"
```

The response follows the JSON Feed format:

```json
{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "GIA Legacy Planning",
  "home_page_url": "https://gialegacyplanning.na.teamtailor.com/jobs",
  "feed_url": "https://gialegacyplanning.na.teamtailor.com/jobs.json",
  "next_url": "https://gialegacyplanning.na.teamtailor.com/jobs.json?page=2&per_page=100",
  "items": [ /* jobs */ ]
}
```

`title` is the company name, `items` is the list of jobs, and `next_url` is the link to the next page whenever there is one. Each item has `id`, `title`, `url`, `date_published`, `content_html` and a `_jobposting` object.

## Pagination

The feed returns 100 jobs per page. When there are more, the response includes a `next_url` such as `?page=2&per_page=100`. Request that URL, add its `items`, and keep going until a response has no `next_url`. You follow the links rather than computing offsets.

This matters on larger boards. One career site we checked had over 1,500 open jobs spread across 16 pages, so reading only the first response would miss more than 90% of them.

Because `content_html` is in the feed, a whole board costs only a handful of requests.

## Fetching Jobs (Node/TypeScript)

```ts
interface TeamtailorItem {
  id: string; // a UUID
  title: string;
  url: string;
  date_published?: string;
  content_html?: string;
  _jobposting?: {
    identifier?: { value?: string | number };
    validThrough?: string;
    hiringOrganization?: { name?: string };
    jobLocation?: Array<{
      address?: {
        addressLocality?: string | null;
        addressRegion?: string | null;
        addressCountry?: string | null;
      };
    }>;
  };
}

export async function fetchTeamtailorJobs(host: string) {
  let next: string | null = `https://${host}/jobs.json`;
  const items: TeamtailorItem[] = [];

  while (next) {
    const res = await fetch(next, { headers: { accept: "application/json" } });
    if (!res.ok) throw new Error(`Teamtailor feed ${res.status} for ${host}`);
    const feed = (await res.json()) as { items?: TeamtailorItem[]; next_url?: string };
    items.push(...(feed.items ?? []));
    next = feed.next_url ?? null;
  }

  return items.map((item) => {
    const jp = item._jobposting ?? {};
    const locations = (jp.jobLocation ?? []).map((l) => ({
      city: l.address?.addressLocality ?? null,
      region: l.address?.addressRegion ?? null,
      country: l.address?.addressCountry ?? null, // ISO alpha-2, e.g. "US"
    }));
    return {
      // Prefer the numeric schema.org identifier; the UUID is only a fallback.
      id: String(jp.identifier?.value ?? item.id),
      title: item.title,
      url: item.url,
      published: item.date_published,
      expires: jp.validThrough,
      company: jp.hiringOrganization?.name,
      locations,
      html: item.content_html ?? "",
    };
  });
}
```

## Fields Worth Knowing

- **`_jobposting.identifier.value`** is a numeric ID from the embedded schema.org data. It matches the number at the start of the job's URL slug (for example `/jobs/700014-…`), so it's the stable ID to use. `item.id` is a UUID, which is a different format entirely.
- **`title`**, **`url`** and **`content_html`** are what you'd expect: the job title, the public job page, and the description as HTML.
- **`date_published`** is a timestamp with a timezone offset, for example `2026-09-12T00:00:00-05:00`. The same value appears as `datePosted` inside `_jobposting`.
- **`_jobposting.validThrough`** is the date the posting is set to close, and it's present on almost every job. It's useful for hiding jobs that are about to expire.
- **`_jobposting.jobLocation`** is an array. Each entry has an `address` with `addressLocality`, `addressRegion` and `addressCountry`. The country is already an ISO alpha-2 code such as `GB` or `US`, which saves a lookup. Multi-location jobs have several entries, so don't read only the first one if all locations matter. `streetAddress`, `postalCode` and `addressRegion` are often `null` even when the city and country are filled in.
- **`_jobposting.hiringOrganization.name`** is the company name.

## Gotchas

**The two IDs differ.** If you fall back to the UUID whenever `_jobposting` is missing, the same job could get a different ID on a later run. Pick the numeric identifier as your source of truth and treat a missing one as something to log, not to silently paper over.

**Salary and employment type aren't in the feed.** Across the first 500 jobs of a large board, none of the `_jobposting` objects included `baseSalary` or `employmentType`. If a company mentions pay, it's inside the description text, so you'd have to extract it from `content_html` yourself.

**There's no remote or hybrid flag.** Look for "remote" or "hybrid" in the title, in the location city text, and in phrases like "fully remote" or "remote-first" in the description. Expect some misses and some false positives, since a description that mentions "hybrid" in passing can trip a naive check.

**The company website isn't in the feed.** If you need it, the career site's homepage usually links to it, along with social accounts, in the footer. Skip Teamtailor's own links and social domains when you read them.

**Custom domains should work, but check.** A custom domain serves the same `/jobs.json` path when Teamtailor hosts it. If one returns HTML instead of JSON, try the company's `teamtailor.com` subdomain.

**Old postings can stay open.** Compare `date_published` and `validThrough` with today's date before treating a job as fresh.

**No webhooks.** It's a pull-only feed. Poll on an interval and compare the set of IDs between runs to detect closed jobs.

## When This Is Enough

For jobs from one Teamtailor customer, the feed is about as simple as scraping gets: one endpoint, full descriptions, no authentication.

## When It Isn't

Across many employers you still deal with inconsistent locations, missing pay and free-text workplace hints, and each platform has its own shape. Compare this with [Greenhouse](https://cleanjobdata.com/articles/how-to-scrape-greenhouse-job-listings) and [Lever](https://cleanjobdata.com/articles/how-to-scrape-lever-job-listings).

CleanJobData reads Teamtailor feeds together with other platforms and returns one schema, with locations resolved to cities, states and countries using the country codes in the feed. It doesn't invent salary that the feed never carried. Try it in the [Playground](https://cleanjobdata.com/playground) or see the [API Documentation](https://cleanjobdata.com/docs).

## Skip the scraper: get this data from the CleanJobData API

If you'd rather not maintain a Teamtailor collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in `pagination.next_page`.

```bash
curl "https://api.cleanjobdata.com/jobs?title=engineer&remote=true&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"
```

Jobs from ATS-hosted career sites come back in the same schema as every other source, so there's no per-platform parsing on your side. Use `company_website_url` or `employer_id` to scope to one employer. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

- [Jobs API reference](https://cleanjobdata.com/docs/api/jobs) for every filter and response field
- [Playground](https://cleanjobdata.com/playground) to run queries without writing code
- [Pricing](https://cleanjobdata.com/pricing) — the Trial is free with no card required
