# Personio, Recruitee & Teamtailor Jobs API (`belgair/european-ats-jobs`) Actor

Open jobs from company career sites on Personio, Recruitee and Teamtailor in one clean table: title, company, department, city, country, remote, contract, salary, skills and a link to the full ad. Filter by keyword, country, city and date, discover employers, or track new and removed jobs daily.

- **URL**: https://apify.com/belgair/european-ats-jobs.md
- **Developed by:** [Belgair Data](https://apify.com/belgair) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

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

## Personio, Recruitee & Teamtailor Jobs API

**Pay only for what changed.** Schedule this Actor daily in tracking mode and each run returns only the jobs that are new, changed or removed since the last run, so you never pay for a job that did not change (see the example calculation under Pricing). It reads company career sites on **Personio, Recruitee and Teamtailor** and gives one clean table: title, company, department, city, country, remote or not, contract, salary (when published), **skills**, language, date and the link to the full job ad. Paste a list of companies or **discover employers**, and filter by keyword, country, city, department, remote and date. **No API key, no login, no code.**

*The table does not contain the job text itself: read the full ad at its `url`. Job ads belong to the employers. Not affiliated with or endorsed by Personio, Recruitee (Tellent) or Teamtailor.*

### What can you use it for?

- **Job boards and newsletters.** Fill a niche board ("Python jobs in Germany", "remote roles at Dutch scale-ups") with fresh postings straight from the employers' own career sites, in one schema for all three systems, each linking to the original ad.
- **Recruiting and staffing agencies.** Watch the career sites of your target accounts and see every new opening the day it is published, with department, location and the skills it asks for.
- **Sales and hiring signals.** A company that opens five roles asking for Kubernetes and AWS is growing its platform team. Track your prospect list and act on hiring signals before your competitors do.
- **Labour-market research.** Count open roles by country, city, department, contract type or skill across European companies, with salaries where employers publish them.

### Example output

One row per job, the same columns for Personio, Recruitee and Teamtailor (real rows from test runs):

```json
[
    {
        "jobKey": "personio:bitcap:2825575",
        "ats": "personio",
        "jobId": "2825575",
        "title": "(Senior) Accountant / Buchhalter (m/w/d)",
        "company": "BIT Capital GmbH",
        "companySlug": "bitcap",
        "department": "Finance",
        "location": "Berlin HQ",
        "city": "Berlin",
        "country": "Germany",
        "countryCode": "DE",
        "remote": null,
        "contractType": "permanent, full-or-part-time",
        "salaryMin": null,
        "salaryMax": null,
        "salaryCurrency": null,
        "salaryPeriod": null,
        "skills": "DATEV, Excel, HGB",
        "language": "de",
        "publishedAt": "2026-10-02T16:03:20.000Z",
        "url": "https://bitcap.jobs.personio.de/job/2825575",
        "careersUrl": "https://bitcap.jobs.personio.de",
        "retrievedAt": "2026-10-17T12:12:39.317Z"
    },
    {
        "jobKey": "recruitee:attendi:2703191",
        "ats": "recruitee",
        "jobId": "2703191",
        "title": "Machine Learning Engineer",
        "company": "Attendi",
        "companySlug": "attendi",
        "department": "Product",
        "location": "Amsterdam, Netherlands",
        "city": "Amsterdam",
        "country": "Netherlands",
        "countryCode": "NL",
        "remote": "hybrid",
        "contractType": "full-time, fixed-term",
        "salaryMin": 6000,
        "salaryMax": 7000,
        "salaryCurrency": "EUR",
        "salaryPeriod": "month",
        "skills": "Python, Machine Learning, LLM, AWS, Azure, Google Cloud",
        "language": "en",
        "publishedAt": "2026-08-07T08:18:51.000Z",
        "url": "https://attendi.recruitee.com/o/machine-learning-engineer",
        "careersUrl": "https://attendi.recruitee.com",
        "retrievedAt": "2026-10-17T12:12:01.125Z"
    }
]
```

| Field | What it is |
| --- | --- |
| `jobKey` | Stable unique key: system, company and job ID |
| `ats` | `personio`, `recruitee` or `teamtailor` |
| `title`, `department` | As the career site shows them |
| `company` | As the career site shows it; when a career site shows no company name, a readable form of its address (`peter-park` becomes "Peter Park") |
| `location`, `city`, `country`, `countryCode` | All locations as written by the employer, plus the first city and the country (ISO code). For Personio, where the feed names only the office ("Berlin HQ"), city and country come from the public job page |
| `remote` | `remote`, `hybrid` or `onsite` as the employer states it; empty when the feed does not say |
| `contractType` | Contract and schedule, e.g. `permanent, full-time` |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | Only when the employer publishes a salary; the period only when the employer states it |
| `skills` | Skills from a fixed list of more than 110 (programming languages, frameworks, data and AI, databases, cloud and DevOps, methods, tools such as SAP, DATEV, Salesforce or Excel) found in the job title and text, comma-separated. Ambiguous names (Go, R, Swift, Excel, React) count only in a clear context, so "react quickly" or "excel in" never become a skill |
| `language` | Language of the job text (`de`, `en`, `nl`, `fr`, ...); empty when it is unclear |
| `publishedAt` | Publication date |
| `url` | The job ad on the career site: **read the full text there** |
| `careersUrl` | The company's career site |

**Why no job text?** Employers sometimes name colleagues in their ads ("you report to …"), and this Actor never stores names of people. We read the text only to apply your keyword filter and to find skills and language, then drop it. The full ad is one click away at `url`.

What each system publishes differs: salaries come from Recruitee, contract types from Personio and Recruitee. A Personio feed carries each job's text only in the language it was published in, so for jobs published in another language we also read the English or German feed (for your keyword filter and the skills). A field the system does not publish stays empty (`null`); we never guess.

You also get two records in the run's storage: **COMPANIES** (one row per line you entered: jobs found, no matching jobs, not found on any of the three systems, not checked because the run stopped early, an unreadable line, a duplicate line, or an error) and **RUN_SUMMARY** (counts, jobs left out by each filter, errors by cause).

### Discover companies

Don't have a list? Choose one or more systems under **Discover companies** and the Actor searches its built-in list of **186 companies with open jobs, most of them European** (October 2026: 54 on Personio, 39 on Recruitee, 93 on Teamtailor). These are employers with open jobs that we found through job posts on Hacker News, through links on companies' own websites, or by name, each checked against the job feed of its system (test accounts and stale feeds are left out); the company name in the output comes from the employer's career site. The list is refreshed regularly, and in tracking mode a company that joins the list starts with its own baseline, so you are never charged for its existing jobs. It does not contain every company on these systems, and some also hire outside Europe (use the country filter). Combine it with filters, for example keyword `Python` and country `Germany`. You can add your own companies in the same run. Reading the whole list across all three systems takes about 5 to 8 minutes (2 to 3 seconds per career site: one feed per second, plus robots.txt and, for Personio, the office address), within the default 10-minute timeout; you can also narrow it to one system or a country.

### Schedule it daily: only what is new

Turn on **Only new, changed or removed since the last run** and the Actor remembers what it has already shown you, per combination of companies and filters:

- **First run:** saves a baseline of all matching jobs. Nothing is delivered and nothing is charged.
- **Every next run:** returns only jobs that are **new**, **changed** (title, department, location, remote, contract, salary, skills or language) or **removed** (taken offline). Each row has `changeType` (`new`, `changed` or `removed`) and `changeDetectedAt`; changed rows also list `changedFields` and `previousValues`.
- **No changes:** the run finishes with an empty dataset and costs only the small start fee. You pay per delivered change, never for an unchanged job.

How to schedule it in Apify:

1. Fill in the input, turn on **Only new, changed or removed since the last run** and click **Save as a new task**.
2. Open **Schedules**, click **Create new**, pick a time (for example every day at 07:00) and add your task.
3. Optional: on the task's **Integrations** tab, send each run's results to email, Slack, Google Sheets or a webhook.

### Pricing

**Example calculation (tracking mode versus fetching everything):** you follow 50 companies with about 500 open jobs between them. Assume about 20 new jobs appear per working day (about 440 a month), about as many close (about 440), and about 200 change (a new salary, location or title).

| | Results per month | Cost at $1.50 per 1,000 |
| --- | --- | --- |
| **Tracking mode** (first run saves a free baseline; then only new, closed or changed jobs: about 440 + 440 + 200) | about 1,100 | about $1.65 |
| Fetching all open jobs every day (500 × 30 days) | about 15,000 | about $22.50 |

Your own numbers depend on how often your companies post and close jobs; the run's start fee comes on top in both cases.

**$1.50 per 1,000 jobs** ($0.0015 per job), plus the standard Apify start fee. You pay only for jobs saved to your dataset: jobs outside your filters, open applications ("Initiativbewerbung", "Open application", "Spontanansökan", talent pools), companies that are not found and failed requests are free. The same job is never saved twice in one run, even if two of your lines point to the same career site. In tracking mode you pay only for new, changed or removed jobs; a run without changes costs only the start fee. Set a maximum cost per run in Apify and the Actor stops cleanly when it is reached.

### Reliability

- **Public job feeds only.** Personio's career-page XML feed, Recruitee's Careers Site API and Teamtailor's career-site RSS feed: published by the systems for exactly this use. No login, no browser, no proxy.
- **Polite:** one request per second, robots.txt respected for every career site, retries with back-off, and it waits when a feed asks it to slow down.
- **One company failing never stops the others.** Every problem is counted by cause (proxy, rate limit, blocked, network, other) in `RUN_SUMMARY`, and every line you entered gets a status in `COMPANIES`.
- **No fake results:** a page that is not a real job feed (for example a maintenance page) is never saved or charged.
- **How many companies per run:** with the default 10-minute timeout, one run reads about 200 to 300 career sites given as careers URLs (2 to 3 seconds each), or about 70 to 100 slugs or domains (each is tried on all three systems). Companies the run did not reach are listed as not checked; for longer lists, raise the run timeout or split the list over several runs.

### Privacy

The output contains company job data only: no job text, no recruiters, contact persons or application mailboxes. The job text is read only to filter and to find skills and language, and is never stored.

### Input

| Field | What it does |
| --- | --- |
| **Companies** | One per line: a careers URL (`https://acme.jobs.personio.de`, `acme.recruitee.com`, `acme.teamtailor.com`), a company slug (`acme`) or a domain (`acme.com`). A slug or domain is tried on all three systems. For a Teamtailor career site on its own domain, write `teamtailor:careers.acme.com`. A line that cannot be read is skipped and listed in COMPANIES. |
| **Discover companies** | Also search the built-in list for Personio, Recruitee and/or Teamtailor. |
| **Keywords** | Keep jobs whose title, department or job text mentions at least one word or phrase (whole words: `go` does not match `good`). |
| **Search keywords in** | Title, department and job text (default), or the job title only. |
| **Countries** | Names (`Germany`, `Deutschland`) or codes (`DE`). |
| **Cities** | Location contains one of these cities. |
| **Departments** | Department contains one of these words. |
| **Remote** | Any, remote only, remote or hybrid, hybrid only, on-site only. |
| **Posted since** | A date (`2026-09-01`) or a period (`14 days`). |
| **Only new, changed or removed since the last run** | Tracking mode (see above). |
| **Maximum results** | Stop after this many jobs (in tracking mode: changes). Default 100. |

### FAQ

**Which companies can I use?** Any company whose career site runs on Personio, Recruitee or Teamtailor. If you are not sure which system a company uses, enter its name or domain: the Actor tries all three and the COMPANIES record tells you where it was found.

**Can I get the full job description?** Not in the table: open the job at its `url`. The keyword filter still searches the full text, so "Python jobs" finds jobs that mention Python only in the description.

**Why does a Personio job have no country?** Personio feeds name only the office ("Berlin HQ"). We look up city and country on the public job page, once per office (up to 10 offices per company); if that page has no address, the country stays empty and a country filter leaves the job out.

**Can I track more companies than fit in one run?** Raise the run timeout, or split the list over several runs or tasks (up to 500 lines per run).

# Actor input Schema

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

One company per line: a careers URL (https://acme.jobs.personio.de, acme.recruitee.com, acme.teamtailor.com), a company slug (acme) or a domain (acme.com). A slug or domain is tried on all three systems. For a Teamtailor career site on its own domain, write teamtailor:careers.acme.com. With the default 10-minute timeout one run reads about 200 to 300 careers URLs, or about 70 to 100 slugs or domains (each is tried three times); companies not reached are listed as not checked. For longer lists, raise the run timeout or split the list. A line that cannot be read is skipped.

## `discover` (type: `array`):

Also search the built-in list of employers with open jobs on these systems (most of them European; found through job posts on Hacker News, links on their own websites or by name, each checked against the job feed). Combine with filters, for example keyword Python and country Germany. The list does not contain every company on these systems.

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

Keep jobs whose title, department or job text mentions at least one of these words or phrases (whole words: "go" does not match "good"). The job text is only searched, never returned. Empty = all jobs.

## `searchIn` (type: `string`):

Where the keywords must appear.

## `countries` (type: `array`):

Keep jobs in these countries: a name (Germany, Deutschland) or a code (DE). Personio feeds name only the office, so for Personio jobs the country is looked up once per office on the public job page; a job whose page gives no address is left out.

## `cities` (type: `array`):

Keep jobs whose location contains one of these cities (Berlin, München, Amsterdam).

## `departments` (type: `array`):

Keep jobs whose department contains one of these words (Engineering, Sales).

## `remote` (type: `string`):

Work model as the employer states it. Jobs without this information only appear with "Any".

## `postedSince` (type: `string`):

Keep jobs published on or after this date, or within a period such as 14 days.

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

Return only jobs that are new, changed (title, department, location, remote, contract, salary, skills or language) or removed since the previous run with the same companies and filters. The first run saves a baseline and returns nothing. Schedule it daily; you only pay for the changes.

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

Stop after this many jobs (in tracking mode: changes).

## Actor input object example

```json
{
  "companies": [
    "https://bitcap.jobs.personio.de",
    "https://attendi.recruitee.com",
    "https://bunnynet.teamtailor.com"
  ],
  "searchIn": "titleAndDescription",
  "remote": "any",
  "onlyNew": false,
  "maxResults": 20
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `companies` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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://bitcap.jobs.personio.de",
        "https://attendi.recruitee.com",
        "https://bunnynet.teamtailor.com"
    ],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("belgair/european-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://bitcap.jobs.personio.de",
        "https://attendi.recruitee.com",
        "https://bunnynet.teamtailor.com",
    ],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("belgair/european-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://bitcap.jobs.personio.de",
    "https://attendi.recruitee.com",
    "https://bunnynet.teamtailor.com"
  ],
  "maxResults": 20
}' |
apify call belgair/european-ats-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,belgair/european-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/fXuyBilxc0a4OvsYf/builds/1ycwB9RQuvIdRhFkR/openapi.json
