# EU Vacancy Feed — Search 50 European Job Boards at Once (`studio-amba/eu-vacancy-feed`) Actor

Search up to 50 European job boards in one run: general boards, national and regional public employment services, and eFinancialCareers. Deduplicates the same vacancy across boards, flags multi-board postings as a hiring-intensity signal, and parses language and location on every row.

- **URL**: https://apify.com/studio-amba/eu-vacancy-feed.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 result scrapeds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## EU Vacancy Feed

Search up to 50 European job boards at once and get back one deduplicated feed of vacancies. Give it a keyword, and it returns a merged list of jobs with company, location, language, salary (when published) and the date posted — with duplicate postings across boards collapsed into a single row.

This actor does not scrape a website itself. It runs our own job board scrapers in parallel — general boards, national and regional public employment services (PES), and pan-European sector boards — normalizes every result to one schema, and merges rows that are the same vacancy.

### Why use this actor?

Recruiters, sales teams doing account research, and labor-market analysts all care about the same signal: who is hiring for what, right now, across borders. Checking dozens of national job boards by hand is slow, and every board names its fields differently (`jobTitle` vs `itemTitle` vs `title`, `company` vs `employer`). This actor does the fan-out and the field mapping in one call, and adds something none of the individual boards give you: when the same vacancy shows up on more than one board, that's flagged directly on the row. A company posting the same role on 2-3 national boards simultaneously is pushing hard to fill it — useful as a hiring-intensity signal on its own, not just deduplication housekeeping.

Public employment service (PES) boards are included from the same run. Sites like France Travail, VDAB, Bundesagentur für Arbeit, and Werk.nl carry postings that never reach the big commercial boards — smaller employers, apprenticeships, and public-sector roles that don't pay for listings.

### What it returns

Each row is one deduplicated vacancy. `sourceBoards` lists every board it was found on; `postingCount` is the length of that list. A `postingCount` of 1 means it only appeared on one board. Real example from a live run against Jobs.ch and JobUp.ch — the same Swiss listing indexed on both:

```json
{
    "title": "Application Developer Java 80 - 100%",
    "company": "Fust | Swiss Household Services AG",
    "location": "Oberbüren",
    "country": "CH",
    "language": "de",
    "salary": null,
    "postedAt": "2026-07-15T08:25:35+02:00",
    "url": "https://www.jobs.ch/de/stellenangebote/detail/bea8b500-2c38-4338-9995-c02f124246f4/",
    "sourceBoard": "Jobs.ch",
    "sourceBoards": ["Jobs.ch", "JobUp.ch"],
    "postingCount": 2,
    "scrapedAt": "2026-08-16T07:16:40.611Z"
}
```

And a single-board example from a public employment service, France Travail:

```json
{
    "title": "Alternance Business Developer (H/F)",
    "company": "ORBO",
    "location": "59 - Lille",
    "country": "FR",
    "language": "fr",
    "salary": null,
    "postedAt": "Publié il y a 6 jours",
    "url": "https://candidat.francetravail.fr/offres/recherche/detail/...",
    "sourceBoard": "France Travail (France PES)",
    "sourceBoards": ["France Travail (France PES)"],
    "postingCount": 1,
    "scrapedAt": "2026-08-16T11:29:49.082Z"
}
```

### Use cases

- **Recruiters and staffing agencies** scanning multiple national markets for a role at once instead of checking each board separately, including the public boards commercial job-board scrapers miss.
- **Sales and account research** — a spike in vacancies at a company, or the same role posted on 3 boards, is a leading indicator of growth, funding, or restructuring worth knowing before a sales call.
- **Labor-market and workforce analysts** tracking hiring demand for a role or sector across the EU without stitching together dozens of different data sources, public and commercial.
- **Financial-sector recruiters and compliance teams** — eFinancialCareers is included as a dedicated board for banking, trading and fintech roles across the UK, Switzerland and Germany.
- **Job boards and HR platforms** backfilling international listings for a niche vertical without building 50 separate scrapers.

### How to scrape European job listings

1. Enter a **Keyword** — a job title or skill (e.g. `developer`, `accountant`, `nurse`). It's passed as-is to every selected board's own search, so a term that exists in the local language of that market works best (English tech terms like "developer" or "engineer" usually match across most boards too).
2. Leave **Countries** and **Boards** empty to run the default set: the original 13 general boards, every public employment service, and eFinancialCareers UK — 26 boards, tuned to stay fast and affordable.
3. To go wider: set **Countries** to pull every board for a market (e.g. `["DE"]` returns StepStone Germany, Jobware, XING Jobs and Bundesagentur für Arbeit together), or set **Boards** to pick exact boards from the full 50-board roster (sector boards like NHS Jobs, CharityJob, or jobs.ac.uk are opt-in this way).
4. Set **Max Per Board** to control how many raw vacancies each board contributes before deduplication.
5. Run the actor. All selected boards are queried in parallel via our own scraper actors, so adding boards doesn't multiply run time — a default 26-board run typically finishes in 1-3 minutes.
6. Open the dataset. Each row is one deduplicated vacancy. Sort or filter on `postingCount > 1` to see which vacancies are being pushed on multiple boards at once.
7. Check `RUN_SUMMARY` in the key-value store for per-run stats: boards queried, boards failed, raw count before dedup, and how many vacancies were multi-posted.

### Pre-configured Tasks

Run these directly, no input needed:

- [Developer jobs in Belgium, Ireland, Germany, and Switzerland](https://apify.com/studio-amba/eu-vacancy-feed/examples/developer-jobs-belgium-ireland-germany-switzerland)
- [IT jobs in Poland](https://apify.com/studio-amba/eu-vacancy-feed/examples/it-jobs-poland)
- [Nursing jobs in Germany and Austria](https://apify.com/studio-amba/eu-vacancy-feed/examples/nurse-jobs-germany-austria)

### Input

| Field | Type | Description |
|-------|------|-------------|
| `keyword` | string | Job title or keyword to search for across all selected boards. Defaults to `"developer"`. |
| `countries` | array | Filter which country boards to query — matches every board for that country (general + PES + sector). ISO2 codes: `DE`, `AT`, `NL`, `GB`, `FR`, `CH`, `PL`, `DK`, `ES`, `BE`, `IE`, `EU`, `LU`, `CZ`, `BG`, `PT`, `RO`, `SE`, `NO`. |
| `boards` | array | Pick exact boards by key, overriding `countries`. See the board roster table below for the full key list. |
| `maxPerBoard` | integer | Maximum vacancies to fetch from each board before deduplication (1-200, default 20). |
| `timeoutPerBoardSecs` | integer | Max seconds to wait for each board scraper before treating it as failed (30-600, default 120). |
| `proxyConfiguration` | object | Proxy settings passed to the underlying board scrapers. Leave default — each board otherwise falls back to its own known-good country-pinned residential proxy. |

Leaving both `countries` and `boards` empty runs the **default set of 26 boards**: the original general-board roster plus every PES plus eFinancialCareers UK. The other ~24 boards (additional general/sector boards) are reachable via `countries` or `boards`.

#### Example input

```json
{
    "keyword": "software engineer",
    "countries": ["DE", "FR", "GB", "CH"],
    "maxPerBoard": 20
}
```

#### Cost estimate (per result)

Each board queried triggers one child-actor run, billed on the caller's own Apify account like any other actor-to-actor call — so cost scales with the number of boards selected, not with results returned. The default 26-board set at `maxPerBoard: 20` runs in roughly 1-3 minutes and, based on live test runs, uses roughly the same compute per result as running any one of the underlying board scrapers directly. Widening `boards` to cover more of the full 50-board roster adds proportionally more child-actor runs; fan-out stays parallel (bounded by `timeoutPerBoardSecs`, not board count), so run time doesn't scale the same way cost does.

Usage cost only settles after the run reports **SUCCEEDED** — reading the dataset mid-run, before the run finishes, will undercount the actual cost of the boards still in flight.

### Output

| Field | Type | Description |
|-------|------|-------------|
| `title` | string | Job title of the vacancy |
| `company` | string | Hiring company, when the board exposes one |
| `location` | string | City or region, when the board exposes one. Parsed from a direct location field where the board has one, or composed from structured parts (city/canton/region) for boards that only expose those |
| `country` | string | ISO2 country code of the board (or the vacancy's own country for EURES) |
| `language` | string | ISO 639-1 language code of the posting, when the source board exposes it. Falls back to the board's known primary language when the source doesn't carry a per-item language field |
| `salary` | string | Salary as listed, raw text or a min–max range. Not every board publishes this |
| `postedAt` | string | Date posted, as reported by the source board (raw text, format varies) |
| `url` | string | Link to the vacancy on the first board it was found on |
| `sourceBoard` | string | First board this vacancy was found on |
| `sourceBoards` | array | Every board this vacancy was found posted on |
| `postingCount` | integer | Number of distinct boards — 2+ is a hiring-intensity signal |
| `scrapedAt` | string | ISO timestamp of collection |

### Board roster

Four layers per market: general boards, national or regional public employment services, pan-European sector boards, and a wide tail of niche/sector boards. `★` marks the 26 boards in the default set (no `countries`/`boards` filter).

#### General boards

| Key | Country | Board |
|-----|---------|-------|
| `stepstone-de` ★ | DE | StepStone Germany |
| `stepstone-at` ★ | AT | StepStone Austria |
| `stepstone-nl` ★ | NL | StepStone Netherlands |
| `stepstone-be` | BE | StepStone Belgium |
| `reed` ★ | GB | Reed |
| `totaljobs` | GB | Totaljobs |
| `jobsite` | GB | Jobsite |
| `nhs-jobs` | GB | NHS Jobs (healthcare sector) |
| `charityjob` | GB | CharityJob (nonprofit sector) |
| `jobs-ac-uk` | GB | jobs.ac.uk (academia sector) |
| `hellowork` ★ | FR | HelloWork |
| `cadremploi` | FR | Cadremploi (executive) |
| `jobteaser` | FR | JobTeaser (student/grad) |
| `jobs-ch` ★ | CH | Jobs.ch |
| `jobup-ch` ★ | CH | JobUp.ch |
| `jobscout24-ch` | CH | JobScout24.ch |
| `pracuj` ★ | PL | Pracuj.pl |
| `rocketjobs-pl` | PL | RocketJobs.pl (IT) |
| `nofluffjobs` | PL | No Fluff Jobs (IT) |
| `jobindex` ★ | DK | Jobindex |
| `infojobs` ★ | ES | InfoJobs |
| `jobat` ★ | BE | Jobat |
| `irishjobs` ★ | IE | IrishJobs |
| `moovijob` | LU | Moovijob |
| `jobs-lu` | LU | Jobs.lu |
| `jobs-cz` | CZ | Jobs.cz |
| `jobs-bg` | BG | Jobs.bg |
| `jobware` | DE | Jobware |
| `xing-jobs` | DE | XING Jobs |
| `willhaben-jobs` | AT | Willhaben Jobs |
| `werkzoeken` | NL | Werkzoeken.nl |
| `jobbird` | NL | Jobbird |
| `itjobs-pt` | PT | ITJobs.pt (IT) |
| `ejobs` | RO | eJobs.ro |
| `eures` ★ | EU | EURES (pan-European aggregator) |

#### Public employment services (PES)

National, or regional where the market is regionalized (Belgium: Brussels + Flanders).

| Key | Country | Board |
|-----|---------|-------|
| `actiris` ★ | BE | Actiris (Brussels) |
| `vdab` ★ | BE | VDAB (Flanders) |
| `arbeitsagentur` ★ | DE | Bundesagentur für Arbeit |
| `ams` ★ | AT | AMS |
| `werk-nl` ★ | NL | Werk.nl |
| `arbetsformedlingen` ★ | SE | Arbetsförmedlingen |
| `nav` ★ | NO | NAV |
| `sepe` ★ | ES | SEPE |
| `francetravail` ★ | FR | France Travail |
| `job-room-ch` ★ | CH | Job-Room |
| `jobnet` ★ | DK | Jobnet |
| `jobsireland` ★ | IE | JobsIreland |

#### Pan-European sector boards

| Key | Country | Board |
|-----|---------|-------|
| `efinancialcareers-gb` ★ | GB | eFinancialCareers (UK — highest-volume market) |
| `efinancialcareers-ch` | CH | eFinancialCareers (Switzerland) |
| `efinancialcareers-de` | DE | eFinancialCareers (Germany) |

### Deduplication logic

Vacancies are matched across boards by a fingerprint of company + title + location, normalized for:

- **Case** — "Software Engineer" and "SOFTWARE ENGINEER" match
- **Diacritics** — "Développeur" and "Developpeur" match
- **Common EU legal-entity suffixes** — "Acme GmbH", "Acme Gmbh", and "ACME" all normalize to the same company, likewise for SARL, BV, NV, Ltd, PLC, Srl, Sp z o.o., S.r.o., Oy, AB, Kft and a dozen others

When a fingerprint matches across two or more boards, the row keeps the first board's data as primary, fills in any missing salary/company/location/language/postedAt from the duplicate, and appends the new board to `sourceBoards`. A fingerprint match within the *same* board (e.g. pagination overlap) is silently deduplicated but does not count toward `postingCount`.

### FAQ

**Does this actor scrape the job boards itself?**
No. It calls our own board scrapers and merges their output. Each is also available separately on the Store.

**Why is `salary` often null?**
Most European job boards don't publish salary on the listing page — it's negotiated after contact. Boards that show it do so when the poster included it; several boards generally don't expose it at all.

**Why is `language` sometimes null?**
It's populated whenever the source board carries a per-item language field, and otherwise falls back to that board's known primary language. It's only null for a board whose items carry neither.

**Why did a board return 0 results?**
Job board anti-bot protection is intermittent — a board can 403 one run and work fine the next. A single failing board never blocks the run; check `boardsFailed` in `RUN_SUMMARY` to see which boards didn't come back that run.

**How often will I see `postingCount > 1`?**
Less often than you might expect for a broad keyword — the fingerprint requires an exact normalized match on title, company AND location, so two boards need to carry genuinely the same posting, not just a similar one. It shows up reliably for markets where two boards share an underlying platform (Jobs.ch/JobUp.ch) or where EURES re-indexes a national board's own listings.

**Can I run just one country or a handful of boards?**
Yes. Set `countries` to a single ISO2 code (e.g. `["FR"]` for every French board), or set `boards` to an exact list of board keys.

### Limitations

- `sourceBoards` and `postingCount` only capture postings within the boards actually queried in a given run — a vacancy posted on a board outside the selected set won't be detected as a duplicate.
- Salary, when present, is left as raw board text (currency and period vary by board) rather than normalized into a single numeric format.
- `postedAt` format varies by board: some report an ISO timestamp, others relative or free text as published on the site.
- Data is scraped from public job board listings and may change without notice.

### Related actors

Each board behind this feed also runs standalone if you need full-catalogue coverage of a single market:

- [StepStone Germany](https://apify.com/studio-amba/stepstone-de)
- [Reed](https://apify.com/studio-amba/reed-scraper)
- [France Travail](https://apify.com/studio-amba/francetravail-scraper)
- [eFinancialCareers](https://apify.com/studio-amba/efinancialcareers-scraper)
- [EURES](https://apify.com/studio-amba/eures-scraper)

We also run this as a managed service: scheduled runs, a fixed keyword/country list, and delivery to your inbox, Google Sheets, or API, maintenance included. See [studioamba.dev/services](https://studioamba.dev/services/) or email <hello@studioamba.dev> for a free data sample.

# Actor input Schema

## `keyword` (type: `string`):

Job title or keyword to search for across all selected boards (e.g. 'developer', 'accountant', 'nurse'). Use a common role name for the broadest match.

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

Filter which country boards to query — matches ALL boards for the given country (general boards, PES, sector boards). Leave empty (and leave Boards empty) to run the default set: the original 13 general boards, every public employment service, and eFinancialCareers UK.

## `boards` (type: `array`):

Pick exact boards to query by key, overriding Countries. Leave empty (with Countries also empty) to run the default set of 26 boards (general boards + every PES + eFinancialCareers UK). Selecting boards here lets you reach the other ~24 general/sector boards in the full 50-board roster — see README for the full list and what each key covers.

## `maxPerBoard` (type: `integer`):

Maximum number of vacancies to fetch from each board before deduplication. Higher values give more coverage but take longer and cost more. Each additional board queried adds roughly one child-actor run's worth of compute to the total run cost — keep this in mind when selecting a large number of boards.

## `timeoutPerBoardSecs` (type: `integer`):

Maximum time to wait for each underlying board scraper to finish. A slow board is skipped after this and reported as failed rather than blocking the whole run.

## `proxyConfiguration` (type: `object`):

Proxy settings passed to the underlying board scrapers. Leave default (Apify Residential) for reliable results — each board otherwise uses its own known-good country-pinned residential proxy.

## Actor input object example

```json
{
  "keyword": "developer",
  "countries": [
    "BE",
    "IE"
  ],
  "boards": [],
  "maxPerBoard": 5,
  "timeoutPerBoardSecs": 120,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (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 = {
    "keyword": "developer",
    "countries": [
        "BE",
        "IE"
    ],
    "boards": [],
    "maxPerBoard": 5,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/eu-vacancy-feed").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 = {
    "keyword": "developer",
    "countries": [
        "BE",
        "IE",
    ],
    "boards": [],
    "maxPerBoard": 5,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/eu-vacancy-feed").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 '{
  "keyword": "developer",
  "countries": [
    "BE",
    "IE"
  ],
  "boards": [],
  "maxPerBoard": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call studio-amba/eu-vacancy-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/eu-vacancy-feed"
        }
    }
}

```

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/oF9LeJX23EsT57Vgr/builds/EYgu0CXdU6C6wBDVt/openapi.json
