# Nordic ATS Jobs - Teamtailor, Recruitee, Personio, Workable (`nordic-data/nordic-ats-jobs`) Actor

Get every open job from company career sites on Teamtailor, Recruitee, Personio and Workable via their public feeds. Auto-detects the ATS, filters by keyword & location, only-new mode for alerts.

- **URL**: https://apify.com/nordic-data/nordic-ats-jobs.md
- **Developed by:** [Niklas Bergström](https://apify.com/nordic-data) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.40 / 1,000 job records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Nordic ATS Jobs - Teamtailor, Recruitee, Personio, Workable

Get **every open job from a company's own career site** - as clean, normalized JSON - for companies that recruit with **Teamtailor, Recruitee, Personio or Workable**, the applicant tracking systems most used by Nordic and European startups and scale-ups (Tibber, Polestar, Kahoot!, Channable, tado and thousands more).

Paste career-site URLs or slugs, the Actor **detects the ATS automatically**, reads the official public job feed, and returns one record per job with title, department, locations with country codes, remote/hybrid status, employment type, seniority, salary (when published), publish date, job and apply URLs, and the description as plain text or HTML.

Turn on **Only new since last run** and schedule it: you get (and pay for) only jobs you haven't seen before - a hiring-signal feed for sales, recruiting, VC dealflow and job boards.

### What can it do?

- **Hiring signals for sales / ABM** - know when target accounts open roles (e.g. a new "Head of Data" = buying signal).
- **Recruitment & sourcing** - monitor competitors' and clients' openings.
- **Job boards & aggregators** - fill a niche board with fresh jobs straight from the source (no duplicates).
- **Market research** - hiring velocity, departments, locations and salary bands across a portfolio.

### Supported sources

| ATS | Accepted input | Public endpoint used |
|---|---|---|
| Teamtailor | `https://acme.teamtailor.com`, custom domains like `https://jobs.tibber.com`, `teamtailor:acme` | Career-site RSS feed `/jobs.rss` (documented by Teamtailor for syndication) |
| Recruitee | `https://acme.recruitee.com`, custom domains, `recruitee:acme` | Careers Site API `/api/offers/` (falls back to the public XML job feed) |
| Personio | `https://acme.jobs.personio.de` (or `.com`), `personio:acme` | Public XML job feed `/xml` (Personio Recruiting API docs) |
| Workable | `https://apply.workable.com/acme`, `workable:acme` | Public job widget API |

Plain slugs (`tibber`) are tried against all four. Greenhouse, Lever, Ashby and SmartRecruiters are not covered (SmartRecruiters' robots.txt disallows automated access).

### Input

| Field | What it does | Example |
|---|---|---|
| `companies` | Career-site URLs, `ats:slug`, or plain slugs | `["https://tibber.teamtailor.com", "personio:tado"]` |
| `keywords` | Keep jobs whose title/department/tags contain any keyword | `["engineer", "data"]` |
| `searchInDescription` | Also match keywords in the description | `false` |
| `excludeKeywords` | Drop titles containing any of these | `["intern"]` |
| `locations` | Country codes, city/country names or `remote` | `["SE", "NO", "Helsinki"]` |
| `remoteOnly` | Only remote/hybrid jobs | `false` |
| `postedWithinDays` | Only jobs published in the last N days | `30` |
| `onlyNew` + `monitorId` | Monitoring mode (see below) | `true`, `"nordic-eng"` |
| `descriptionFormat` | `text`, `html`, `both`, `none` | `"text"` |
| `maxJobsPerCompany` / `maxResults` | Caps (newest first) | `50` / `0` |

```json
{
  "companies": ["https://tibber.teamtailor.com", "polestar", "recruitee:channable", "personio:tado", "workable:kahoot"],
  "keywords": ["engineer", "data"],
  "locations": ["SE", "NO", "remote"],
  "onlyNew": true,
  "monitorId": "nordic-engineering"
}
```

### Output

One item per job (dataset views: **Overview**, **Salaries**, **Errors**):

```json
{
  "jobId": "teamtailor:tibber:8527023",
  "ats": "teamtailor",
  "company": "tibber",
  "companyName": "Tibber",
  "title": "Data Engineer",
  "department": "House of Product & Engineering",
  "locations": [{"name": "Stockholm", "city": "Stockholm", "country": "Sweden", "countryCode": "SE", "zip": "111 52", "address": "Vattugatan 17"}],
  "location": "Stockholm, Sweden",
  "countryCodes": ["SE"],
  "workplaceType": "hybrid",
  "employmentType": null,
  "seniority": null,
  "publishedAt": "2026-10-09T17:02:39+02:00",
  "url": "https://jobs.tibber.com/jobs/8527023-data-engineer",
  "applyUrl": "https://jobs.tibber.com/jobs/8527023-data-engineer",
  "salary": null,
  "description": "We're Tibber - the energy company that wants you to use less energy. ...",
  "feedUrl": "https://tibber.teamtailor.com/jobs.rss",
  "fetchedAt": "2026-10-28T05:19:54+00:00"
}
```

Personio and Recruitee jobs often include `salary` (`min`, `max`, `currency`, `period`), `employmentType`, `seniority` and `tags`; extra source fields (`role`, `division`, `category`, `education`, `yearsOfExperience`, `occupation`, `industry`) are kept when available. Fields an ATS doesn't publish are `null`. `jobId` is stable across runs (`ats:company:id`).

Companies that can't be read produce a free error item (`error`: `not-found`, `unsupported`, `fetch-error`). Per-company stats (detected ATS, jobs in feed, jobs delivered) are saved as `RUN_STATS`.

### Only new since last run

The Actor stores delivered `jobId`s per `monitorId` in the named key-value store **`nordic-ats-jobs-state`** on your account (kept for 120 days after a job was last seen). Each run returns only jobs not delivered before - ideal for a daily schedule feeding Slack, a CRM, Google Sheets or a webhook. Delete the record to start over.

### Pricing

Pay per event - you pay only for jobs delivered to your dataset:

| Event | Price |
|---|---|
| Job record | see the Pricing tab |

Free: already-seen jobs in monitoring mode, jobs removed by your filters, and error items. Runs are light (one or a few HTTP requests per company, 256 MB).

**Spending limit:** set *Maximum cost per run*; the Actor checks the remaining budget before each job and stops cleanly when the next one would exceed it.

### Data source, robots.txt & fair use

Only public, no-login endpoints that the ATS vendors provide for publishing jobs (RSS/XML feeds, careers-site API, job widget) are used - the same data shown on the company's public career page. Requests are rate-limited (about 1 request/second per host, max 4 companies in parallel) with retries and backoff. robots.txt of all four sources allows these paths. Job descriptions are the employers' content: reuse them in line with your own legal requirements (e.g. link back to `url`, don't republish personal data of recruiters).

### FAQ

**How do I find a company's ATS?** Open their "Careers" page and look at the job links: `*.teamtailor.com`, `*.recruitee.com`, `*.jobs.personio.de`, `apply.workable.com/...`. Or just paste the career URL / company name and let the Actor detect it.

**A company's custom career domain isn't detected?** Custom domains work for Teamtailor and Recruitee. For Personio and Workable use the `ats:slug` form.

**Why do some jobs lack a salary?** The employer didn't publish one in the ATS.

# Changelog

This Actor's version history is a separate document: https://apify.com/nordic-data/nordic-ats-jobs/changelog.md

# Actor input Schema

## `companies` (type: `array`):

One entry per company. Accepts career-site URLs (https://tibber.teamtailor.com, https://jobs.channable.com, https://tado.jobs.personio.de, https://apply.workable.com/kahoot), prefixed slugs (teamtailor:tibber, recruitee:channable, personio:tado, workable:kahoot) or plain slugs (the ATS is auto-detected).

## `keywords` (type: `array`):

Keep jobs whose title, department or tags contain ANY of these (case-insensitive substring). Empty = all jobs.

## `searchInDescription` (type: `boolean`):

Match keywords against the full description too (broader results).

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these, e.g. intern, werkstudent.

## `locations` (type: `array`):

Keep jobs in ANY of these locations: 2-letter country codes (SE, NO, FI, DK), city or country names (Stockholm, Norway), or 'remote'. Empty = all locations.

## `remoteOnly` (type: `boolean`):

Keep only jobs marked remote or hybrid by the ATS.

## `postedWithinDays` (type: `integer`):

Keep only jobs published in the last N days. 0 = any date.

## `onlyNew` (type: `boolean`):

For scheduled runs: returns (and charges) only jobs not delivered in earlier runs. State is kept in the named key-value store 'nordic-ats-jobs-state' on your account.

## `monitorId` (type: `string`):

Optional name of the only-new state, e.g. 'nordic-engineering'. If empty, it is derived from companies + filters.

## `descriptionFormat` (type: `string`):

text = plain text, html = original HTML, both, or none (smallest output).

## `maxJobsPerCompany` (type: `integer`):

0 = no limit. Newest jobs first.

## `maxResults` (type: `integer`):

0 = no limit.

## `maxConcurrency` (type: `integer`):

How many career sites to read in parallel (each host is still rate-limited to ~1 request/second).

## `maxRetries` (type: `integer`):

Retries on network errors, HTTP 429 and 5xx (exponential backoff, honours Retry-After).

## Actor input object example

```json
{
  "companies": [
    "https://tibber.teamtailor.com",
    "recruitee:channable",
    "personio:tado",
    "workable:kahoot"
  ],
  "keywords": [
    "engineer",
    "data"
  ],
  "searchInDescription": false,
  "locations": [
    "SE",
    "NO",
    "Helsinki",
    "remote"
  ],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyNew": false,
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0,
  "maxResults": 0,
  "maxConcurrency": 4,
  "maxRetries": 3
}
```

# Actor output Schema

## `jobs` (type: `string`):

All job records from the default dataset in the Overview view (other views: Salaries, Errors).

## `runStats` (type: `string`):

Per-company detected ATS, jobs in feed and jobs delivered; already-seen skips, charged events and HTTP requests.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "https://tibber.teamtailor.com",
        "recruitee:channable",
        "personio:tado",
        "workable:kahoot"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nordic-data/nordic-ats-jobs").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "companies": [
        "https://tibber.teamtailor.com",
        "recruitee:channable",
        "personio:tado",
        "workable:kahoot",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("nordic-data/nordic-ats-jobs").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "https://tibber.teamtailor.com",
    "recruitee:channable",
    "personio:tado",
    "workable:kahoot"
  ]
}' |
apify call nordic-data/nordic-ats-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nordic-data/nordic-ats-jobs"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TMyMDqoZr10jXf6pP/builds/aZNOwAxdGxE5lmP56/openapi.json
