# Big Four Careers Scraper - Deloitte, PwC, EY, KPMG & More (`studio-amba/big-four-careers-scraper`) Actor

Pull every open role at the Big Four and adjacent professional-services firms across Europe, verified from their own career pages, not a job board index. Covers Deloitte, PwC, EY, KPMG and more, with real location, language and ATS platform per role. Diff mode reports new/removed roles.

- **URL**: https://apify.com/studio-amba/big-four-careers-scraper.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (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.20 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## Big Four Careers Scraper — Verified Live Roles from Deloitte, PwC, EY, KPMG & More

Every open role at the Big Four and adjacent professional-services firms across Europe, verified from their own career pages, with a daily appeared/disappeared diff. Covers Deloitte, PwC, EY, KPMG, BDO, Forvis Mazars, Grant Thornton, RSM and a supporting roster of banks, insurers and asset managers across the EU/EEA, UK and Switzerland.

### What you get

- **~2,500 live roles per run**, pulled fresh from each employer's own careers system, not re-indexed from a job board.
- **18 firms, 24 European countries, 6 ATS platforms** in a single run. Deloitte, PwC, EY, KPMG, BDO, Forvis Mazars, Grant Thornton, RSM and more.
- **Verified live.** Every role is confirmed open on the employer's own page at run time, with the real location, real posting language, and the ATS platform it came from.
- **Daily diff mode.** Put it on a schedule and get only the roles that appeared or disappeared since your last run. A hiring-signal feed by employer.
- **~90 seconds per full run.** Pay per result, no subscription. Narrow by country or employer to pay for only what you need.

### Why use this actor?

Job boards re-index what a firm posts, on their own schedule, with their own taxonomy. This actor reads each employer's Applicant Tracking System (ATS) directly, so every row carries what a board can't reliably give you: the real location (parsed from the ATS, not a board's regional feed), the real posting language, confirmation the role is live on the employer's own site right now, and which ATS platform the employer runs (Workday, SuccessFactors, Avature, Radancy, SmartRecruiters, Greenhouse and others).

A companion **diff mode** turns the same pull into a hiring-signal feed: run it on a schedule and it reports only which roles newly appeared or disappeared since the last run, instead of re-exporting the whole list every time.

Built on a proven ATS-detection mechanism (see `docs/strategy/eu-employer-jobs-poc-findings-2026-08-16.md` in the source repo), then widened in two stages: first to a ~32-employer BE/LU/CH/NL/FR finance/tax roster, then rescoped on 2026-08-16 to the Big Four and adjacent professional-services firms across all of Europe — see `docs/strategy/big-four-adjacent-board-map-2026-08-16.md` for the full board-mapping research behind this roster (which firms run a single pan-European ATS tenant vs a separate board per country, and why).

### How to scrape Big Four careers data from Deloitte, PwC, EY and KPMG

1. Leave the input empty (`{}`) to run the full built-in roster — Deloitte, PwC, EY, KPMG, BDO, Forvis Mazars and more across Europe.
2. Narrow it with `countries` (e.g. only `DE`) or `employerNames` (e.g. `["Deloitte", "PwC"]`) to focus a run. The country filter works at the ROLE level: pan-European boards are pulled and filtered down, so a `countries: ["IE"]` run returns Deloitte Ireland plus PwC's and EY's Irish roles, not PwC's whole pan-EU board.
3. Add your own employers with `customEmployers` — just a name, country and careers-page URL. The same ATS-detection pipeline runs against them automatically; no per-employer scraper code needed.
4. Switch `mode` to `diff` and put the actor on a schedule (daily is plenty — corporate careers pages don't churn roles faster than that) to get an appeared/disappeared feed instead of a full re-export every run.
5. Read `locationCountry`, `locationCity`, `employerAtsPlatform` and `verifiedLiveAt` on every row — that's the ATS-sourced data a job board can't give you.

### Single-tenant multipliers — one board, many countries

Several firms in this roster run ONE shared ATS tenant across multiple countries rather than a separate board per market — the highest-leverage entries in the roster:

- **PwC** — one shared Workday tenant (`pwc.wd3.myworkdayjobs.com`) serves Ireland, Netherlands, Belgium, Luxembourg, Austria, France, Spain, Switzerland, Denmark and Poland from a single board (confirmed live: 1,000+ roles across 8+ European countries in one fetch).
- **EY** — `careers.ey.com` is genuinely global: one SAP SuccessFactors tenant, country is a facet, not a separate URL (confirmed live: ~7,300 roles worldwide, most European markets represented).
- **Deloitte Central Europe** — one white-label Avature board (`apply.deloittece.com`) covers Poland, Czech Republic, Slovakia, Romania and Hungary.
- **Deloitte Central Mediterranean** — one Avature board (`deloittecm.avature.net`) covers Italy and Greece.
- **Deloitte Nordic** — one SmartRecruiters board (`careers.smartrecruiters.com/DeloitteNordic`) covers Sweden, Norway, Denmark and Finland.

Firms with genuinely SEPARATE per-country boards (Deloitte UK/Belgium/Switzerland/Germany/Luxembourg/Portugal/Austria, KPMG, most of the adjacent tier) each get their own roster row.

### How ATS detection works

For each employer, the actor fetches the careers-page URL, fingerprints which ATS it runs by looking for that platform's own domain or markup in **structural** locations only — a script tag, a link href, an iframe src, a canonical URL, a genuine `<a href>` to the ATS's own domain, or a literal URL string inside an inline `<script>` JSON hydration payload (common on Next.js/React career sites). It never trusts loose body text, which matters: both KPMG's and PwC's marketing pages mention "Workday" in a nav link because they're Workday *implementation partners*, not because that's how they run their own hiring — a naive keyword search would misfire on that; this actor doesn't. It also excludes known non-jobs Avature tenants (talent-community/newsletter signup portals some firms link from their real, differently-hosted jobs page) from counting as evidence.

Once the platform is known, a dedicated fetcher pulls the live roster directly:

| Platform | Method | Notes |
|---|---|---|
| Workday | Official CXS JSON API | Paginated 20 rows/request. Locale-prefixed site paths (e.g. `/en-GB/`) are handled. |
| SuccessFactors Career Site Builder | Server-rendered HTML, paginated via `?startrow=N` | Two link shapes recognized: `performancemanagerN.successfactors.*` and `career5.../career?company=X`. |
| Avature | Server-rendered HTML, paginated via `?jobOffset=N` | Both the standard `tenant.avature.net` theme and white-label deployments on the employer's own domain (detected via an Avature portal meta tag) are supported, across two different card-markup themes. |
| Radancy ("TMP Worldwide") | AJAX results endpoint, paginated via `CurrentPage` | Three markup themes seen and handled. |
| SmartRecruiters | Official public JSON API | Board tokens resolved from `jobs.smartrecruiters.com` job-detail permalinks (the only structural evidence these career pages actually render). |
| Greenhouse | Official public JSON API | Recognizes both the legacy `boards.greenhouse.io` and the newer region-prefixed `job-boards.<region>.greenhouse.io` domains. |
| Ashby, Lever | Official public JSON APIs | Not yet matched to an employer in the current roster — kept ready for the next one that uses them. |

**Detection is cached** per employer (14-day TTL, a named key-value store) so a tenant that already resolved once doesn't re-run the full multi-candidate probe on every single run. If a cached detection stops working, the actor transparently re-detects.

**Vertical scoping**: a single-country entry point sometimes turns out to be a genuinely GLOBAL tenant once pulled — confirmed live: PwC's Workday board and EY's SuccessFactors board both list offices worldwide. Every role's parsed `locationCountry` is checked against the full EU/EEA + UK + CH country set; a role resolved to a different country is dropped, one with an unresolved location is kept (better to keep an ambiguous in-scope role than drop real coverage over a city our lookup table doesn't have yet).

### Parked platforms

Two employers are detected but deliberately never fetched, because the block is at the network edge, not an ATS-detection gap:

- **BNP Paribas Group** (`group.bnpparibas`) — Akamai edge block, HTTP 403 to both a plain request and a real headless browser.
- **Zurich Insurance** (`zurich.com/careers`) — Imperva Incapsula JS-challenge wall, an empty iframe shell to a plain fetch.

Both need Bright Data's Web Unlocker (currently blocked by a rejected BD KYC application) or a captured real-user network trace. They're excluded with a documented reason rather than silently failing the run.

Several other firms/boards researched are behind similar bot-mitigation (McKinsey/Akamai, Bain/Cloudflare Turnstile, RSM Netherlands/Cloudflare) and are simply not in the active roster yet rather than being fetched and failing every run.

Phenom People, Talentsoft, Cornerstone OnDemand, Oracle Recruiting Cloud, Yello, Pinpoint, Teamtailor, Recruitee and eRecruiter are real platforms confirmed live across multiple firms in this vertical (see the board-mapping doc) but not fingerprinted yet — several of those employers are kept in the roster reporting `unknown` honestly, as the next platform worth adding.

### Input

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mode` | String | No | `full` (default) returns every verified-live role this run. `diff` returns only roles that newly appeared or disappeared since the last run. |
| `countries` | Array | No | Return only roles located in these countries (ISO 3166-1 alpha-2, any EU/EEA + UK + CH code, e.g. `DE`, `FR`, `PL`). Pan-European boards (PwC's shared Workday tenant, EY's global board) are included automatically and filtered down to your countries — you never lose a firm's roles in a country just because its board is registered elsewhere. A role whose location text could not be resolved to a country is kept only when the whole board's coverage sits inside your filter. |
| `employerNames` | Array | No | Filter the built-in roster to employers whose name contains one of these strings. |
| `customEmployers` | Array | No | Add your own `{name, country, careersUrl}` employers, run through the same detection pipeline. |
| `maxRolesPerEmployer` | Integer | No | Cap on roles pulled per employer per run. Default 150. |
| `resetState` | Boolean | No | Testing only — clears diff-mode tracking history before the run. |
| `proxyConfiguration` | Object | No | Every targeted ATS is a public JSON API or plain HTML page with no anti-bot (confirmed live, zero blocks) — automatic/no-groups is correct by default. |

### Output

One row per verified-live role in `full` mode, one row per `appeared`/`disappeared` event in `diff` mode, plus an occasional `run_status` housekeeping row (first-run seeding, or a genuinely quiet diff run — never an empty dataset).

| Field | Type | Example |
|---|---|---|
| `eventType` | String or null | `"appeared"`, `"disappeared"`, or `null` for a straight live listing |
| `jobTitle` | String | `"Senior Associate - Legal Transformation & Managed Services"` |
| `employer` | String | `"PwC (Workday network — IE/NL/BE/LU/AT/FR/ES/CH/DK/PL)"` |
| `employerBrand` | String | `"PwC"` — clean grouping key per firm, without the board naming |
| `employerAtsPlatform` | String | `"workday"` |
| `locationCity` | String or null | `"Amsterdam"` — null when the ATS text didn't resolve |
| `locationCountry` | String or null | `"NL"` (ISO 3166-1 alpha-2) — null when unresolved |
| `locationRaw` | String | `"Amsterdam"` — untouched location text as returned by the ATS |
| `language` | String or null | `"en"` |
| `employmentType` | String or null | `"Full time"` |
| `url` | String | Direct application URL on the employer's own ATS |
| `reqId` | String or null | `"1396234733"` |
| `postedAt` | String or null | Best-effort ISO date; `null` when the ATS only gives a vague relative label |
| `verifiedLiveAt` | String or null | ISO timestamp this run confirmed the role live; `null` for a `disappeared` row |
| `lastSeenAt` | String | ISO timestamp of the last run that saw this role live |
| `source` | String | Always `"employer-ats"` |

### Example output

```json
{
    "eventType": null,
    "jobTitle": "Gestionnaire de paie - H/F - Bayonne",
    "employer": "Forvis Mazars France",
    "employerAtsPlatform": "smartrecruiters",
    "locationCity": "Bayonne",
    "locationCountry": "FR",
    "locationRaw": "Bayonne, fr",
    "language": null,
    "employmentType": "Full-time",
    "url": "https://jobs.smartrecruiters.com/MAZARS/744000143639529",
    "reqId": "744000143639529",
    "postedAt": "2026-08-15T07:28:57.976Z",
    "verifiedLiveAt": "2026-08-16T16:19:45.662Z",
    "lastSeenAt": "2026-08-16T16:19:45.662Z",
    "detectedAt": "2026-08-16T16:19:55.546Z",
    "source": "employer-ats",
    "recordType": "live_role"
}
```

### Scheduling for diff mode

Diff mode's entire value is in the delta, not the snapshot — put it on a schedule, not a one-off run.

1. Apify Console → your run → **Schedules → Create new schedule**.
2. Daily is a reasonable cadence — corporate careers pages don't churn roles faster than that.
3. Leave `resetState` off between scheduled runs — enabling it wipes tracking history and turns every run back into a first-run seed.
   3b. Keep the same `countries`/`employerNames` filters on every run of a schedule. The diff baseline only contains what past runs actually pulled, so changing filters mid-schedule reports the newly-included scope as one big batch of appeared roles.
   3c. `disappeared` events for an employer only fire when the run pulled that employer's full in-scope list. A capped (partial) pull can't prove a role left the ATS, so it reports appearances only — raise `maxRolesPerEmployer` above the employer's live-role count if disappearances matter to you.
4. The first scheduled run always seeds (one `run_status` row per employer, no delta events yet — there's nothing to diff against). Every later run reports real appeared/disappeared events.

### Cost estimate

Workday, SmartRecruiters and Greenhouse cost roughly one HTTP request per 20–100 roles (each platform's own JSON API page-size ceiling). The HTML-tier platforms (SuccessFactors, Avature, Radancy) cost one request per 15–25 roles. Per-result compute is minimal — no browser rendering, no proxy spend on any currently-supported platform. Actor usage is billed per result and per run start; your Apify usage dashboard is the source of truth. **Usage cost for a run only settles after it reports SUCCEEDED** — reading the dataset mid-run will undercount what you'll actually be billed.

### Limitations

- **Detection coverage is honest, not exhaustive.** Roughly half the built-in roster resolves to a known, currently-supported ATS today (see the `unknown`/parked notes in the run log for the rest) — employers on platforms this actor doesn't fingerprint yet (Phenom People, Talentsoft, Cornerstone OnDemand, and several bespoke in-house systems) report `unknown` rather than being silently skipped or faked.
- **`postedAt` is best-effort.** Several ATSs only expose a relative label ("Posted Today", "30+ Days Ago") with no exact timestamp; a vague label yields `null` rather than a fabricated date.
- **`employmentType` is exactly as labeled by the ATS**, not normalized across employers (`"Full time"` vs `"Tijdelijk"` vs `"Permanent"` all appear verbatim).
- **A country filter on a non-Workday pan-European board is applied after pulling.** Workday tenants are filtered server-side (the query itself is scoped to your countries), but EY's global SuccessFactors board can only be pulled in its own ordering and filtered afterwards, bounded by `maxRolesPerEmployer`. Raise `maxRolesPerEmployer` when you filter to a single country and want deep EY coverage there — the run log reports exactly how many fetched roles the filter dropped per employer.
- **Location resolution favors coverage over strict precision on huge global tenants.** A role whose ATS text has no country name or code at all can't be resolved to a country and is kept rather than dropped — the alternative would also drop genuine in-scope roles whose city just isn't in the curated lookup table yet. A role with a RECOGNIZED non-European country is always dropped.
- Data is scraped from each employer's own public careers page and may change without notice.

### Need this data on a schedule, or a custom version?

We run this scraper as a managed service for businesses: scheduled runs, deduplication, delta detection, and delivery to your inbox, Google Sheets, or API — maintenance included. We can also widen the employer roster to your specific target list or combine it with other sources into one feed.

See [studioamba.dev/services](https://studioamba.dev/services/) or email <hello@studioamba.dev> for a free data sample. We maintain 300+ European web scrapers and answer within one business day.

# Actor input Schema

## `mode` (type: `string`):

'full' returns every verified-live role found this run. 'diff' compares against the previous run (per-employer persistent state) and returns only roles that newly appeared or disappeared since then — the hiring-signal product. Both modes always refresh the underlying state, so you can switch modes run to run.

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

Return only roles located in these countries (ISO 3166-1 alpha-2 codes, e.g. DE, FR, PL — any EU/EEA + UK + CH code). Pan-European boards (PwC, EY) are included automatically and filtered down to your countries.

## `employerNames` (type: `array`):

Only include employers whose name contains one of these strings (case-insensitive), e.g. 'Deloitte', 'PwC', 'ING'. Leave empty to use every employer in the built-in roster (after any country filter).

## `customEmployers` (type: `array`):

Add your own employers instead of (or alongside) the built-in roster. Each entry: {"name": "Acme Bank", "country": "BE", "careersUrl": "https://acme.com/careers"}. The same ATS-detection pipeline runs against these — no per-employer scraper code needed, just a real careers-page URL.

## `maxRolesPerEmployer` (type: `integer`):

Cap on live roles pulled per employer per run. Large tenants (e.g. EY's SuccessFactors board carries 7,000+ roles worldwide) are paginated up to this cap; the ATS-reported total is always included in the run summary even when capped. Higher values mean a longer, heavier run.

## `resetState` (type: `boolean`):

Clears this actor's persistent per-employer seen-roles state before the run, so diff mode treats every role as a fresh first-run seed (no appeared/disappeared events) and a full-mode run starts state tracking over. Never enable this on a real scheduled run — it defeats diff mode's whole purpose.

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

Every employer ATS this actor targets (Workday, SuccessFactors, Avature, Radancy, Greenhouse, Ashby, Lever, SmartRecruiters) is a public JSON API or plain server-rendered HTML page with no anti-bot — confirmed in the POC with zero proxy and zero blocks. Automatic/no-groups is the correct default; escalate only if you see blocks in the run log.

## Actor input object example

```json
{
  "mode": "full",
  "maxRolesPerEmployer": 30,
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "mode": "full",
    "maxRolesPerEmployer": 30,
    "resetState": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/big-four-careers-scraper").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 = {
    "mode": "full",
    "maxRolesPerEmployer": 30,
    "resetState": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/big-four-careers-scraper").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 '{
  "mode": "full",
  "maxRolesPerEmployer": 30,
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call studio-amba/big-four-careers-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/big-four-careers-scraper"
        }
    }
}

```

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/ewAXyr4tNMarb6aTa/builds/W1xclX2w8l4t9GKEf/openapi.json
