# EuropeEnglishJobs (`pillowy_scraper_2pv/europeenglishjobs`) Actor

This scraper will help in finding jobs in Europe, specially in Germany and Switzerland

- **URL**: https://apify.com/pillowy\_scraper\_2pv/europeenglishjobs.md
- **Developed by:** [akaji Banpu](https://apify.com/pillowy_scraper_2pv) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## English-speaking Jobs — Germany & Switzerland

An Apify Actor that aggregates English-speaking job listings focused on
Germany and Switzerland, plus remote/work-from-anywhere jobs regardless of
country (Netherlands is still supported via `countries`, just no longer the
primary focus).

### Sources

**Free, direct API calls** (no key needed, no separate Actor cost, no known
legal/ToS issue with this kind of use):

- [Arbeitnow](https://www.arbeitnow.com/api/job-board-api)
- [Remotive](https://remotive.com/api/remote-jobs)
- [Jobicy](https://jobicy.com/api/v2/remote-jobs) — supports Germany, Switzerland, Netherlands via `geo=`. Does **not** use Jobicy's own `tag` param for keyword filtering — that param only accepts values from Jobicy's fixed taxonomy (e.g. "python"), and an arbitrary keyword like "QA" causes a 400 Bad Request. Filtering is done entirely client-side against the title instead, like the other sources.
- [RemoteOK](https://remoteok.com/api) — 100%-remote listings. **Its own API response embeds an explicit terms clause: "please link back... and mention Remote OK as a source, so we get traffic back from your site. If you do not we'll have to suspend API access."** If you build anything user-facing on top of this Actor's output, credit RemoteOK and link back to the job's URL on remoteok.com.
- [We Work Remotely](https://weworkremotely.com/remote-jobs.rss) — 100%-remote listings via the RSS feed the site itself publishes for external consumption.
- [Adzuna](https://developer.adzuna.com/) (optional — needs a free `adzunaAppId`/`adzunaAppKey`) — officially supports Germany (`de`), Switzerland (`ch`), and Netherlands (`nl`)
- [Himalayas](https://himalayas.app/jobs/api) — free public JSON API, no key, documented, supports a `country` filter
- [The Muse](https://www.themuse.com/api/public/jobs) — free without a key (500 req/hr); its own `location` query param didn't reliably narrow results in testing, so this fetches broadly and filters client-side like Arbeitnow
- [Jooble](https://jooble.org/api/about) (optional — needs a free `joobleApiKey` from a signup form) — legitimate free API meant for exactly this use, but the free tier is a **lifetime cap of 500 requests total** (not per-day), so it's opt-in and should be used sparingly

RemoteOK, We Work Remotely, Himalayas, and The Muse are all global,
English-by-default remote/job boards, so — unlike Arbeitnow, a German board
mixing German/English postings — their results are **not** run through the
English-language filter; nearly everything on them already qualifies, and
the filter would mostly produce false negatives.

**Keyword matching is title-only, not title+description.** Found via
testing: Remotive (via Lemon.io postings) and other sources reuse the same
generic marketing boilerplate paragraph across every job listing
regardless of actual role — a "QA" search matched "Senior Golang
Developer" and other unrelated roles because that shared boilerplate text
happened to mention "qa automation" once, deep in the description. None of
the upstream APIs' own keyword/search/tag params were found to reliably
filter either (Himalayas' `keyword` and Remotive's `search` silently
returned everything; Jobicy's `tag` outright 400s on non-taxonomy values) —
every free source's keyword filter is applied client-side, against the
title only, as the actual source of truth.

One official option investigated but not added: **EURES**, the EU/EFTA's
own job mobility network (Switzerland is a full member via a bilateral
agreement, confirmed). It has no sanctioned public API — only a
reverse-engineered, community-documented endpoint exists, which could
change or break without notice since it isn't actually meant for third-party
use. Worth revisiting if you want the reach of an official government
jobs database and are OK with that fragility.

**Composed from existing Apify Store Actors** (each is a separate,
pay-per-result Actor run):

- LinkedIn — default `curious_coder/linkedin-jobs-scraper`
- Indeed — default `borderline/indeed-scraper`
- Xing — default `fatihtahta/xing-jobs-scraper`
- StepStone — default `trev0n/stepstone-scraper` — covers Germany, Austria, Netherlands, and Belgium (only DE/NL are in our `countries` list; Switzerland is skipped, StepStone doesn't operate there). Filters for English-language ads directly via its own `adLanguage` param, so this source's results are more precisely English than the others in this section. **Verified against a real run** — `title`/`location`/`url`/`companyName` all confirmed correct.
- Robert Half — default `studio-amba/roberthalf-scraper` — a staffing agency covering Germany, Netherlands, and Switzerland (via its English-language `CH-en` locale). **Unverified** — every test attempt (from two different Apify accounts) was rejected pre-flight with "exceeds remaining usage," seemingly due to this Actor's own Store pricing configuration rather than the caller's resource request. Verify before relying on it.
- Relocate.me (visa-sponsored jobs) — default `khadinakbar/visa-sponsored-jobs-scraper` — international tech roles with explicit visa sponsorship and a full relocation package (visa services, flights, housing, language courses); covers Germany and Netherlands, not Switzerland. Every listing from this source is inherently visa-sponsored, so it's tagged as `"Relocate.me (Visa Sponsored)"` in the `source` field rather than adding a dedicated schema field for one source. **Verified against a real run** — note there's no single `location` field, it's split into separate `city`/`country` fields (handled in code).

#### Why compose existing Actors instead of scraping these sites directly

LinkedIn, Indeed, Xing, StepStone, Robert Half, and Relocate.me all prohibit
automated scraping in their Terms of Service and run active anti-bot
defenses (LinkedIn in particular has pursued legal action against
scrapers). Rather than writing bespoke scraping logic against those
defenses in this repo, `src/sources/{linkedin,indeed,xing,stepstone,
robert_half,visa_sponsored}.py` each call an existing, actively-maintained
Actor from the Apify Store over the Apify API (`Actor.call(...)`) and read
its output dataset. That plumbing (proxies, anti-bot handling,
markup-change maintenance) already lives in those Actors. You are still
bound by Apify's own platform terms and by whatever terms the upstream
Actor's author has agreed to — this doesn't remove the underlying legal
risk, it just avoids re-implementing scraping logic that the target sites
explicitly forbid. **These six sources are excluded from the default
`sources` list precisely because of that residual legal exposure** — the
free/direct-API sources above are the default, ToS-clean set.

You can swap in a different Actor for any of these via the
`linkedinActorId` / `indeedActorId` / `xingActorId` / `stepstoneActorId` /
`robertHalfActorId` / `visaSponsoredActorId` input fields.

Robert Half and Relocate.me both have small user counts on Apify (2 and 81
respectively) — lower track record than the others in this list, so treat
their output field names as even less certain until you've run a real test.

Every composed-Actor call passes explicit `memory_mbytes=512`/
`timeout_secs=180` (see `src/sources/apify_actor.py`) so cost stays bounded
regardless of the called Actor's own resource defaults — found necessary
during testing when one Actor's default allocation alone made Apify's
pre-flight cost estimate exceed a small account's remaining monthly free
credit, even before any real per-result charges applied. Each source is
also isolated in `main.py` (`_safe()` wrapper) so one failing composed
Actor — insufficient balance, the Actor being down, a bad input field —
can never discard the other sources' already-collected results; a failure
is logged and that source just comes back empty.

Two boards considered but not added: **Glassdoor** and **Monster** both have
usable Apify Store Actors, but weren't wired in this round — Monster in
particular describes itself as US-focused, a weak fit for Germany/Switzerland.
Add them the same way (a new `src/sources/<site>.py` following the
`stepstone.py` pattern) if you want them later.

Four expat/English-specific job sites were also investigated
(iamexpat.nl, undutchables.nl, thelocal.de/jobs, berlinstartupjobs.com) as
candidates for a custom scraper. Only undutchables.nl came back clear (no
scraping clause in its ToS, permissive robots.txt); the other three either
explicitly prohibit scraping in their terms or have no real job-listing
surface left to scrape. None are implemented yet — see git history for the
full per-site findings if you want to revisit undutchables.nl.

**English-language filtering for the Actor-composed sources:** LinkedIn,
Indeed, Xing, StepStone, and Robert Half are all run through
`looks_english_language` (`src/models.py`) — real language detection via
`langdetect` against each listing's description, not a keyword-presence
check. This distinction matters: an earlier version reused
`looks_english_speaking` (which only checks whether text *explicitly says*
"English", the right check for Arbeitnow where most postings are German
and we want the subset that calls out English-speaking teams) for these
sources too — but a normal English LinkedIn posting essentially never
contains the literal word "English", so that check would have wrongly
rejected good English listings, not just filtered German ones. Found via a
real test: a LinkedIn run with no keywords returned plainly German-only
postings ("Dachdecker (m/w/d) in Vollzeit gesucht!") completely unfiltered,
since LinkedIn has no server-side language param at all (unlike StepStone's
`adLanguage: "en"`, which is now double-checked with the same client-side
detector rather than trusted blindly — consistent with this project's
"verify server-side filters, don't trust them" pattern). Relocate.me is the
one exception, left unfiltered as an English-by-default international
board, same reasoning as RemoteOK/We Work Remotely/Himalayas/The Muse.
Detection needs a reasonably long description to be reliable (job titles
alone are too short — verified `langdetect` misclassifying "DevOps
Engineer" as Dutch and "QA" as Vietnamese), so text under 40 characters is
never filtered out rather than risk a false rejection.

### CV-based job scoring

Both `keywords` and CV scoring are entirely optional and independent — you
can search with neither (broad fetch), either one alone, or both together
(CV scoring applies on top of whatever `keywords`/`countries`/`sources`
already narrowed down to).

**If `keywords` is empty and a CV is provided, the CV's own top skills are
now used to drive the actual search** (`cvAutoKeywordCount`, default 5) —
not just to re-rank results afterward. This was a real, confirmed gap:
running with a CV but no keywords used to fetch each source's generic
default feed (LinkedIn/StepStone with no keyword returned a roofer, a
florist, "Product Owner" — nothing security/engineering-related) and rely
on `match_score` to sort that essentially-random set, producing near-zero
scores and irrelevant top results no matter how good the scoring math was.
The CV was never actually driving what got searched for. Now it is.

Provide a CV via `cvText` (paste plain text) or `cvFile` (upload a PDF,
DOCX, or plain-text file) and every job gets a `match_score` (0-100),
`matched_keywords`, and `cv_suggestions`, and the results are sorted
best-match-first.

Scoring is **free deterministic matching, not AI/semantic matching** — a
deliberate trade-off (see `src/cv_matching.py`):

1. Extract the ~40 most frequent non-stopword words from the CV text (a
   large, CV/job-posting-specific stopword list filters out both normal
   grammar words and generic resume boilerplate like "experience", "team",
   "senior", "led" — otherwise scores are dominated by words that appear in
   almost every job posting regardless of actual role).
2. For each job, extract its OWN significant terms the same way — title
   terms weighted higher than description terms (more central to what the
   role actually is). **Score = what fraction of the JOB's own terms the CV
   covers** — framed this way deliberately, not "what fraction of the CV
   the job uses": a CV naturally lists far more skills than any single job
   description mentions, so using the CV's full keyword set as the
   denominator was tried first and produced misleadingly low scores even
   for excellent matches (the best real match in testing scored 24/100).
   Scoring against the job's own requirements instead gives a number that
   means what people actually expect "match %" to mean.
3. A seniority-fit adjustment: years-of-experience extracted from the CV
   (regex for "N years... experience") is mapped to a 0-5 level (intern
   through director) and compared against the same scale detected from the
   job's title/description — a close fit adds a bonus, a big mismatch (e.g.
   a senior candidate vs. an internship) subtracts a penalty.
4. `cv_suggestions`: the job's own significant terms that don't appear
   anywhere in the CV, listed as "consider adding this if you genuinely
   have that experience" — an honest, computable stand-in for AI-generated
   ATS advice, not a claim of understanding what "ATS-friendly formatting"
   means beyond keyword coverage.

This won't catch synonyms ("k8s" vs "kubernetes") or truly reason about
experience the way an LLM-based matcher would — an explicit choice over
adding an AI API dependency/cost (asked directly, chose the free
deterministic version). Verified locally and live on the deployed Actor
against a sample security-analyst CV: "Senior Penetration Tester" scored
73, "Junior QA Tester" scored 14, "Florist" scored 0.

**Jobs requiring German language are always excluded entirely** (not just
scored lower), regardless of whether a CV is provided —
`models.requires_german` matches phrasings like "fluent in German",
"verhandlungssicheres Deutsch", "German C1", "Deutschkenntnisse
erforderlich" against title+description, deliberately excluding "nice to
have" framings ("German is a plus") so only genuinely mandatory
requirements are filtered. This is distinct from the English-language
detection elsewhere (`looks_english_language`) — a posting can be written
entirely in English and still require German as a stated prerequisite,
which written-language detection alone can't catch. Verified against 13
realistic phrasings (7 that should filter, 6 that shouldn't) before
deploying.

**Verification status of the two CV input paths:**

- `cvText` — fully verified end-to-end, live on the deployed Actor.
- `cvFile` — the extraction logic (PDF/DOCX/plain-text parsing, and reading
  an Apify key-value-store URL with the run's own authenticated
  credentials) is verified in isolation, but Apify's own input-schema docs
  don't actually specify what URL format the `fileupload` editor hands to
  the Actor at runtime ("up to the Actor developer to interpret"). A test
  against a manually-created key-value-store record correctly failed with
  a permissions error (the run is sandboxed to its own storages under
  `LIMITED_PERMISSIONS`) — consistent with the real Console upload widget
  likely storing the file in the run's *own* default store, which this
  code should then read correctly, but that specific path needs one real
  test: upload an actual file via the Console's Input tab and run it.

Note: the `description` field (used as scoring context) is only populated
for sources whose API/Actor actually exposes one — empty for others, so
scoring for those falls back to title-only.

### Security & Privacy

Three legitimate concerns worth addressing directly, with evidence rather
than just reassurance — check the referenced line numbers yourself.

**Does the CV (cvText/cvFile) get sent to LinkedIn/Indeed/Xing/StepStone/
Robert Half/Relocate.me, or anywhere else?** No, with one precise nuance.
`cv_matching.py` (regex + word counting, no network calls with the CV
content) is the only code that ever touches the CV text or file — none of
the six composed-Actor `fetch()` functions accept a CV parameter at all,
check their signatures in `src/sources/{linkedin,indeed,xing,stepstone,
robert_half,visa_sponsored}.py`, all `(actor, actor_id, keyword, countries,
max_items, log)`. The nuance: when `keywords` is empty and a CV is
provided, individual extracted keywords (single common words like
"python", "security" — never the CV text itself) are used as the search
`keyword` for those same calls, functionally identical to a user typing
those words into `keywords` by hand. The full CV text/file never reaches
any composed Actor under any configuration. Separately, the extracted
keywords (occasionally a name fragment) are written to the
run's own log via `Actor.log.info` in `main.py` — visible only to you as
the account owner in Apify Console, never sent anywhere else. Remove that
log line if you want it tighter.

**Do the Adzuna/Jooble API keys go anywhere besides Adzuna/Jooble?** No —
`app_id`/`app_key` are sent only as query params to `api.adzuna.com`
(`fetch_adzuna` in `free_apis.py`); `api_key` is used only in the URL path
for `jooble.org/api/{key}` (`fetch_jooble`). Never logged, never passed to
any composed Actor. Both are marked `isSecret: true` in the input schema,
so Apify encrypts and masks them.

**Can the `*ActorId` override fields be used to run arbitrary code with my
credentials?** This Actor declares `LIMITED_PERMISSIONS` (visible in every
run's log: `ACTOR: Running under "LIMITED_PERMISSIONS"`). Per
[Apify's own permissions docs](https://docs.apify.com/platform/actors/running/permissions):
a limited-permission Actor "can only call other Actors that also have
limited permissions," each called Actor "receives a restricted token" (not
a copy of your account's real credentials), and it "can't access any other
data in your Apify account" beyond its own storages and whatever's
explicitly passed to it. What we explicitly pass to a composed Actor is
just `keyword`/`country`/`maxItems` (see the `fetch()` signatures above) —
never your CV, never your Adzuna/Jooble keys. The real residual risk from
pointing an override at an untrusted Actor ID is narrower than credential
theft: it would run under your account's billing/budget (confirmed
directly during testing — a composed-Actor call failed with "you will
exceed your remaining usage" when the account's balance was too low), and
it would receive that same plain keyword/country/maxItems input. It cannot
reach your CV, your other API keys, or anything else in your account.

### Input

See [`.actor/input_schema.json`](.actor/input_schema.json). Key fields:

| Field | Description |
|---|---|
| `keywords` | Array of job title/keyword filters, e.g. `["QA", "security analyst"]`. Optional — leave empty to fetch broadly with no keyword filter, unless a CV is provided (see below). Each keyword is searched separately across every enabled source and results are merged/deduplicated. Matches against job **title only** (not description) — see below for why. `linkedin`/`indeed`/`xing`/`stepstone`/`roberthalf`/`visasponsored` each run once **per keyword**, so their cost multiplies with the number of keywords |
| `cvText` / `cvFile` | Optional. See "CV-based job scoring" above |
| `cvAutoKeywordCount` | Default `5` — when `keywords` is empty and a CV is provided, how many of the CV's top skills to actually search for instead of fetching each source's generic feed |
| `countries` | `["germany", "switzerland"]` by default; `"netherlands"` also supported |
| `remoteAnywhere` | Default `true` — also include jobs tagged Worldwide/Anywhere/Europe/EMEA/Flexible regardless of `countries`, from arbeitnow/remotive/remoteok/weworkremotely/himalayas/themuse |
| `sources` | Which sources to query. Default is the free, ToS-clean set: `arbeitnow`, `remotive`, `jobicy`, `remoteok`, `weworkremotely`, `himalayas`, `themuse`. `jooble` is free but opt-in (signup + 500-request lifetime cap). `linkedin`/`indeed`/`xing`/`stepstone` are opt-in and cost money per run — see the legal note above before enabling |
| `maxItemsPerSource` | Cap on results per paid Actor — keep this low while testing |
| `adzunaAppId` / `adzunaAppKey` | Only needed if `adzuna` is in `sources` |
| `joobleApiKey` | Only needed if `jooble` is in `sources` |
| `linkedinActorId` / `indeedActorId` / `xingActorId` / `stepstoneActorId` / `robertHalfActorId` / `visaSponsoredActorId` | Override which Store Actor to call |

### Local development

```bash
pip install -r requirements.txt
apify run
```

`apify run` needs the [Apify CLI](https://docs.apify.com/cli/) installed and
an `APIFY_TOKEN` configured (`apify login`) so it can call the LinkedIn/
Indeed/Xing/StepStone/Robert Half/Relocate.me sub-Actors if those sources
are enabled. The free sources (the default) work without a token.

Before enabling `linkedin`/`indeed`/`xing`/`stepstone`/`roberthalf`/
`visasponsored`: verify the output field names assumed in the corresponding
`src/sources/*.py` module (`pick(item, "title", ...)` etc.) against a small
test run of each upstream Actor in the Apify Console — Store search doesn't
expose output schemas, only input schemas, so those were written
defensively but unverified against live output.

### Deploy to Apify

```bash
apify login
apify push
```

# Actor input Schema

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

One or more job titles/keywords to search for, e.g. \['QA', 'security analyst', 'penetration tester']. Each keyword is searched separately across every enabled source and all results are merged and deduplicated. If left empty AND a CV is provided (cvText/cvFile), the top cvAutoKeywordCount keywords extracted from the CV are used to search automatically — without this, a CV-only run would just fetch each source's generic default feed and rely on match\_score to sort it after the fact, which produces near-useless results (confirmed: LinkedIn/StepStone with no keywords returned a roofer and a florist). Leave empty with no CV either to fetch broadly with no keyword filter at all. Matching is against job TITLE only (not description) — see README for why. Note: linkedin/indeed/xing/stepstone/roberthalf/visasponsored each run a paid Actor once PER keyword, so cost multiplies by the number of keywords for those sources.

## `cvText` (type: `string`):

Optional. Paste resume/CV text directly (e.g. from a chat) to score (0-100) and sort every job by how well it matches, and get per-job 'add this to your CV' suggestions — free deterministic scoring, not AI/semantic matching (see README). Score reflects what fraction of the JOB's own requirements the CV covers, plus a bonus/penalty for how well your detected years-of-experience fits the job's seniority level. If `keywords` is left empty, the CV's own top skills are also used to drive the actual search (see cvAutoKeywordCount) — otherwise the CV can only re-rank whatever generic feed the sources return. Jobs requiring German language are always excluded entirely, regardless of CV. Privacy: this text is processed entirely inside this Actor's own code and is never sent to LinkedIn/Indeed/Xing/StepStone/Robert Half/Relocate.me or any other third-party Actor — see README's Security section for how to verify this yourself in the source. Leave blank if uploading a file below, or if you just want to search without scoring.

## `cvFile` (type: `array`):

Optional. Upload a PDF, DOCX, or plain-text resume — used the same way as pasted CV text above (provide one or the other, or neither). Same privacy guarantee as cvText: never sent to any third-party Actor.

## `cvAutoKeywordCount` (type: `integer`):

Only used when `keywords` is empty and a CV is provided: how many of the CV's top extracted skills/keywords to actually search for (across all enabled sources), rather than just fetching each source's generic feed and scoring it after the fact. Higher = broader search but more paid-Actor cost if linkedin/indeed/xing/stepstone/roberthalf/visasponsored are enabled (each keyword runs once per those Actors).

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

Countries to search in. StepStone only supports germany (also Austria/Belgium, not modeled here); all others support all three.

## `remoteAnywhere` (type: `boolean`):

Also include jobs that don't name a specific country — tagged Worldwide/Anywhere/Europe/EMEA/Flexible — regardless of the `countries` filter above. Applies to arbeitnow, remotive, remoteok, weworkremotely, himalayas, and themuse.

## `sources` (type: `array`):

Which job sources to query. Direct-API sources (arbeitnow/remotive/jobicy/adzuna/remoteok/weworkremotely/himalayas/themuse/jooble) are free, no legal/ToS exposure — each either has no restriction on this kind of use, or (RemoteOK) an explicit API-terms-of-service permitting it with attribution. jooble needs a free signup key (joobleApiKey) and has a lifetime cap of 500 requests on its free tier, so it's opt-in. linkedin/indeed/xing/stepstone/roberthalf/visasponsored each run a separate paid Apify Store Actor composed against a site whose own Terms of Service prohibit scraping directly — see README before enabling those.

## `maxItemsPerSource` (type: `integer`):

Cap on how many results to request from each of linkedin/indeed/xing (these are pay-per-result Actors — keep this modest while testing).

## `adzunaAppId` (type: `string`):

Optional. Only needed if 'adzuna' is included in sources. Get a free key at https://developer.adzuna.com/ — sent only to Adzuna's own official API (api.adzuna.com), never anywhere else. Masked/encrypted by Apify (isSecret).

## `adzunaAppKey` (type: `string`):

Optional. Only needed if 'adzuna' is included in sources. Sent only to Adzuna's own official API (api.adzuna.com), never anywhere else. Masked/encrypted by Apify (isSecret).

## `joobleApiKey` (type: `string`):

Optional. Only needed if 'jooble' is included in sources. Get a free key (500-request lifetime cap) at https://jooble.org/api/about — sent only to Jooble's own official API (jooble.org), never anywhere else. Masked/encrypted by Apify (isSecret).

## `linkedinActorId` (type: `string`):

Apify Store Actor to call for LinkedIn. Default is a well-reviewed public Actor. SECURITY: only override this with an Actor ID you trust — the Actor you point this at runs under YOUR account's billing/budget (already-known risk: a run can fail if it would exceed your remaining balance). It does NOT receive your CV, API keys, or any other input field — only a plain keyword/country/max-results string, and this Actor's own LIMITED\_PERMISSIONS declaration means it can't reach anything else in your account either way (see README's Security section).

## `indeedActorId` (type: `string`):

Apify Store Actor to call for Indeed. SECURITY: same trust boundary as linkedinActorId above — only override with an Actor ID you trust.

## `xingActorId` (type: `string`):

Apify Store Actor to call for Xing. SECURITY: same trust boundary as linkedinActorId above — only override with an Actor ID you trust.

## `stepstoneActorId` (type: `string`):

Apify Store Actor to call for StepStone. Covers Germany, Austria, Netherlands, and Belgium (Austria/Belgium aren't in our countries list); Switzerland is skipped since StepStone doesn't operate there. Filters for English-language ads directly. SECURITY: same trust boundary as linkedinActorId above — only override with an Actor ID you trust.

## `robertHalfActorId` (type: `string`):

Apify Store Actor to call for Robert Half. Covers Germany, Netherlands, and Switzerland (via its English-language CH-en locale). SECURITY: same trust boundary as linkedinActorId above — only override with an Actor ID you trust.

## `visaSponsoredActorId` (type: `string`):

Apify Store Actor to call for Relocate.me visa-sponsored international tech jobs. Covers Germany and Netherlands; Relocate.me doesn't cover Switzerland. SECURITY: same trust boundary as linkedinActorId above — only override with an Actor ID you trust.

## Actor input object example

```json
{
  "keywords": [],
  "cvFile": [],
  "cvAutoKeywordCount": 5,
  "countries": [
    "germany",
    "switzerland"
  ],
  "remoteAnywhere": true,
  "sources": [
    "arbeitnow",
    "remotive",
    "jobicy",
    "remoteok",
    "weworkremotely",
    "himalayas",
    "themuse"
  ],
  "maxItemsPerSource": 25,
  "linkedinActorId": "curious_coder/linkedin-jobs-scraper",
  "indeedActorId": "borderline/indeed-scraper",
  "xingActorId": "fatihtahta/xing-jobs-scraper",
  "stepstoneActorId": "trev0n/stepstone-scraper",
  "robertHalfActorId": "studio-amba/roberthalf-scraper",
  "visaSponsoredActorId": "khadinakbar/visa-sponsored-jobs-scraper"
}
```

# Actor output Schema

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

Job listings from every enabled source, deduplicated by URL.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("pillowy_scraper_2pv/europeenglishjobs").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("pillowy_scraper_2pv/europeenglishjobs").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 '{}' |
apify call pillowy_scraper_2pv/europeenglishjobs --silent --output-dataset

```

## MCP server setup

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

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/zV2eOtsB5WyA2u1b7/builds/AugugFsaVjbixqwat/openapi.json
