# StepStone.de Jobs Scraper — German Job Listings & Leads (`scrapersdelight/stepstone-de-jobs-scraper`) Actor

From $0.50 per 1,000 listings. Scrape StepStone.de by keyword, city, Bundesland, radius, sector, discipline, contract type, career level, home-office & date: employer name, company page, location, posted date, snippet, apply route. Incremental 'new mandates only' mode with Slack/webhook alerts.

- **URL**: https://apify.com/scrapersdelight/stepstone-de-jobs-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 per job listing 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?

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

## 🇩🇪 StepStone.de Jobs Scraper — German Job Listings, Employers & Hiring Leads

**Scrape StepStone.de — Germany's largest job board — by keyword, city, Bundesland, radius, sector, discipline, contract type, career level, home-office and date posted. Every row is one live vacancy: the employer, their StepStone company page, the location, the posting date and a description snippet. 20+ server-side filters, an incremental "new mandates only" monitor, and one flat price of $0.50 per 1,000 listings.**

### Why this one?

| | **This actor** | Typical StepStone scraper |
|---|---|---|
| **Price per 1,000 listings** | **$0.50** | $1.00 – $3.00 |
| Actor-start fee | **none** | $0.005 – $0.09 per run |
| Server-side filters | **20+** (sector, discipline, career level, application method, advert language, skills, region, city…) | keyword + location |
| **Proves the filters bound** | ✅ reads StepStone's own `categorization` echo back **on every search and every route** (keyword, company, pasted URL) and **fails the run rather than bill you for rows that don't match your search**; on a zero-row page it names the unbound filter instead of blaming StepStone | ❌ |
| Duplicates | ✅ collapsed on StepStone's listing id **before** billing — 250 raw → 100 delivered, 150 collapsed on an overlapping 4-query run (run JcpJGdYucxuFcbd6g, 2026-09-06) | often billed twice |
| StepStone's "similar jobs" padding | ✅ dropped by default, so a narrow search can't bill you 24 non-matching rows | billed as results |
| Staffing-agency noise (Arbeitnehmerüberlassung) | ✅ removed server-side, so you don't pay for competitors reposting mandates | ❌ |
| Incremental "new listings only" mode | ✅ named key-value store that survives between scheduled runs | ❌ |
| Employer-level sweep | ✅ every open role at one company, by id or company URL | ❌ |
| Salary claims | ❌ **and we say so** — see *Honest limits* | some claim it; the field is empty on this surface |

No login, no API key, no browser. The actor reads the structured JSON that StepStone's own public result pages are rendered from — 25 listings per request; a healthy page lands in about a second, and the median run on 2026-09-06 took 21 s.

***

### What does StepStone.de Jobs Scraper do?

It extracts **live German job postings** from [stepstone.de](https://www.stepstone.de) into clean rows you can export as **JSON, CSV, Excel** or pull through the Apify API.

- 🔎 **Search the way the site does** — keyword (*Was*), city / postal code / Bundesland (*Wo*) + radius, and StepStone's full facet set. All applied server-side, so you only pay for rows that already match.
- 🏢 **Employer-first output** — company name, numeric company id, and the employer's StepStone profile URL on every row. That's the lead.
- 🗺️ **Real German geography** — all 16 Bundesländer by name, exact-city ids that ignore radius spill, and sub-state region ids (Ruhrgebiet, Rhein-Main…).
- 🏭 **Segment like an agency** — sector (*Branche*), discipline (*Berufsfeld*), contract type (*Vertragsart*), career level (*Karrierestufe*), working hours, advert language, extracted skills.
- 📮 **Application route** — `EXTERNAL` means the employer runs its own ATS (a direct mandate you can pitch); `INTERNAL` is StepStone quick-apply.
- 🔔 **Incremental monitor** — schedule it and get only the listings that are new since the last run, with Slack / webhook alerts.
- 🎯 **Noise control that works** — `excludeStaffingAgencies` removes Arbeitnehmerüberlassung postings (staffing agencies reposting other people's mandates) server-side, before you are charged. Toggles for sponsored and anonymous rows exist too, but read *Honest limits* first — those flags are currently always false.

***

### What data does it extract?

Every listing is one dataset row. **Fill rates below are measured on 430 real rows across seven runs on 2026-09-06, not estimated** (runs 88I8ff2ju9nnwzgkk · vhizaSgmf5DnI2j9e · JcpJGdYucxuFcbd6g · FabH8m3esnd22xaQM · 3QlZWTy2rpUx1Eq7t · zs29FBmZPZzoRgTUM · Unqkj9aV8fbhbSavU)**:**

| Field | Fill | What it is |
|---|---|---|
| `jobId` | 100% | StepStone's numeric listing id — the dedupe key |
| `title` | 100% | Job title |
| `jobUrl` | 100% | Absolute listing URL, click-tracking suffix stripped |
| `companyName` | 100% | **The employer — the lead** |
| `companyId` | 100% | Numeric employer id (feed it back in to sweep all their roles) |
| `companyUrl` | 430/430 on keyword searches; 10/10 on a **Company page URL** run; **0/10 on a `companyId` run** | The employer's StepStone profile / jobs page. StepStone omits it when the result list is already scoped to one employer by id — use `companyUrl` (the page route) if you need that column (runs 88I8ff2ju9nnwzgkk · UtizOckyVXZN5JDgE · 64wRh1OSbv8CF59vZ) |
| `companyLogoUrl` | 100% | Employer logo |
| `location` | 100% | City, or a multi-city string for nationwide roles |
| `datePosted` | 100% | ISO-8601 with timezone |
| `textSnippet` | 100% | ~300-character description teaser, **HTML entities decoded** — StepStone ships this field escaped, so German umlauts arrive as `ä`/`ö`/`ü`, never as `&auml;` |
| `harmonisedId` | 100% | StepStone's cross-brand UUID |
| `workFromHome` / `workFromHomeCode` | 100% | Whether home office is offered (read *Honest limits*) |
| `isSponsored`, `isTopJob`, `isHighlighted`, `isAnonymous` | present on 100%, **but `false` on 430/430** | Placement + anonymity flags — see *Honest limits* |
| `isPartnershipJob`, `isBackfilled`, `isCrossPosted` | present on 100%, **`false` on 430/430** | Syndication flags |
| `partnerSourceSite` | 0/430 | Always null on this surface |
| `labels`, `labelTypes` | **174/430 overall (40.5%); 80/200 (40.0%) on a single 200-row *vertrieb* run (run UP3f6WyjxARTBWmJD)** | Badges such as QUICK\_APPLY, NO\_COVER\_LETTER. Genuinely sparse **and clustered by page** — inside that one run the per-page fill ran from 4/25 to 16/25, and two 50-row *vertrieb* runs the same day read 7/50 and 8/50. So ~40% is the fill; any 50-row slice is noise, not a per-keyword rate |
| `isQuickApply` | **true/false on the ~40% of rows that carry labels, `null` on the rest** | Measured 2026-09-06 on 100 *maschinenbau* rows (run Unqkj9aV8fbhbSavU): 30 `true`, 16 `false`, 54 `null`; and on 200 *vertrieb* rows (run UP3f6WyjxARTBWmJD): 76 `true`, 4 `false`, 120 `null`. `null` means "this row carries no badges at all", not "no quick apply" |
| `section` | 100% | `main` = a real match (padding rows are dropped by default) |
| `searchQuery`, `sourceUrl`, `resultPage`, `scrapedAt` | 100% | Provenance for every row |
| `positionOnPage`, `positionAbsolute` | 100% | Ad-ranking position (turn off with `compactOutput`) |
| `isNew` | monitor mode | Set on listings unseen since the previous run |
| `raw` | opt-in | StepStone's untouched source object, via `includeRawJson` |

***

### Who is it for?

- 🤝 **Recruitment & staffing agencies** — a live map of who in Germany is hiring what, where, and through whose ATS. Filter to `applicationMethod: "EXTERNAL"` and `experienceLevels: ["manager"]` for direct mandates with a decision-maker attached.
- 🎯 **B2B / GTM teams** — hiring is the strongest public buying signal there is. A company posting five DevOps roles is buying tooling; one posting warehouse staff is expanding logistics.
- 📊 **Labour-market and HR analysts** — corpus sizes measured 2026-09-06 run from 8,731 listings for *maschinenbau* to 47,962 for *pflege*, sliceable by sector, region and date.
- 🏗️ **Job boards & aggregators** — a clean, deduplicated German feed with stable ids.
- 💼 **Sales teams doing account-based selling** — pass a `companyId` and get every open role at one employer.

***

### Quick start

```json
{ "searchQueries": ["vertrieb"], "maxItems": 50 }
```

#### Fresh mandates in Bavaria, direct employers only

```json
{
  "searchQueries": ["vertriebsingenieur", "key account manager"],
  "bundesland": ["Bayern"],
  "postedWithin": "7d",
  "applicationMethod": "EXTERNAL",
  "excludeStaffingAgencies": true,
  "sortBy": "date",
  "maxItems": 500
}
```

#### Daily "new listings only" monitor with Slack alerts

```json
{
  "searchQueries": ["pflegefachkraft"],
  "location": "Hamburg",
  "radius": 30,
  "postedWithin": "24h",
  "sortBy": "date",
  "incrementalMode": true,
  "slackWebhookUrl": "https://hooks.slack.com/services/...",
  "maxItems": 0
}
```

Attach an Apify **Schedule** and each run emits only what is new since the last one.

#### Every open role at one employer

```json
{ "companyUrl": "https://www.stepstone.de/cmp/de/gi-group-deutschland-gmbh-213579/jobs", "maxItems": 0 }
```

#### Paste a search you built in your browser

```json
{ "startUrls": ["https://www.stepstone.de/jobs/pflege/in-hamburg?radius=30&wfh=2"], "maxItems": 100 }
```

The URL's filters are parsed back out and the whole result set is re-paged. A pasted URL is re-paged **exactly as pasted**, so the filter fields on the input form (location, radius, Bundesland, contract type…) do **not** apply to it — set any of them alongside a start URL and the run names each one it is ignoring, in the log and in the status message (measured run tgfpr06C8zrQTQsUD, where `location: "Berlin"` and `radius: 50` were both named as not applied).

***

### How much does it cost?

Pay-per-event, one event, no start fee, no subscription.

| Event | What it covers | Price |
|---|---|---|
| `job-listing-scraped` | each job listing returned and saved | **$0.0005** |

**$0.50 per 1,000 listings.** Duplicates, rows you excluded with the filters, StepStone's non-matching "similar jobs" padding, and rows your `outputFields` list would leave empty are all dropped **before** delivery, so you are never charged for them. Delivery and billing happen in the same call, so a charge cap can never leave you paying for rows you didn't receive.

Measured 2026-09-06 on the shipped build: `outputFields: ["salary","jobTitle"]` — two names this actor does not produce — returns **0 rows and charges $0.00** (run YYL1cHyQAWvbXzTh8), with the reason in the status message. It used to deliver and bill five empty `{}` rows.

***

### Honest limits

Things this actor deliberately does **not** claim:

- **There are no salary figures on this surface.** StepStone's result pages carry a `unifiedSalary` object on about half the rows, but every field inside it — min, max, currency, period — was `null` on 25 of 25 rows measured. Real salary numbers live on the individual listing page, which StepStone's `robots.txt` disallows. Some competing actors advertise StepStone salary data anyway; this one does not return a salary field at all rather than ship an empty column.
- **`workFromHome` tells you *whether*, not *which*.** Measured across 450 rows: the row-level code is effectively binary (`none` / `offered`). Code `offered` covers 97–100% of *both* the "Nur Home-Office" and the "Teilweise Home-Office" result sets, so it cannot separate fully-remote from hybrid. To actually select fully-remote roles, use the server-side `homeOffice: "only"` filter — that one provably narrows the corpus (22,136 → 112 listings for *vertrieb*, runs OHj6iaW9j9X3bJmbC / kZ34fh3c6bKmDmYb4, 2026-09-06).
- **The sponsored / anonymous / syndication flags are always `false` here.** Re-measured 2026-09-06 across 430 listings on 6 keywords, `isSponsored`, `isTopJob`, `isHighlighted`, `isAnonymous`, `isTrafficFromPartner`, `hasFuturePosting`, `isPartnershipJob`, `isBackfilled` and `isCrossPosted` came back `false` on 430 of 430 rows on StepStone's logged-out result pages. The fields are returned because they are part of the source record and may start populating, and the matching `excludeSponsored` / `excludeAnonymous` toggles are wired correctly — but as of today they have nothing to remove. The exclusion that *does* bite is `excludeStaffingAgencies`, which filters server-side on contract type.
- **No commute-time (Pendelzeit) filter.** StepStone's own commute facet has no URL form — it is a client-side widget that calls an endpoint `robots.txt` disallows. Rather than ship a parameter that silently does nothing, it is not offered.
- **No detail-page enrichment.** Full descriptions and requirements live under `/listing/`, which `robots.txt` disallows. This actor scrapes result pages only — one site, one function.
- **StepStone is a commercial job board and can tighten its defences.** Transport is measured, not assumed (below). If first-attempt rates ever slide, switch `proxyConfiguration` to RESIDENTIAL with country DE.

***

### Reliability — measured, not asserted

The actor runs plain HTTP through the Apify **datacenter** proxy, forced to **HTTP/1.1**.

That detail is the whole ballgame. Over HTTP/2 the datacenter pool draws `NGHTTP2_INTERNAL_ERROR` stream resets that poison an **entire proxy session** — one session failed 10 of 10 consecutive calls while another was clean 10 of 10. Fresh-session HTTP/2 measured **60.0%** (12/20) and **71.4%** (25/35) first-attempt. Forcing HTTP/1.1 and retiring the session on every transport error removes that failure class.

**Measured on the Apify platform itself** (not on a dev machine, which flatters the numbers), on the shipped transport, across **59 result pages in 34 runs on 2026-09-06**:

| | |
|---|---|
| Runs completed | **34 of 34 SUCCEEDED** — 0 FAILED, 0 TIMED-OUT |
| First-attempt success | **64.4%** (38/59 pages) |
| Pages lost after exhausting retries | **0 of 59** |
| Rows delivered vs rows charged | **987 / 987 — exact match on every run** |
| Residential rescues needed | **1 page of 59** (run 88I8ff2ju9nnwzgkk) |
| Run time | median **21 s**, worst single-search run **145 s** (the one that needed the residential rescue), against a 300 s health-check threshold |

**Be aware that this number moves.** The same test on 2026-09-04 measured 86.8% first-attempt over 114 pages; on 2026-09-06 the Apify datacenter pool was materially worse and the same code measured 64.4%. What did *not* move is the outcome: **every run still succeeded and no page was lost**, because the retry budget (default 8, floor 5 in code) and the residential rescue absorb it. That is the honest shape of this lane — the first-attempt rate is a weather report, the delivered/charged match and the zero lost pages are the guarantee.

Retries are what turn that first-attempt rate into complete results, which is why the retry floor is enforced in code and the default is 8. Retries cost time, not money: you are only ever charged for rows you receive.

**And if the datacenter proxy burns every one of those attempts on a page** — measured: it happens during bad patches, and it happened once in these 34 runs — the actor automatically retries that page through **RESIDENTIAL / country DE** rather than leaving a hole in your results. Tested by cutting the datacenter path entirely: **5 out of 5 dead pages were rescued**, full 25 rows each. It only ever fires *after* the cheap path is exhausted, so a healthy run never pays for it, and it steps aside if you pin your own proxy groups.

If a page is *still* unreachable after all of that, the run says so explicitly in its status message and log — you are told about the coverage gap rather than quietly handed a short dataset, and you are not charged for the missing rows.

Pagination is `?page=N`, 25 rows per page. (`?of=<offset>` is silently ignored by StepStone — it echoes offset 0 and re-serves page 1.) Crawled *maschinenbau* contiguously through page 14 plus deep probes at 20/30/40/60/100/…/347: every page served 25 rows, page 347 served 17, and pages beyond it returned nothing with `pageCount` pinned at 347.

If first-attempt rates ever fall meaningfully below this, switch `proxyConfiguration` to RESIDENTIAL with country DE — measured working, just slower.

***

### Is it legal to scrape StepStone.de?

The actor reads **publicly available job advertisements** — no login, no paywall, and no job-seeker data. Employer names and company pages are business information the postings publish deliberately.

StepStone's `robots.txt`, fetched 2026-09-04, reads in relevant part:

```
User-agent: *
Disallow: /*?*
Disallow: /jobs/*?*
Allow: /jobs/*?q=*
Disallow: /jobs/*?q*&*
Disallow: /jobs/vollzeit/
Disallow: /jobs/teilzeit/
Disallow: /public-api/
Disallow: /listing
Disallow: /listing/*
```

This actor deliberately stays off `/public-api/` and `/listing/` — which is where most competing scrapers get their salary and description data — and routes the working-hours filter through the query parameter `wt=` rather than the disallowed `/jobs/vollzeit/` path.

Scraping may still conflict with StepStone's Terms of Service, and **you are responsible for how you use the data**, including your obligations under the GDPR for any personal data a listing happens to contain (a named contact person, for instance). Review StepStone's current terms before commercial use.

***

### FAQ

**What is StepStone.de?**
StepStone is Germany's largest job board, carrying hundreds of thousands of live vacancies across every sector. Broad keywords are deep — counts read off StepStone's own result pages on **2026-09-06**: *pflege* 47,962 listings, *einkauf* 23,284, *vertrieb* 22,138, *logistik* 14,583, *it* 12,361, *maschinenbau* 8,731. (These move by a few rows an hour; the run ids are in SIGNOFF.md.)

**Do I need an account, login, or API key?**
No. The actor reads public result pages.

**Does it return salaries?**
No — and that is deliberate. See *Honest limits*: the field exists on StepStone's result pages but is empty on every row.

**How do I get only fully-remote jobs?**
Set `homeOffice: "only"`. Do not filter on the output `workFromHome` column for this — it cannot separate remote from hybrid.

**How do I know the filters actually applied?**
The actor reads StepStone's own `categorization` object back off the page, which echoes every parameter the server bound, and compares it to what you asked for. If they disagree it **fails the run instead of delivering and billing rows that don't match your search**. The check runs on **every** search in the run and on **every** route, including a pasted start URL — verified 2026-09-06: a 3-task run (two keywords + a company page) logged three separate binding verifications (run IrjPnRC8EgptlVO58), and a start URL carrying `wfh=2` logged its own (run fLEmCBMsJgTIVAeGW). Two honest caveats: filters you set on the form do not apply to a pasted URL at all (the run says which ones it ignored), and StepStone echoes an **id**-type filter (`regionIds`, `cityIds`, `sectorIds`, `skillIds`) straight back even when the id matches nothing — so on a zero-row run the message tells you the echo does not vouch for those values and names them (run pPcdlgljvzmCB9Px7).

**Will I be charged twice for the same job across overlapping searches?**
No. Deduplication is on by default and collapses on StepStone's numeric listing id across every page and every keyword in the run, before delivery. Measured 2026-09-06: the same search fed four times read 250 raw rows and delivered **100, all unique** — 150 repeats collapsed before billing (run JcpJGdYucxuFcbd6g). The control with `deduplicate:false` delivered 50 rows containing only 25 unique ids (run FabH8m3esnd22xaQM).

**What are "similar jobs"?**
When a search is narrow StepStone pads the page with recommended/regional listings that do not match your filters — a one-result query came back as 1 real match plus 24 padded rows. Those are dropped (and never billed) unless you set `includeSimilarJobs: true`.

**Can I scrape every open role at one employer?**
Yes — pass `companyId` or `companyUrl`. On **both** routes the numeric employer id is checked against every row on page 1 and the run fails rather than deliver another employer's leads (`companyUrl`'s id is read out of the URL slug). Verified 2026-09-06: `companyUrl` → 10/10 rows owned by employer 213579 (run UtizOckyVXZN5JDgE); `companyId` → 5/5 (run W0QQmkjLapgK4j2cy).

**Can I combine an employer with a keyword?**
Yes, with `companyId`. Give it a `companyId` *and* `searchQueries` and the two are intersected into one server-side search per keyword — measured 2026-09-06: companyID 213579 + "lager" = 83 matching jobs, 10/10 delivered rows owned by that employer (run 64wRh1OSbv8CF59vZ). A **company page URL** cannot be narrowed this way (StepStone's `/cmp/` page takes no keyword); the run says so in the log and sweeps the page in full.

**Can I resume or shard a large crawl?**
Yes — `startPage` shifts the window (page N covers results (N−1)×25 … N×25) and `maxPagesPerQuery` caps the depth. **If you start past the last page, the run tells you so with the real number instead of blaming StepStone**: `{"searchQueries":["vertrieb"],"startPage":2000}` returns 0 rows, charges nothing, and says *"this search only has 889 page(s) … page 2000 does not exist … start at a page between 1 and 889"* (run pg9BuwQhY9AH6B4bC, 2026-09-06). The page count is read off StepStone's own pagination before the empty answer is explained — and if the out-of-range page carries no pagination at all, page 1 is fetched to get it (that probe delivers nothing and is never billed).

**In incremental mode, does a `maxItems` cap mean I get fewer rows each run?**
No — and it's worth understanding why. Incremental mode suppresses listings it has already seen, then **keeps paging to try to fill your cap** with unseen ones. So on a huge keyword, run #2 can still return a full 50 rows (just deeper, older ones). To get a true "only what's new today" feed, bound the corpus rather than the row count: pair `incrementalMode` with `postedWithin: "24h"` and `sortBy: "date"`, and set `maxItems: 0`.

**How do I keep costs down on a daily monitor?**
`sortBy: "date"` + `stopAfterDays`: the crawl stops paging as soon as a page is entirely older than your cutoff. Add `incrementalMode` so you only get (and only pay for) listings you haven't seen.

**What happens if a search matches nothing?**
The run exits cleanly with zero rows, charges nothing, and the status message names **which** of the causes it actually was — it never falls back to blaming StepStone. Measured 2026-09-06, one run per case:

| Cause | What the run says |
|---|---|
| You started past the last page | *"this search only has 889 page(s)… page 2000 does not exist… StepStone is NOT empty"* (run pg9BuwQhY9AH6B4bC) |
| Your `outputFields` names no real column | *"names no field this run produces… dropped instead of delivered and nothing was charged — StepStone had results, this is a column-name problem"* (run YYL1cHyQAWvbXzTh8) |
| A `disciplines` value isn't one StepStone offers | *"None of the Discipline (Berufsfeld) value(s) you set is one StepStone offers… nothing was scraped and nothing was charged"* (run 1SrziGfztnbPB5OtW) |
| Start URLs that aren't stepstone.de | the URLs are named and nothing else is scraped in their place |
| Your **Max total charge** cap was hit first | says so, and tells you to raise the cap |
| StepStone blocked every attempt | says it is a transient block, not an empty search |
| StepStone genuinely has nothing | *"it served a valid result page with an empty list"*, plus whether the filters were verified as bound (run tgfpr06C8zrQTQsUD) |
| **Incremental mode had already delivered everything on the page** | *"All 25 listing(s) on this search were already delivered by a previous run of this monitor… nothing is new since last time. That is the normal quiet day of a scheduled monitor, not a misconfiguration"*, $0.00 (run KicEWtqZDAUo7JvMd, 2026-09-07) |
| Everything on the page was on your `skipJobIds` list | *"25 listing(s) were on the 'Skip job IDs' list you supplied"*, $0.00 (run ZHXTb8VOEV1bA1jTu, 2026-09-07) |
| A filter you set removed the rest | names the count and the toggle(s) **you actually set** — it never suggests one you left off |

If a filter you set never bound, that is named too, rather than reported as an empty StepStone. And if the page carried no result-list block at all, the zero is reported as **unconfirmed** instead of confirmed (run ILdkl9AiL7mSFXhPm).

The actor's **own memory is never reported as your filters**. Incremental mode and `skipJobIds` are counted separately from the filters you set, in both the status message and the run log (`0 excluded by the filters you set | 25 already delivered by an earlier run (incremental mode)` — run KicEWtqZDAUo7JvMd). A monitor's quiet day says so in those words; it does not tell you to widen a search that is working correctly.

**Can I choose my own columns?**
Yes — `outputFields` takes a list and returns exactly those, in that order. A name this actor does not produce is ignored and named in a log warning alongside the full field list, so a typo cannot silently cost you a column (run gdy0Q8yfZGmWgXZgU; a mixed list still works — `["companyName","title","salary"]` returned 25 two-column rows on run fLEmCBMsJgTIVAeGW). **If *none* of your names is real, the rows would be empty — those rows are dropped rather than delivered, and you are not charged for them**: `["salary","jobTitle"]` returns 0 rows and $0.00 with the reason in the status message (run YYL1cHyQAWvbXzTh8, 2026-09-06). `compactOutput` drops the ad-tracking positions for a clean CSV.

**Can I integrate with Make, Zapier, n8n or my CRM?**
Yes — `webhookUrl` and `slackWebhookUrl` push new listings in monitor mode, or pull the dataset through the Apify API.

**Which proxy should I use?**
Leave it on the default. Apify datacenter is the cheap path and is measured sufficient — 64.4% of pages first try on 2026-09-06, 0 pages lost, 34/34 runs SUCCEEDED — and when a page does fail every datacenter attempt the actor escalates that page to RESIDENTIAL / country DE by itself. You only need to pin RESIDENTIAL manually if you want every request on it from the start (fewer retries, so a faster run, at a higher proxy cost).

***

### Feedback

Missing a field or a filter? Open an issue on the actor — fixes and feature requests welcome.

# Actor input Schema

## `searchQueries` (type: `array`):

One StepStone search per keyword — exactly what you'd type in the 'Was' box (e.g. 'Vertrieb', 'SAP Berater', 'Pflegefachkraft'). Every filter below is applied to all of them. Broad terms are deep — corpus sizes measured 2026-09-06: vertrieb 22,138 · pflege 47,962 · einkauf 23,284 · logistik 14,583 · it 12,361 · maschinenbau 8,731 listings.

## `startUrls` (type: `array`):

Paste any stepstone.de result URL verbatim — build the search in your browser, copy the address, drop it here. Its OWN filters are parsed back out and re-paged from page 1; the filter fields on this form apply to your keyword searches only, NOT to a pasted URL (the run logs a warning if you set both). Company pages (/cmp/de/…/jobs) work too. Runs alongside your keywords. A URL that is not on stepstone.de is refused and the run ends with zero rows and zero charge rather than quietly scraping something else.

## `location` (type: `string`):

City ('Berlin'), postal code ('80331') or Bundesland name ('Bayern') — all three verified working. Leave empty for all of Germany.

## `radius` (type: `integer`):

Radius around the location. StepStone's own options are 0, 5, 10, 20, 30, 50 and 100 km. Ignored when no location is set, and ignored (with a warning) on start URLs, which are re-paged exactly as pasted.

## `bundesland` (type: `array`):

Filter by German federal state — server-side, no radius maths. Combine several. (Uses StepStone's region ids internally; all 16 verified individually.)

## `regionIds` (type: `array`):

Raw StepStone region ids for sub-state regions the Bundesland list doesn't cover (e.g. 200000063 Ruhrgebiet, 200000062 Rhein-Main-Gebiet, 200000061 Rheinland). Read them off the 'Region' facet links on any result page.

## `cityIds` (type: `array`):

Exact-city filter — unlike 'Location + radius' it does NOT spill into neighbouring towns. e.g. 419239 Berlin · 423640 Hamburg · 420876 München · 423047 Bremen. Read ids off the 'Stadt' facet links.

## `postedWithin` (type: `string`):

Only listings published inside this window — StepStone's own 'Aktualität' filter, applied server-side. The fresh-mandates setting for a daily monitor.

## `contractTypes` (type: `array`):

Restrict to these contract types. Leave empty for all.

## `workTypes` (type: `array`):

Vollzeit or Teilzeit. NOTE, measured: StepStone binds only ONE working-hours value server-side — if you pick both, the first is used and a warning is logged.

## `experienceLevels` (type: `array`):

Seniority band. 'manager' = mit Personalverantwortung — the decision-makers a recruiter or agency actually wants.

## `homeOffice` (type: `string`):

StepStone's Home-Office filter, applied server-side. This is the ONLY reliable way to select fully-remote roles: the row-level `workFromHome` field says whether home office is offered but does not separate hybrid from remote-only (measured over 450 rows). It bites hard: "vertrieb" went 22,136 listings unfiltered to 112 with homeOffice:"only" — both read within the same hour on 2026-09-06 (runs OHj6iaW9j9X3bJmbC / kZ34fh3c6bKmDmYb4). The corpus itself drifts by a handful of rows an hour, which is why that pair reads 22,136 and the headline count above reads 22,138.

## `applicationMethod` (type: `string`):

High-value for agencies: EXTERNAL = the employer runs its own ATS (a direct mandate you can pitch), INTERNAL = StepStone quick-apply.

## `jobLanguage` (type: `string`):

Language StepStone detected for the advert. 'en' surfaces the international-hiring subset.

## `disciplines` (type: `array`):

StepStone's job-family filter (Berufsfeld). Use one of StepStone's exact labels (case-insensitive): Vertrieb und Verkauf | Groß- und Einzelhandel | Kundenservice | Logistik | Handwerk | Management | Administration | Gastronomie, Hotellerie | IT | Ingenieurwesen | Marketing | Versicherungen | Personal | Beratung | Banken | Gesundheit | Fertigung, Produktion | Immobilien | Bauwesen | Finanzen. Anything else is named in a warning and dropped; if none of your values is valid the run stops rather than silently return — and bill — rows from job families you filtered out.

## `sectorIds` (type: `array`):

Industry segmentation, the way agencies buy. e.g. 15000 Groß- & Einzelhandel · 21000 IT & Internet · 12004 Maschinen-/Anlagenbau · 14001 Baugewerbe · 17000 Transport & Logistik · 29000 Gesundheit & soziale Dienste. Ids come off the 'Branche' facet links.

## `skillIds` (type: `array`):

StepStone's extracted-skill filter (e.g. 668610). Read ids off the 'Kenntnisse' facet links on a result page.

## `companyId` (type: `string`):

Account-based selling: every open role at one employer. The numeric id at the end of a StepStone company URL (…/cmp/de/oms-pruefservice-gmbh-374729/jobs → 374729). Add 'Search keywords' and the two are INTERSECTED server-side into one search per keyword (measured 2026-09-06, run 64wRh1OSbv8CF59vZ: companyID 213579 + "lager" = 83 matches, 10/10 delivered rows owned by that employer). Every row is checked against the id and the run fails rather than deliver another employer's leads. On this route StepStone omits the row-level companyUrl (0/10 filled, run 64wRh1OSbv8CF59vZ) — use 'Company page URL' if you need that column.

## `companyUrl` (type: `string`):

Alternative to the id — paste the employer's StepStone jobs page (https://www.stepstone.de/cmp/de/<slug>-<id>/jobs). The whole page is swept: a company PAGE cannot be narrowed by a keyword or by the filters above (use 'Company id' for that, which can). The numeric id is read out of the slug and every row is checked against it. Measured 2026-09-06, run UtizOckyVXZN5JDgE: 10/10 rows owned by that employer, companyUrl 10/10 filled.

## `sortBy` (type: `string`):

'date' is the one to use for monitors — it puts the newest mandates on page 1 and makes 'Stop after N days' able to cut the crawl short.

## `startPage` (type: `integer`):

Resume or shard a big crawl. Page N covers results (N-1)×25 … N×25. If you start past the last page of the result set, the run tells you how many pages the search actually has and returns nothing — it never reports the search as empty.

## `maxPagesPerQuery` (type: `integer`):

Ceiling on result pages fetched per keyword (25 listings a page). 0 = walk the whole result set, up to StepStone's own last page.

## `maxItemsPerQuery` (type: `integer`):

Per-keyword cap, so one huge term can't eat the whole run. 0 = no per-keyword cap.

## `maxItems` (type: `integer`):

Hard total cap across every search — your cost guard. Defaults to 50 for a fast first run. Set 0 for an unlimited sweep (backstopped at 10,000 per keyword).

## `stopAfterDays` (type: `integer`):

Drop listings older than N days. With 'Sort by = date' it also STOPS paging as soon as a page is entirely older — the big cost saver for a daily monitor. 0 = off.

## `deduplicate` (type: `boolean`):

Collapse repeats on StepStone's numeric listing id across pages AND across every keyword in the run, so overlapping searches never bill you twice for the same job.

## `incrementalMode` (type: `boolean`):

The recruiter's daily 'new mandates' feed: remembers every listing id it has seen (in a NAMED key-value store that survives between runs) and outputs only ones that are new since last time. Pair with an Apify Schedule + Sort by date + Posted within 24h.

## `skipJobIds` (type: `array`):

Listing ids you already have — excluded before delivery, so you are not charged for them.

## `excludeSponsored` (type: `boolean`):

Drops rows flagged isSponsored or isTopJob. HONEST NOTE: re-measured 2026-09-06 across 430 listings on 6 keywords, StepStone's logged-out result pages returned these flags false 430/430, so today this toggle has nothing to drop. It is wired and correct, and will take effect if StepStone starts flagging paid placements publicly — but do not expect it to change your row count right now.

## `excludeAnonymous` (type: `boolean`):

Drops rows flagged isAnonymous (no employer named = worthless as a lead). HONEST NOTE: re-measured 2026-09-06 across 430 listings on 6 keywords, this flag was false 430/430 on StepStone's public result pages, so today it has nothing to drop. Wired and correct, but it will not change your row count right now.

## `excludeStaffingAgencies` (type: `boolean`):

Drop Arbeitnehmerüberlassung postings — your own competitors reposting other people's mandates. Applied server-side by selecting every OTHER contract type (measured 2026-09-06: "vertrieb" 22,138 listings unfiltered vs 18,754 with staffing agencies excluded, runs 88I8ff2ju9nnwzgkk / saZiCgRWX3RbKQgu0). If Arbeitnehmerüberlassung is the ONLY contract type you also ticked above, the two settings contradict each other: this exclusion wins, a warning names the contradiction, and every other contract type is selected.

## `includeSimilarJobs` (type: `boolean`):

Off by default, and you want it off. When a search is narrow StepStone pads the page with recommended/regional listings that do NOT match your filters (measured: a 1-result query returned 1 real + 24 padded). Off = you are only charged for real matches.

## `outputFields` (type: `array`):

Keep only these fields, in this order — e.g. companyName, title, location, jobUrl, datePosted. Leave empty for the full record. A name this actor does not produce is ignored and named in a log warning, with the full field list, so a typo cannot silently cost you a column. If NONE of the names you list exists, the rows would be empty — those rows are dropped, not delivered and not billed, and the run says so instead of charging you for blank rows.

## `compactOutput` (type: `boolean`):

Drop StepStone's ad-tracking positions (positionOnPage, positionAbsolute) for a clean CSV.

## `includeRawJson` (type: `boolean`):

Attach StepStone's untouched item object as `raw` on every row. For power users diffing against the source.

## `absoluteUrls` (type: `boolean`):

Return full https://www.stepstone.de/… links with StepStone's ?rltr= tracking suffix stripped. Turn off to keep the raw relative path.

## `webhookUrl` (type: `string`):

POST endpoint for new-listing alerts in incremental mode (Make / Zapier / n8n / your CRM). One JSON body per run.

## `slackWebhookUrl` (type: `string`):

Slack incoming-webhook URL for a formatted new-listings digest in incremental mode.

## `alertOnNewListing` (type: `boolean`):

In incremental mode, deliver each run's new listings to the webhook / Slack channels above.

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

Apify datacenter proxy is the default and the cheap path. Measured on the platform 2026-09-06 across 59 pages in 34 runs: 64.4% of pages landed first try, retries and the automatic RESIDENTIAL/DE rescue carried the rest, and 34 of 34 runs SUCCEEDED with 0 pages lost. If you would rather not spend the retries, switch this to RESIDENTIAL with country DE.

## `residentialFallback` (type: `boolean`):

If the datacenter proxy burns every retry on a page (measured: it happens during bad patches — one run lost page 1 across 8 consecutive fresh sessions), try that page once more through RESIDENTIAL / country DE instead of leaving a hole in your results. Only ever fires AFTER the datacenter attempts are exhausted, so it costs nothing on a healthy run. Ignored if you pin your own proxy groups above.

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

Parallel page fetches. Each worker keeps its own sticky proxy session. Keep it modest to stay polite.

## `maxRequestRetries` (type: `integer`):

Attempts per page before giving up on it. Each retry mints a FRESH proxy session — that is the reliability knob for this site, because a bad datacenter session fails every call it makes. Measured on the Apify platform 2026-09-06 over 59 pages in 34 runs: 64.4% of pages landed first try and the retries recovered every one of the rest (0 pages lost, 34/34 runs SUCCEEDED); one page needed all 8 datacenter attempts plus the RESIDENTIAL rescue. Default is 8; floor of 5 is enforced in code.

## `sessionRotateAfter` (type: `integer`):

Voluntarily retire a healthy proxy session after this many pages, on top of the automatic retire-on-error.

## `requestDelayMs` (type: `integer`):

Politeness gap each worker waits before fetching the next page.

## Actor input object example

```json
{
  "searchQueries": [
    "vertrieb"
  ],
  "radius": 30,
  "postedWithin": "any",
  "homeOffice": "any",
  "applicationMethod": "any",
  "jobLanguage": "any",
  "sortBy": "relevance",
  "startPage": 1,
  "maxPagesPerQuery": 0,
  "maxItemsPerQuery": 0,
  "maxItems": 50,
  "stopAfterDays": 0,
  "deduplicate": true,
  "incrementalMode": false,
  "excludeSponsored": false,
  "excludeAnonymous": false,
  "excludeStaffingAgencies": false,
  "includeSimilarJobs": false,
  "compactOutput": false,
  "includeRawJson": false,
  "absoluteUrls": true,
  "alertOnNewListing": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "residentialFallback": true,
  "maxConcurrency": 5,
  "maxRequestRetries": 8,
  "sessionRotateAfter": 40,
  "requestDelayMs": 250
}
```

# Actor output Schema

## `items` (type: `string`):

The dataset of scraped StepStone.de listings (one job per row).

# 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 = {
    "searchQueries": [
        "vertrieb"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/stepstone-de-jobs-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 = {
    "searchQueries": ["vertrieb"],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/stepstone-de-jobs-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 '{
  "searchQueries": [
    "vertrieb"
  ],
  "maxItems": 50
}' |
apify call scrapersdelight/stepstone-de-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/stepstone-de-jobs-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/5pa40rQetIGq6a0rQ/builds/3AMMnanWGh5Ci8Jv7/openapi.json
