# StepStone.de - German Jobs, Employers & Salaries (`abotapi/stepstone-de`) Actor

Scrape StepStone.de jobs by keyword, location, filters, or URL. Extract 35+ fields including employer, logo, location, posting date, home-office status, and optional full descriptions, GPS, employment type, and company profiles.

- **URL**: https://apify.com/abotapi/stepstone-de.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 job results

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## StepStone.de Jobs Scraper: Job Listings, Employer & Salary Data

StepStone.de Jobs Scraper turns StepStone.de, Germany's job platform, into structured job data. Search by keyword, location and eight working filters, or paste search-result links, and get 35 or more fields per job: title, employer, location, posting date, home-office status, labels and more. Turn on detail enrichment for the full German job description, GPS coordinates, employment type, valid-through date and company profile. Any salary figures the site publishes are in EUR. Export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Two ways to find jobs.** Build a search from keyword, location and eight working filters, or paste one or more StepStone.de search-result links and let the actor paginate them forward.
- **35+ listing fields per job**, with optional detail enrichment adding around 12 more: full description, GPS coordinates, structured address, employment type, contract type, valid-through date and a company profile (benefits, industries, size, logo).
- **Filters that are honestly documented.** Every filter option was measured against a real search so nothing advertised here silently does nothing, and the one filter StepStone.de does not actually support (salary) is deliberately left out of the input rather than shipped as a switch that does nothing.
- **Resume and recurring updates built in.** Continue an interrupted run by pasting its run or dataset id, or turn on incremental mode so scheduled runs return only NEW, UPDATED and REAPPEARED jobs, with UNCHANGED ones suppressed by default.
- **Best-effort recruiter contact harvesting.** When detail enrichment is on, phone numbers and email addresses are pulled from tel:/mailto: links and the job description text.
- **A refused run fails loudly instead of returning an empty dataset**, so "no jobs matched your filters" is never confused with "the site could not be read".
- **MCP connector export.** Pipe results into Notion, Linear, Airtable or Apify, selected straight from the input form once you authorize a connector.

### Use Cases

- **Recruiters and talent teams:** track newly posted roles from target employers or an industry across German cities.
- **Job seekers and career coaches:** monitor a keyword and location search daily so new postings are caught early.
- **Labor market research:** build historical datasets of demand by contract type, work model and experience level.
- **Sales and lead generation:** use employer and company-profile fields (industries, size, benefits) to prioritize outbound targets.
- **HR tech and job-board products:** pull structured German job data into your own product through the dataset or the API.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `rowType` / `brand` / `backend` | `"job"` / `"stepstone-de"` / `"unified"` |
| `jobId` / `harmonisedId` | `12345678` / `"12345678"` |
| `jobUrl` / `applyUrl` | `"https://www.stepstone.de/stellenangebote--Beispiel-Job-Berlin-Beispiel-GmbH--12345678-inline.html"` |
| `title` | `"Softwareentwickler (m/w/d)"` |
| `datePosted` / `publishFromDate` / `publishToDate` | `"2026-09-20"` / `"2026-09-20"` / `"2026-10-20"` |
| `sourceSite` / `sourceSearchUrl` | `"StepStone.de"` / search URL that found the job |
| `employer` | `{ "id": "9001", "name": "Beispiel GmbH", "url": "...", "logoUrl": "...", "isAnonymous": false }` |
| `location` | `{ "text": "Berlin", "postalCode": "10115", "locality": null, "region": null, "country": null, "latitude": null, "longitude": null, "streetAddress": null }` (detail-only fields filled when `fetchDetails` is on) |
| `salary` | `{ "rawText": null, "min": null, "max": null, "currency": null, "period": null }` (filled when the site serves a range, otherwise null; see the tip below) |
| `workFromHome` / `labels` / `skills` | `"Home-Office teilweise möglich"` or `null` / `["Vollzeit"]` / `[]` |
| `textSnippet` / `crossPostedCount` | short teaser text from the search-result card / `2` |
| `isSponsored` / `isHighlighted` / `isTopJob` / `isTrafficFromPartner` | `false` / `false` / `false` / `false` |
| `partnership` | `{ "isBackfilled": false, "isCrossPosted": false, "isPartnershipJob": false, "sourceSiteFriendlyName": null }` |
| `travelTime` | `null` |
| `description` / `descriptionText` | full HTML / plain-text job description (detail-only) |
| `employmentType` / `contractType` / `workType` | `["FULL_TIME"]` / `"permanent"` / `"vollzeit"` (detail-only) |
| `jobLocationType` / `applicantLocationRequirements` | `"TELECOMMUTE"` / `"Germany"` (detail-only) |
| `validThrough` / `externalId` | `"2026-11-20T00:00:00"` / `"REQ-2026-001"` (detail-only) |
| `directApply` / `applyType` | `true` / `"DirekteBewerbung"` (detail-only) |
| `contactPhones` / `contactEmails` | `["+4930XXXXXXX"]` / `[]` (best-effort, detail-only) |
| `company` | `{ "id": "9001", "name": "Beispiel GmbH", "jobsCount": 12, "founded": "2005", "size": "201-500", "industries": ["IT"], "benefits": ["Home-Office"], "description": "..." }` (detail-only) |
| `companyName` / `companyId` / `companyUrl` / `companyLogoUrl` / `postCode` | employer name, id, profile link, logo and postal code from the search card |
| `unifiedSalary` / `section` / `topLabels` / `metaData` | structured salary when published, result section, highlighted labels, extra card metadata |
| `detailFetched` / `scrapedAt` | `true` / ISO timestamp of this run |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | `"NEW"` / `[]` / ISO timestamp / ISO timestamp (incremental mode only) |

### How to Use

1. Pick a **mode**: `search` (build a search from filters) or `url` (paste one or more StepStone.de search-result links).
2. In search mode, set **Locations** and **Keywords**, and switch on any of the eight working filters. In URL mode, filter fields are ignored; each pasted link is paginated forward from wherever it starts. URL mode accepts search links (city, keyword, region, remote, paginated, filtered or sorted, with pasted filter parameters kept), company pages, legacy /work/ links and single job pages (a job-page link needs Fetch full job details switched on). The field is prefilled in the Console with one example of each of these link shapes; replace the list with your own links. In URL mode, a run with no links fails with a clear message.
3. Turn on **Fetch full job details** for the extra description, geo and company fields (job pages are fetched over Residential DE), and set **Max jobs** to control run size and cost.
4. Click **Start**. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword and location:**

```json
{
  "mode": "search",
  "locations": ["Berlin"],
  "keywords": "Softwareentwickler",
  "workType": "vollzeit",
  "maxListings": 50
}
```

**Daily monitoring for new postings only:**

```json
{
  "mode": "search",
  "locations": ["München"],
  "keywords": "Pflegefachkraft",
  "datePosted": "1",
  "incrementalMode": true,
  "maxListings": 100
}
```

**Full detail enrichment for one city:**

```json
{
  "mode": "search",
  "locations": ["Hamburg"],
  "keywords": "Marketing Manager",
  "fetchDetails": true,
  "maxListings": 30,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "DE"
  }
}
```

**Paste search-result links:**

```json
{
  "mode": "url",
  "urls": ["https://www.stepstone.de/jobs/ingenieur/in-frankfurt-am-main"],
  "maxListings": 50
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/stepstone-de").call(run_input={
    "mode": "search", "locations": ["Berlin"], "keywords": "Softwareentwickler", "maxListings": 20
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["employer"]["name"], job["location"]["text"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/stepstone-de').call({
    mode: 'search', locations: ['Berlin'], keywords: 'Softwareentwickler', maxListings: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Filters that actually narrow results (measured)

Every filter was measured live against the search "python" in Berlin, which returned 310 jobs with no filter applied. These are illustrative of how much each filter narrows a result set; your own search will differ.

| Field | Value | Effect (of the 310 measured) |
| --- | --- | --- |
| `workType` | `vollzeit` / `teilzeit` | 278 / 57 |
| `contractType` | `permanent` / `student` / `fixed-term` / `internship` / `freelance` / `apprenticeship` / `trainee` / `doctorate` | 261 / 19 / 6 / 6 / 5 / 4 / 3 / 3 |
| `datePosted` | `1` / `3` / `7` / `14` / `30` (days) | 22 / 70 / 116 / 254 / 309 |
| `workFromHome` | `remote` / `hybrid` | 15 / 208 |
| `jobLanguage` | `de` / `en` | 222 / 86 |
| `experienceLevel` | `experienced` / `entry` / `management` | 264 / 83 / 6 |
| `applyMethod` | `company-website` / `quick-apply` | 170 / 112 |
| `sortBy` | `relevance` / `date` | changes order, not the match count |

All eight filters combined can narrow a search sharply: `vollzeit` plus permanent plus last 7 days plus German plus hybrid plus experienced plus company-website plus newest-first returned 27 of the 310 in that same measurement.

There is deliberately no salary filter (the site's own salary facet ships no selectable options), no employer-vs-agency filter, and no salary or distance sort. Ad language exists only as a search filter, not as a per-job output field, because the job pages themselves carry no language attribute.

#### What StepStone.de does not offer

`salary` and `unifiedSalary` are filled when the site serves a salary range (21 of 25 jobs in a 2026-11-04 sample of `python` in Berlin, saved in `evidence/live_rows_python_berlin_2026-11-04.json`) and are `null` otherwise. In that sample all 21 ranges were marked `Predicted` in `unifiedSalary.salaryType`, so treat them as the site's estimates rather than employer-stated figures. `skills`, `postalCode`, `travelTime` and `crossPostedCount` are also frequently empty; the site itself serves them as null, and detail enrichment does not fill them in.

#### Scanned vs emitted caps

**Max jobs** counts jobs scanned, not jobs returned. In incremental mode, a quiet run that suppresses most of a page stops paging at the same depth as a noisy one instead of paging deeper to backfill the quota, which is what makes recurring runs genuinely cheaper.

**Tip: EXPIRED rows are not limited by Max jobs.** Once a run has fully scanned a tracked search, every previously-tracked job that is no longer found is returned as an EXPIRED row in that same run, with no relation to your Max jobs setting. If a state key has not run in a while and many jobs disappeared at once, expect one large EXPIRED batch that can be bigger than Max jobs. Only the NEW/UPDATED/REAPPEARED collection itself is bounded by Max jobs.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset id and the actor skips every job already collected there, so you don't pay twice. This works the same way for both search mode and URL mode.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search. Each job is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED` (only after a run that scanned the whole tracked search, and only with `emitExpired` on). `stateKey` names or shares the stored state; leave it empty to let the actor derive one automatically from your mode, locations or pasted links, filters and the Fetch full job details setting. `ignoreFieldsForChanges` lists extra fields to ignore when deciding UPDATED; it does not start a fresh baseline. Changing any of the settings above (for example switching Fetch full job details on) starts a fresh baseline, so the next run returns every job as NEW again.
- **Platform migration or Resurrect** restarts the same run from its saved position, without re-pushing or re-billing jobs already collected before the interruption.

**Tip: a job re-billed as UPDATED right after a resumed monitoring campaign starts is expected.** If you start a NEW incremental-mode campaign using `resumeFromRunId` to seed it from an older run, those resumed jobs are added to the new baseline by id only (their content is not stored, since it was never re-scraped this run). The FIRST time incremental mode later re-scrapes one of those jobs, it is classified UPDATED (with every field listed in `changedFields`) even if the job itself did not change, simply because there is nothing to compare it against yet. Treat that first appearance as informational rather than a real change.

**Tip: keep State key empty unless two runs really are the same search.** A manually shared State key across two differently configured runs (different locations, keywords or filters) makes them share one baseline. Jobs that only exist in one of those searches can then be marked EXPIRED purely because the other run's search never saw them, and then come back as REAPPEARED (and are billed again) when the first search runs next, so the same jobs flip back and forth run after run. Only reuse a State key across runs with identical search settings.

**Tip: for a recurring URL-mode campaign, paste the first-page link.** A pasted search-result link that starts on a later page (one with `?page=2` or higher) skips every earlier page on every run. Combined with incremental mode and Emit expired jobs, such a run still counts as a complete scan once it reaches the site's last page, even though it never checked the skipped earlier pages. A tracked job that moves up onto one of those skipped pages (because jobs ahead of it were removed or re-ranked) is then wrongly marked EXPIRED while it is still live, and if the link shares a State key with a first-page link, every job on the skipped pages is marked EXPIRED. Use the plain first-page link for any URL you plan to schedule with incremental mode.

**Tip: with several locations and a small Max jobs, raise the cap or split the run.** Max jobs is split evenly between locations (rounded up), and a share one location does not use is not passed on to the others: with Max jobs 10 across a location that has 2 jobs and one that has 30, the run returns 7. A cross-posted job that falls beyond one location's share is also treated as already handled, so it is left out of every later location's results too, even when that location still had room for it. Raise Max jobs, or run each location as its own actor run, if you need full coverage in a multi-location search. (This only affects how many jobs are returned; it never causes a false EXPIRED, because a run cut short by the cap skips EXPIRED detection.)

**Tip: a run that was migrated or resurrected mid-way is less exact in two ways.** The restarted run counts Max jobs again from zero, so the run as a whole can return more jobs than Max jobs (up to the jobs collected before the interruption plus a full Max jobs after it). In incremental mode, the jobs pushed before the interruption are protected from being re-pushed in that run, but their NEW or UPDATED classification is not saved to the stored baseline, so the next scheduled run reports them as NEW or UPDATED (and bills them) a second time. That run also skips EXPIRED detection. If exact counts matter, check the next scheduled run after any migrated run.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify, Settings, Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** (`mcpConnectors`) input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.
4. `maxNotifyListings` caps how many items are sent to each connector per run; it does not affect the dataset.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

#### Extra listing fields

Every row also carries `companyId`, `companyName`, `companyUrl`, `companyLogoUrl`, `isAnonymous`, `postCode`, a `unifiedSalary` object (`min`, `max`, `currency`, `period`, `salaryAvailable`, `salaryType`), `section`, `topLabels`, `metaData` (`positionOnPage`, `positionAbsolute`), `hasFuturePosting` and `cvToJobScore`, exactly as the site serves them (empty when it serves nothing). `section`, `topLabels`, `metaData`, `hasFuturePosting` and `cvToJobScore` are ranking data and never mark a job as UPDATED in incremental mode. Contact emails and phones are already collected from detail pages when Fetch full job details is on. Related and regional jobs are returned like the site shows them; filter on `section` to keep only `main`.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (build from filters) or `url` (paste links). |
| `locations` | array | prefilled `["Berlin"]` | Cities, states, or regions in Germany (search mode). One search runs per location; empty means all of Germany. |
| `keywords` | string | (none) | Job title, skill, or company to search for (search mode). |
| `workType` | string | `any` | `any`, `vollzeit` (full time), or `teilzeit` (part time). |
| `contractType` | string | `any` | `any`, `permanent`, `fixed-term`, `trainee`, `freelance`, `apprenticeship`, `internship`, `student`, or `doctorate`. |
| `datePosted` | string | `0` | Days since posting: `0` (any time), `1`, `3`, `7`, `14`, or `30`. |
| `workFromHome` | string | `any` | `any`, `remote`, or `hybrid`. |
| `jobLanguage` | string | `any` | `any`, `de` (German), or `en` (English) advert language. |
| `experienceLevel` | string | `any` | `any`, `entry`, `experienced`, or `management`. |
| `applyMethod` | string | `any` | `any`, `quick-apply`, or `company-website`. |
| `sortBy` | string | `relevance` | `relevance` or `date` (most recent). |
| `urls` | array | empty list (the Console prefills one example link per supported shape) | StepStone.de search, company or job-page URLs (URL mode). A page number in the URL is honoured as the starting point. |
| `fetchDetails` | boolean | `false` | Fetch detail pages for the full description, GPS, employment type, valid-through date and company profile. Job pages are always fetched over Residential DE (or your own proxy URLs); on a plan without Residential access detail enrichment is skipped with a warning and the listing fields are still returned. |
| `maxListings` | integer | `20` | Total jobs to collect across all searches. `0` = unlimited (bounded by Max pages). |
| `maxItems` | integer | none | Alias of `maxListings`; it overrides it only when set, otherwise `maxListings` applies. |
| `discipline` | string | none | Site discipline name such as `IT` or `Ingenieurwesen`. |
| `sector` | string | none | Numeric sector id from the site's sector filter, for example `21000`. |
| `skillIds` | array | none | Numeric skill ids from the site's skill filter, for example `673565`. |
| `cityId` | string | none | Numeric city id from the site's city filter, for example `419239`. |
| `maxPages` | integer | `0` | Safety limit on pages walked per location/URL (25 jobs per page). `0` = walk the whole catalogue. |
| `maxResidentialRequests` | integer | `0` | Safety cap on Residential proxy requests this run may use. `0` = unlimited. |
| `resumeFromRunId` | string | (none) | Continue a previous run or dataset by id; returns only the delta of jobs not already collected there. |
| `incrementalMode` | boolean | `false` | Turn on for recurring monitoring: later runs return only NEW, UPDATED and REAPPEARED jobs. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only: field names (or dotted paths such as `employer.logoUrl`) left out of UPDATED detection and `changedFields`; they stay in the output and never change the saved-state key. |
| `stateKey` | string | (none) | Name or share an incremental-mode state. Leave empty to derive one automatically per search/filter setup. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) UNCHANGED jobs. |
| `emitExpired` | boolean | `false` | Also return (and bill) EXPIRED jobs, only after a run that fully scanned the tracked search. |
| `proxy` | object | prefilled Residential, country DE | Connection settings. Residential with country DE is the most reliable for search pages; job pages always use Residential DE. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each job to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which job pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items exported per connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "rowType": "job",
  "brand": "stepstone-de",
  "backend": "unified",
  "sourceSearchUrl": "https://www.stepstone.de/jobs/vollzeit/softwareentwickler/in-berlin",
  "jobId": 12345678,
  "harmonisedId": "12345678",
  "jobUrl": "https://www.stepstone.de/stellenangebote--Beispiel-Job-Berlin-Beispiel-GmbH--12345678-inline.html",
  "applyUrl": "https://www.stepstone.de/stellenangebote--Beispiel-Job-Berlin-Beispiel-GmbH--12345678-inline.html",
  "sourceSite": "StepStone.de",
  "title": "Softwareentwickler (m/w/d)",
  "datePosted": "2026-09-20",
  "publishFromDate": "2026-09-20",
  "publishToDate": "2026-10-20",
  "employer": {
    "id": "9001",
    "name": "Beispiel GmbH",
    "url": "https://www.stepstone.de/cmp/de/beispiel-gmbh-9001",
    "logoUrl": "https://images.stepstone.de/beispiel-gmbh-logo.png",
    "isAnonymous": false
  },
  "location": {
    "text": "Berlin",
    "postalCode": "10115",
    "locality": "Berlin",
    "region": "Berlin",
    "country": "DE",
    "latitude": 52.52,
    "longitude": 13.405,
    "lat": 52.52,
    "lng": 13.405,
    "streetAddress": null
  },
  "salary": { "rawText": null, "min": null, "max": null, "currency": null, "period": null },
  "workFromHome": "Home-Office teilweise möglich",
  "labels": ["Vollzeit"],
  "skills": [],
  "textSnippet": "Wir suchen einen erfahrenen Softwareentwickler zur Verstärkung unseres Teams...",
  "crossPostedCount": 1,
  "isSponsored": false,
  "isHighlighted": false,
  "isTopJob": false,
  "isTrafficFromPartner": false,
  "partnership": {
    "isBackfilled": false,
    "isCrossPosted": false,
    "isPartnershipJob": false,
    "sourceSiteFriendlyName": null
  },
  "travelTime": null,
  "description": "<p>Wir suchen einen erfahrenen Softwareentwickler...</p>",
  "descriptionText": "Wir suchen einen erfahrenen Softwareentwickler...",
  "validThrough": "2026-11-20T00:00:00",
  "employmentType": ["FULL_TIME"],
  "industry": "Informationstechnologie",
  "directApply": true,
  "applyType": "DirekteBewerbung",
  "jobLocationType": null,
  "applicantLocationRequirements": null,
  "externalId": "REQ-2026-001",
  "contractType": "permanent",
  "workType": "vollzeit",
  "company": {
    "id": "9001",
    "name": "Beispiel GmbH",
    "jobsCount": 12,
    "founded": "2005",
    "size": "201-500",
    "industries": ["Informationstechnologie"],
    "benefits": ["Home-Office", "Weiterbildung"],
    "description": "Beispiel GmbH ist ein fiktives Beispielunternehmen."
  },
  "contactPhones": [],
  "contactEmails": [],
  "detailFetched": true,
  "scrapedAt": "2026-09-30T09:00:00+00:00"
}
```

### Plan Requirement

Search-result pages load most reliably over Residential with country DE, which is the prefilled connection; search pages try a Datacenter connection first only when it is the selected one, and the run moves to Residential DE when they are refused repeatedly. Job-detail pages (`fetchDetails`) always use Residential DE (or your own proxy URLs): in our measurements Datacenter exits reset job-page connections. On a plan without Residential access, detail enrichment is skipped with a warning and only listing fields are returned. If the selected connection is refused, the run logs "The selected connection is unavailable to this run." and "Apify free plans do not include residential proxies.", continues on a backup connection, and then logs "Served over the backup connection (the default connection was unavailable)." Some pages may still be unavailable on a backup connection. If job pages are refused repeatedly, detail enrichment is switched off for the rest of the run and the detail surcharge is not charged for jobs that were not enriched. Setting the environment variable `STEPSTONE_DE_ALTERNATE_CONNECTION=0` disables the backup connection. `maxResidentialRequests` puts a hard ceiling on Residential usage for a run if you want one.

### FAQ

#### How much does it cost?

You pay per job returned (one dataset item per job), plus the `detail-enrichment` event, charged once per job whose detail page was fetched and actually delivered when **Fetch full job details** is on. Jobs suppressed by incremental mode, or whose detail page could not be read, are not charged the detail event. The **Pricing** tab shows the current rates. Use **Max jobs** to cap the cost of any run.

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

This actor collects only publicly available job listing data. You are responsible for how you use it: follow StepStone.de's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Job postings are generally facts, but employer logos and images may be subject to third-party rights.

#### Can I get only new job postings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only NEW, UPDATED and REAPPEARED jobs, and unchanged ones are not billed unless you turn on Emit unchanged.

#### When is the salary field filled?

StepStone.de serves a salary range for many adverts: 21 of 25 jobs in the 2026-11-04 sample (`python` in Berlin) carried `salary` and `unifiedSalary` values, all marked `Predicted` (the site's estimate), and the rest were `null`. There is no salary filter or salary sort, because the site does not offer one.

#### Why did a large batch of jobs suddenly come back as EXPIRED, more than my Max jobs setting?

EXPIRED detection only runs after a complete scan of a tracked search, and the EXPIRED batch itself is not limited by Max jobs: every previously-tracked job no longer found is returned in that one run. If a monitoring campaign has not run in a while, expect a correspondingly large EXPIRED batch the next time it runs.

#### Can incremental mode mark a job UPDATED even though nothing about it changed?

It can, in one specific case: right after you bootstrap a new monitoring campaign with `resumeFromRunId`, the resumed jobs are known only by id, not by content. The first time incremental mode later re-scrapes one of those jobs, it is classified UPDATED because there is nothing stored yet to compare it against, even if the job itself is unchanged. Treat that one occurrence as informational.

#### Why did my run fail instead of returning an empty dataset?

If the site refuses every request, the run stops with a clear message so "no jobs matched" is never confused with "nothing could be read". Run it again in a few minutes. If only some locations or URLs could be read, you still get what was found.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/totaljobs-com-scraper"><b>Totaljobs Scraper</b></a><br>Scrape UK job listings from Totaljobs.com by keyword, location, filters, or URL. Extract...</td><td>💼 <a href="https://apify.com/abotapi/ziprecruiter-com-scraper"><b>ZipRecruiter Scraper</b></a><br>Scrape ZipRecruiter jobs with titles, companies, salaries, locations, benefits, apply...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/careerbuilder-scraper"><b>CareerBuilder Scraper</b></a><br>Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile...</td><td>💼 <a href="https://apify.com/abotapi/jobs-cz-scraper"><b>Jobs.cz</b></a><br>Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/caterer-com-scraper"><b>Caterer.com Scraper</b></a><br>Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and...</td><td>💼 <a href="https://apify.com/abotapi/jobteaser-scraper"><b>JobTeaser Scraper</b></a><br>Scrape early-career jobs and internships from JobTeaser across Europe. Search with 15...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/stepstone-de/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

How to find jobs: build a search from filters, or paginate URLs you paste.

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

Cities, states, or regions in Germany (Search mode). Examples: 'Berlin', 'München', 'Hamburg', 'Frankfurt am Main', 'Bayern'. One search runs per location. Leave empty for all of Germany.

## `keywords` (type: `string`):

Job title, skill, or company to search for. Examples: 'Softwareentwickler', 'Ingenieur', 'Marketing Manager', 'Pflegefachkraft'. Leave empty to list all jobs in the location.

## `workType` (type: `string`):

Full-time (Vollzeit) or part-time (Teilzeit). Measured on 'python' in Berlin: 310 jobs unfiltered, 278 Vollzeit, 57 Teilzeit.

## `contractType` (type: `string`):

Kind of contract offered. Measured on 'python' in Berlin (310 unfiltered): permanent 261, student/Werkstudent 19, fixed-term 6, internship 6, freelance 5, apprenticeship 4, trainee 3, doctorate 3.

## `datePosted` (type: `string`):

Only jobs posted within this many days. Measured on 'python' in Berlin (310 unfiltered): 24 hours 22, 7 days 116, 14 days 254, 30 days 309. Last 3 days measured on 'python' in Berlin: 70 of 355.

## `workFromHome` (type: `string`):

Remote-working arrangement. Measured on 'python' in Berlin (310 unfiltered): fully remote 15, hybrid 208.

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

Language the job advert is written in. Measured on 'python' in Berlin (310 unfiltered): German 222, English 86. Note StepStone.de exposes ad language only as a search filter — there is no per-advert language field to return in the results.

## `experienceLevel` (type: `string`):

Seniority the advert asks for. Measured on 'python' in Berlin (310 unfiltered): with experience 264, without experience 83, with staff responsibility 6.

## `applyMethod` (type: `string`):

Whether the advert takes applications on StepStone or sends you to the company's own site. Measured on 'python' in Berlin (310 unfiltered): quick apply 112, company website 170.

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

Result ordering. StepStone.de offers exactly these two; sorting changes the order, not how many jobs match. (There is no salary or distance sort on this site.)

## `discipline` (type: `string`):

Optional StepStone discipline name, for example IT or Ingenieurwesen. Narrows results (python in Berlin: IT 229 of 355).

## `sector` (type: `string`):

Optional numeric sector id from the site's sector filter (for example 21000). Narrows results (python in Berlin: 189 of 355).

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

Optional numeric skill ids from the site's skill filter (for example 673565). Narrows results.

## `cityId` (type: `string`):

Optional numeric city id from the site's city filter (for example 419239). Narrows results (python in Berlin: 312 of 355).

## `urls` (type: `array`):

StepStone.de search-result, company-page (/cmp/...) or single job-page URLs (URL mode). Multi-URL supported. Filter fields above are ignored; filter parameters pasted in a link are kept. A page number in the URL is honoured as the starting point. Single job-page links need 'Fetch full job details' on.

## `fetchDetails` (type: `boolean`):

Fetch detail pages to add the full description, GPS coordinates, structured address, employment type, valid-through date, and company profile. Adds ~12 fields. Costs more per job. Job pages are fetched over Residential DE; on a plan without Residential access this is skipped with a warning. Off = fast, cheap, 35 listing fields.

## `maxListings` (type: `integer`):

Total jobs to collect across all searches. This is the main limit. Set 0 for unlimited (bounded by Max pages).

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

Alias of Max jobs, for users coming from other StepStone scrapers. It overrides Max jobs only when set; leave it empty to use Max jobs.

## `maxPages` (type: `integer`):

Optional safety limit on pages walked per location/URL (25 jobs per page). Leave at 0 to walk the whole catalogue: the run stops at Max jobs, the site's own last page, or a repeat-page guard, not an artificial page cap.

## `maxResidentialRequests` (type: `integer`):

Safety cap on how many Residential proxy requests this run may use (listing pages and detail pages). 0 = unlimited. When the cap is reached, listing falls back to datacenter and detail enrichment stops, so a run can never overspend on residential bandwidth.

## `proxy` (type: `object`):

Connection settings. StepStone.de accepts Apify Residential with country DE most reliably (the default). Job-detail pages always use Residential DE (or your own proxy URLs); search pages try Datacenter first only when it is the selected connection and move to Residential DE where the plan allows it if refused repeatedly.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Jobs already in that dataset are skipped, so this run returns only NEW jobs (a delta). Combine both runs' datasets for the full set. Max jobs then counts only the new jobs. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching jobs as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs. Turn on "Emit unchanged" or "Emit expired" only when you also want those jobs returned (and billed). State is kept separately for each distinct search/filter setup (or by State key below).

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

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from your search/filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max jobs or Max pages capped it, or when Resume was used. This returns — and bills — extra synthetic rows.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names to leave out of UPDATED detection and changedFields, on top of the built-in exclusions (ad-placement flags, cross-post count, source search link, detail flag, scrape time). Use a plain name such as textSnippet, or a dotted path for a nested field such as employer.logoUrl. The fields stay in the output. Changing this never re-bills your saved jobs.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "Berlin"
  ],
  "workType": "any",
  "contractType": "any",
  "datePosted": "0",
  "workFromHome": "any",
  "jobLanguage": "any",
  "experienceLevel": "any",
  "applyMethod": "any",
  "sortBy": "relevance",
  "urls": [
    "https://www.stepstone.de/jobs/in-berlin",
    "https://www.stepstone.de/jobs/python/in-berlin",
    "https://www.stepstone.de/jobs/python",
    "https://www.stepstone.de/jobs/in-berlin?page=2",
    "https://www.stepstone.de/jobs/vollzeit/python/in-berlin?ag=age_7&sort=2",
    "https://www.stepstone.de/jobs/softwareentwickler/in-hamburg?ct=222",
    "https://www.stepstone.de/jobs/python/in-m%C3%BCnchen",
    "https://www.stepstone.de/jobs/in-bayern",
    "https://www.stepstone.de/jobs/remote",
    "https://www.stepstone.de/stellenangebote--Backend-Entwickler-Python-m-w-d-Data-Engineer-Python-Berlin-Mannheim-Frankfurt-Stuttgart-Karlsruhe-Ratbacher-GmbH--13818030-inline.html",
    "https://www.stepstone.de/job/13818030",
    "https://www.stepstone.de/cmp/de/ratbacher-gmbh-46710/jobs",
    "https://www.stepstone.de/cmp/de/StepStone-GmbH-148733/jobs.html",
    "https://stepstone.de/jobs/in-berlin",
    "https://www.stepstone.de/work/python/in-berlin"
  ],
  "fetchDetails": false,
  "maxListings": 20,
  "maxResidentialRequests": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

Individual job records with 35+ fields, plus ~12 detail fields when fetchDetails is enabled.

## `output` (type: `string`):

Run summary: total jobs, detail pages fetched, pages walked, residential usage, duration.

# 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": "search",
    "locations": [
        "Berlin"
    ],
    "urls": [
        "https://www.stepstone.de/jobs/in-berlin",
        "https://www.stepstone.de/jobs/python/in-berlin",
        "https://www.stepstone.de/jobs/python",
        "https://www.stepstone.de/jobs/in-berlin?page=2",
        "https://www.stepstone.de/jobs/vollzeit/python/in-berlin?ag=age_7&sort=2",
        "https://www.stepstone.de/jobs/softwareentwickler/in-hamburg?ct=222",
        "https://www.stepstone.de/jobs/python/in-m%C3%BCnchen",
        "https://www.stepstone.de/jobs/in-bayern",
        "https://www.stepstone.de/jobs/remote",
        "https://www.stepstone.de/stellenangebote--Backend-Entwickler-Python-m-w-d-Data-Engineer-Python-Berlin-Mannheim-Frankfurt-Stuttgart-Karlsruhe-Ratbacher-GmbH--13818030-inline.html",
        "https://www.stepstone.de/job/13818030",
        "https://www.stepstone.de/cmp/de/ratbacher-gmbh-46710/jobs",
        "https://www.stepstone.de/cmp/de/StepStone-GmbH-148733/jobs.html",
        "https://stepstone.de/jobs/in-berlin",
        "https://www.stepstone.de/work/python/in-berlin"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "DE"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/stepstone-de").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": "search",
    "locations": ["Berlin"],
    "urls": [
        "https://www.stepstone.de/jobs/in-berlin",
        "https://www.stepstone.de/jobs/python/in-berlin",
        "https://www.stepstone.de/jobs/python",
        "https://www.stepstone.de/jobs/in-berlin?page=2",
        "https://www.stepstone.de/jobs/vollzeit/python/in-berlin?ag=age_7&sort=2",
        "https://www.stepstone.de/jobs/softwareentwickler/in-hamburg?ct=222",
        "https://www.stepstone.de/jobs/python/in-m%C3%BCnchen",
        "https://www.stepstone.de/jobs/in-bayern",
        "https://www.stepstone.de/jobs/remote",
        "https://www.stepstone.de/stellenangebote--Backend-Entwickler-Python-m-w-d-Data-Engineer-Python-Berlin-Mannheim-Frankfurt-Stuttgart-Karlsruhe-Ratbacher-GmbH--13818030-inline.html",
        "https://www.stepstone.de/job/13818030",
        "https://www.stepstone.de/cmp/de/ratbacher-gmbh-46710/jobs",
        "https://www.stepstone.de/cmp/de/StepStone-GmbH-148733/jobs.html",
        "https://stepstone.de/jobs/in-berlin",
        "https://www.stepstone.de/work/python/in-berlin",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "DE",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/stepstone-de").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": "search",
  "locations": [
    "Berlin"
  ],
  "urls": [
    "https://www.stepstone.de/jobs/in-berlin",
    "https://www.stepstone.de/jobs/python/in-berlin",
    "https://www.stepstone.de/jobs/python",
    "https://www.stepstone.de/jobs/in-berlin?page=2",
    "https://www.stepstone.de/jobs/vollzeit/python/in-berlin?ag=age_7&sort=2",
    "https://www.stepstone.de/jobs/softwareentwickler/in-hamburg?ct=222",
    "https://www.stepstone.de/jobs/python/in-m%C3%BCnchen",
    "https://www.stepstone.de/jobs/in-bayern",
    "https://www.stepstone.de/jobs/remote",
    "https://www.stepstone.de/stellenangebote--Backend-Entwickler-Python-m-w-d-Data-Engineer-Python-Berlin-Mannheim-Frankfurt-Stuttgart-Karlsruhe-Ratbacher-GmbH--13818030-inline.html",
    "https://www.stepstone.de/job/13818030",
    "https://www.stepstone.de/cmp/de/ratbacher-gmbh-46710/jobs",
    "https://www.stepstone.de/cmp/de/StepStone-GmbH-148733/jobs.html",
    "https://stepstone.de/jobs/in-berlin",
    "https://www.stepstone.de/work/python/in-berlin"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/stepstone-de --silent --output-dataset

```

## MCP server setup

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

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/Fn4PQWvp0EjyUyCv7/builds/qCd7AX8fWWA0XL5JT/openapi.json
