# UK & EU Company Job Feeds (ATS Jobs API) (`dear_enstatite/job-feeds`) Actor

Pull live jobs from Greenhouse, Lever, Ashby, Workable, Teamtailor, Personio and Recruitee boards. UK/EU filters, salaries, remote flags and only-new-jobs mode.

- **URL**: https://apify.com/dear_enstatite/job-feeds.md
- **Developed by:** [Musson Tools](https://apify.com/dear_enstatite) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job returneds

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

## UK & EU Company Job Feeds: Greenhouse, Lever, Ashby, Workable, Teamtailor, Personio, Recruitee jobs API

Get **every open job straight from company career boards**. Give it a list of companies and you get **clean, normalised job data** in one schema, whatever applicant tracking system (ATS) each company uses: Greenhouse, Lever, Ashby, Workable, Teamtailor, Personio or Recruitee. Each job comes with its city and country, a remote flag, salary when published, posted and updated dates, the apply link and the description as Markdown and plain text. It's built for **UK and EU job data**. You can filter by country (UK+EU, DACH, Nordics and more), keywords, remote, employment type and date, and **incremental mode** returns only the jobs that are new since your last run.

- 🏢 **7 ATSs, one schema.** It reads Greenhouse, Lever (US and EU), Ashby, Workable, Teamtailor (including custom career domains), Personio (.de/.com) and Recruitee (including custom domains).
- 🔎 **Auto-detects the ATS.** Paste a board URL, the company's own careers page (for example `monzo.com/careers`), `ats:slug`, or just a slug.
- 🌍 **Parsed locations.** Each location has `city`, `region`, `country` and an ISO `country_code`, plus `remote`. `workplace_type` is remote, hybrid or onsite.
- 💷 **Salary when published.** It comes from the ATS's pay fields (Greenhouse pay transparency, Lever, Ashby, Recruitee) or, as a fallback, from a £/€/$ range in the description. The `salary.source` field tells you which.
- 🆕 **Incremental mode (`onlyNewJobs`).** Schedule it daily and you get (and pay for) only jobs you haven't seen before.
- 🧾 **Descriptions ready for LLMs.** You get Markdown and/or plain text. `content_is_untrusted: true` reminds AI pipelines to treat job text as data.
- 💸 **Failed boards are free.** A wrong slug, a removed board or an undetectable page is listed in the `SUMMARY` record with an error code, and you're never charged for it.
- 🤝 **Polite and compliant.** It only uses the official public job-board endpoints that these ATSs publish for embedding job listings. It needs no logins, no proxies and no scraping of HTML job pages. It respects robots.txt, including crawl-delay, spaces out requests and only fetches public internet addresses (SSRF-safe).

### Who it's for

- **Job boards and aggregators** building niche UK and EU feeds (tech, fintech, climate, remote-in-Europe).
- **Recruiters and agencies** watching target companies for new roles every morning.
- **Sales and market intelligence teams**, where hiring is a buying signal. You can track which companies are hiring for which teams, and where.
- **Job seekers and AI agents** that want a clean, deduplicated list of real openings straight from the source, with apply links.
- **Researchers** studying salaries, remote work and hiring trends in the UK and Europe.

### Supported ATSs and what to paste

| ATS | Board URL example | Or use |
| --- | --- | --- |
| Greenhouse | `https://job-boards.greenhouse.io/monzo` | `greenhouse:monzo` |
| Lever (US/EU) | `https://jobs.lever.co/palantir`, `https://jobs.eu.lever.co/…` | `lever:palantir` |
| Ashby | `https://jobs.ashbyhq.com/synthesia` | `ashby:synthesia` |
| Workable | `https://apply.workable.com/blueground/` | `workable:blueground` |
| Teamtailor | `https://tibber.teamtailor.com` or a custom domain such as `https://jobs.tibber.com` | `teamtailor:tibber` |
| Personio | `https://ottonova.jobs.personio.de` | `personio:ottonova` |
| Recruitee | `https://bunq.recruitee.com` or a custom domain such as `careers.bunq.com` | `recruitee:bunq` |

You can also paste a company's **own careers page**. The Actor finds the embedded or linked ATS board on it, for example `https://monzo.com/careers` → Greenhouse `monzo`. A **bare slug** such as `monzo` is tried against every ATS, and the board with jobs wins.

SmartRecruiters isn't supported, because its API host's robots.txt disallows automated access, and we respect that. Workday and other ATSs that have no public job-board API aren't supported either.

### Input

| Field | Default | Description |
| --- | --- | --- |
| `companies` | – | Board URLs, careers pages, `ats:slug` or slugs (up to 500). |
| `keywords` | – | Keep jobs whose title, department or team contains any of these (word prefix, so `engineer` matches `engineering`). |
| `excludeKeywords` | – | Drop matching titles, e.g. `intern`, `principal`. |
| `searchDescriptions` | false | Also match keywords against the description. |
| `departments` | – | Department or team contains any of these. |
| `countries` | – | ISO codes or names, or groups: `UK+EU`, `EU`, `EEA`, `Europe`, `DACH`, `Nordics`, `Benelux`. |
| `includeRegionalRemote` | true | With European countries selected, also keep remote jobs listed as "Europe", "EMEA" or "Worldwide". |
| `locations` | – | Location text contains, e.g. `London`, `Manchester`, `Berlin`. |
| `remoteOnly` | false | Remote jobs only. |
| `employmentTypes` | – | `full_time`, `part_time`, `contract`, `temporary`, `internship`, `apprenticeship`, `working_student`. |
| `postedSince` | – | `2026-10-01`, `24 hours`, `7 days`, `1 month`. |
| `onlyNewJobs` | false | Incremental mode (see below). |
| `stateKey` | auto | The name of the incremental memory, so you can keep separate searches apart. |
| `descriptionFormat` | `markdown_and_text` | `markdown_and_text`, `markdown`, `text` or `none`. |
| `maxJobsPerCompany` | 0 | 0 = no limit. Newest jobs come first. |
| `maxJobs` | 0 | 0 = no limit. You can also cap spend with the run's maximum cost. |
| `personioLanguage` | – | Language code (`en`, `de`…) for Personio feeds. |
| `maxConcurrency` | 4 | Boards fetched in parallel. |

```json
{
  "companies": [
    "https://monzo.com/careers",
    "https://jobs.lever.co/palantir",
    "ashby:synthesia",
    "https://tibber.teamtailor.com",
    "https://bunq.recruitee.com"
  ],
  "countries": ["UK+EU"],
  "keywords": ["engineer", "data"],
  "excludeKeywords": ["intern"],
  "postedSince": "14 days",
  "descriptionFormat": "markdown"
}
```

### Output

One dataset item per job, shown here shortened:

```json
{
  "company": "GoCardless",
  "company_slug": "gocardless",
  "ats": "greenhouse",
  "job_id": "8305812",
  "job_key": "greenhouse:gocardless:8305812",
  "title": "Product Manager",
  "department": "Product Development",
  "team": null,
  "locations": [
    { "raw": "Lisbon, Portugal", "city": "Lisbon", "region": null, "country": "Portugal", "country_code": "PT", "remote": false }
  ],
  "location_text": "Lisbon, Portugal",
  "remote": false,
  "workplace_type": null,
  "employment_type": null,
  "employment_type_raw": null,
  "seniority": null,
  "salary": { "min": 46400, "max": 69600, "currency": "EUR", "interval": "year", "interval_inferred": true, "text": "€46,400 - €69,600", "source": "description" },
  "posted_at": "2026-10-21T13:14:15Z",
  "updated_at": "2026-10-26T15:03:11Z",
  "job_url": "https://job-boards.greenhouse.io/gocardless/jobs/8305812",
  "apply_url": "https://job-boards.greenhouse.io/gocardless/jobs/8305812",
  "description_markdown": "## About us\n\nGoCardless is a global payments company…",
  "description_text": "About us\nGoCardless is a global payments company…",
  "fetched_at": "2026-10-26T16:58:04Z",
  "content_is_untrusted": true
}
```

- `employment_type` is normalised. `employment_type_raw` keeps the ATS's own wording.
- `workplace_type` and `employment_type` are `null` when the ATS doesn't publish them.
- `posted_at` and `updated_at` are UTC.
- Use `job_key` (`ats:slug:job_id`) as a stable ID for deduplication.

The dataset has three views. **Overview** shows one row per job. **Locations** shows one row per job location. **Descriptions** shows the job descriptions.

A **`SUMMARY`** record in the run's key-value store lists every input with:

- the ATS and slug detected
- jobs found, matched and returned
- an error code for any board that failed: `NOT_FOUND`, `ATS_NOT_DETECTED`, `URL_NOT_ALLOWED`, `ROBOTS_DISALLOWED`, `FETCH_FAILED`, `INVALID_FEED`, `RESPONSE_TOO_LARGE` or `INVALID_INPUT`

### Incremental mode: only new jobs

Turn on **`onlyNewJobs`** and schedule the Actor, for example daily.

- The **first run** returns all matching jobs. That's your baseline.
- **Later runs** return only jobs that weren't returned before under the same `stateKey`, and you **only pay for those new jobs**.
- Seen jobs are stored in your own named key-value store, `job-feeds-state`, which keeps up to 300,000 job keys.
- If a run stops early because of its budget, any jobs it didn't return stay "new" for the next run.

### Pricing (pay per event)

- **$0.0015 per job returned**, which is $1.50 per 1,000 jobs, after your filters.
- **Failed or empty boards cost nothing.** Jobs your filters drop, or that incremental mode skips, aren't charged.
- The run respects your **maximum cost per run** and stops cleanly when it's reached.

Example: 50 UK/EU tech companies with about 40 matching roles each, run daily in incremental mode. The baseline costs about $3, and a typical day after that (about 30 new jobs) costs about $0.05.

### Use it from code, agents and MCP

```bash
curl -X POST "https://api.apify.com/v2/acts/dear_enstatite~job-feeds/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companies":["https://monzo.com/careers"],"countries":["GB"],"descriptionFormat":"none"}'
```

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("dear_enstatite/job-feeds").call(run_input={
    "companies": ["greenhouse:monzo", "lever:palantir"], "countries": ["UK+EU"], "onlyNewJobs": True})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["company"], job["title"], job["location_text"], job["apply_url"])
```

**MCP (Claude, Cursor, ChatGPT and other AI agents):** add `https://mcp.apify.com/?tools=dear_enstatite/job-feeds` as an MCP server.

### Tips

- Not sure which ATS a company uses? Paste its careers page URL and check the `SUMMARY` record.
- For a "remote in Europe" feed, use `countries: ["Europe"]` and `remoteOnly: true`. Keep `includeRegionalRemote` on.
- Use `descriptionFormat: "none"` for quick, small monitoring runs, and full descriptions for LLM enrichment.
- Location parsing is rule-based and tuned for the UK and Europe. Always keep `location_text` for display.

### Limitations

- Only companies whose ATS has a public job-board feed are supported (see the table above). Career pages built entirely in JavaScript may not reveal their ATS. In that case, pass `ats:slug`.
- Teamtailor and Personio feeds don't include salary or employment details beyond what's published there. Workable's public widget has no salary.
- EU-hosted Greenhouse boards (`job-boards.eu.greenhouse.io`) aren't supported yet.
- Job data belongs to the companies that publish it. Use it in line with their terms and applicable law, such as the UK GDPR for any personal data in descriptions.

### Related

- [Any Document to Markdown for RAG](https://apify.com/dear_enstatite/doc-to-markdown): turn PDFs, DOCX, PPTX and web pages into Markdown chunks.
- [PDF for RAG](https://apify.com/dear_enstatite/pdf-for-rag): fast page-aware text extraction from PDFs.

# Actor input Schema

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

One per line. Accepts ATS board URLs (job-boards.greenhouse.io/monzo, jobs.lever.co/palantir, jobs.ashbyhq.com/synthesia, apply.workable.com/blueground, tibber.teamtailor.com, ottonova.jobs.personio.de, bunq.recruitee.com), a company careers page that embeds one of them (monzo.com/careers), "ats:slug" (greenhouse:monzo, lever:palantir, ashby:synthesia, workable:blueground, teamtailor:tibber, personio:ottonova, recruitee:bunq) or a bare slug (all ATSs are tried). Up to 500 per run.

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

Keep jobs whose title, department or team contains any of these (case-insensitive word prefix: "engineer" also matches "engineering"). Empty = all jobs.

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

Drop jobs whose title, department or team contains any of these (e.g. intern, senior).

## `searchDescriptions` (type: `boolean`):

Match keywords against the full job description as well as the title.

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

Keep jobs whose department or team contains any of these (e.g. Engineering, Data).

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

ISO codes or names (GB, UK, Germany, IE…) or groups: UK+EU, EU, EEA, Europe, DACH, Nordics, Benelux. A job matches if any of its locations is in one of these countries.

## `includeRegionalRemote` (type: `boolean`):

With a European country filter, also keep remote jobs whose location is a region such as Europe, EMEA or Worldwide.

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

Keep jobs whose location text, city, region or country contains any of these (e.g. London, Manchester, Berlin).

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

Keep only jobs flagged remote by the ATS or by their location text.

## `employmentTypes` (type: `array`):

Keep only these normalised employment types. Empty = all (including jobs with unknown type).

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

Absolute date (2026-10-01) or relative (24 hours, 7 days, 2 weeks, 1 month). Uses the posted date, or the updated date if the ATS has no posted date. Jobs without any date are kept.

## `onlyNewJobs` (type: `boolean`):

Remembers returned jobs in your named key-value store "job-feeds-state" and returns (and bills) only jobs that are new since the last run with the same state key. The first run returns everything as the baseline.

## `stateKey` (type: `string`):

Name of the incremental-mode memory. Leave empty to derive it from the companies list. Use different keys for different searches.

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

How to return the job description.

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

0 = no limit. Newest jobs first.

## `maxJobs` (type: `integer`):

0 = no limit. You can also cap spend with the run's maximum cost.

## `personioLanguage` (type: `string`):

Optional language code for Personio feeds (en, de, fr…). Empty = the company's default.

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

Requests to the same ATS host are still spaced out politely (and robots.txt crawl-delay is honoured).

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/gocardless",
    "https://tibber.teamtailor.com"
  ],
  "searchDescriptions": false,
  "includeRegionalRemote": true,
  "remoteOnly": false,
  "onlyNewJobs": false,
  "descriptionFormat": "markdown_and_text",
  "maxJobsPerCompany": 0,
  "maxJobs": 0,
  "maxConcurrency": 4
}
```

# Actor output Schema

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

All jobs on the Overview view: company, title, department, location, workplace type, employment type, salary, posted date, apply link.

## `locations` (type: `string`):

One row per parsed job location (city, region, country, ISO code, remote).

## `descriptions` (type: `string`):

Job descriptions as Markdown.

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

Per-board status (ATS detected, jobs found/matched/returned, error code), totals and incremental-state location.

# 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://job-boards.greenhouse.io/gocardless",
        "https://tibber.teamtailor.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dear_enstatite/job-feeds").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://job-boards.greenhouse.io/gocardless",
        "https://tibber.teamtailor.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dear_enstatite/job-feeds").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://job-boards.greenhouse.io/gocardless",
    "https://tibber.teamtailor.com"
  ]
}' |
apify call dear_enstatite/job-feeds --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dear_enstatite/job-feeds"
        }
    }
}
```

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/PyoPaSdcs3bGHYt3r/builds/LrHJPMpce7qvwQwIn/openapi.json
