# SmartRecruiters Jobs Scraper - Any Company's Job Postings (`neverempty/smartrecruiters-jobs-scraper`) Actor

For recruiters, talent intelligence and competitor hiring tracking: every public job of any company on SmartRecruiters - Bosch, Ubisoft, CERN and thousands more - one row per job with title, department, city, country, remote and hybrid flags and level. A wrong ID is never reported as 0 jobs.

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

## Pricing

from $4.38 / 1,000 job returneds

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?

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

## SmartRecruiters Jobs Scraper - Any Company's Job Postings

**For recruiters, sales teams selling into hiring companies, and anyone building a jobs feed**: every public job posting of any company that hires through **SmartRecruiters** — Bosch, Ubisoft, CERN, Delivery Hero, ServiceNow, Accor, Canva and thousands more — read straight from SmartRecruiters' own public Posting API. Up to **200** companies per run.

**Measured on 2026-09-11:** Ubisoft (`Ubisoft2`) **291** ads, Bosch (`BoschGroup`) **4,839**. Bosch reads all 4,839 in **49 requests in about 17 seconds**; with descriptions on, all **4,843** ads with their full text took **301 seconds** on 512 MB of memory in a production run. Fill rates that day: `title`, `function`, `industry`, `employmentType`, `experienceLevel`, `city` and `countryCode` on **100%** of both; `region` on **99%** (Ubisoft) and **74%** (Bosch); `department` **100%** at Ubisoft and **0%** at Bosch — the source, not the scraper.

**A wrong company ID is never reported as "0 jobs".** SmartRecruiters' API answers **HTTP 200 with 0 postings for any company ID, including IDs that do not exist** — `IKEA`, `Atlassian`, `Zalando` and a made-up ID all look exactly like a real company with no openings. This Actor checks: when a company returns 0, it looks the ID up on SmartRecruiters' careers site, where an ID that is not a SmartRecruiters company gets HTTP 404 (50 company IDs measured, 2026-09-11).

**And it fixes the three filters that silently return nothing.** SmartRecruiters only accepts **lower-case** country codes (`de` returned 780 Bosch jobs, the correct number; `DE` returned **0**) and returns **0** when two countries are sent together. Its `city` filter is case-sensitive (`Stuttgart` **57**, `stuttgart` **0**) and its `location` parameter is silently ignored — so city and location are filtered here instead.

One row per job: title, department, function, industry, employment type, experience level, city, region, country, remote and hybrid flags, posting date and a link to the job page — plus, if you ask for it, the full job description. `postedAt` is when that ad was released and `firstPostedAt` is the earliest ad with the same reference and title, because SmartRecruiters' release date is a real publication date rather than a "last updated" stamp. Export as JSON, CSV or Excel.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by SmartRecruiters. All product names are trademarks of their respective owners.*

***

### A wrong company ID is never reported as "0 jobs"

SmartRecruiters' API answers **HTTP 200 with 0 postings for any company ID - including IDs that do not exist.** `IKEA`, `Atlassian`, `Zalando` and a made-up ID all come back exactly like a real company that simply has no openings. A scraper that trusts that answer tells you a company "is not hiring" when you only typed its ID wrong.

This Actor checks. When a company returns 0 postings, it looks the ID up on SmartRecruiters' careers site: an ID that is not a SmartRecruiters company gets HTTP 404 there (50 company IDs measured, 2026-09-11). Where the careers site answers but lists nothing, the row says so plainly - SmartRecruiters answers that way for real companies with no openings (Visa, Spotify) and also for generic words such as `search`, `api` or `jobs`, so this Actor does not claim the company exists.

| `rowType` | `ok` | What it means | Charged? |
|---|---|---|---|
| `job` | `true` | A real open job | Yes |
| `no-open-jobs` | `true` | The company's SmartRecruiters careers site lists job slots, but the API has no public postings | **No** |
| `no-matching-jobs` | `true` | The company has jobs, but none match your filters - the row says how many it has | **No** |
| `company-not-found` | `false` | No SmartRecruiters company has this ID. The row tells you where to find the right one | **No** |
| `company-unverified` | `false` | 0 postings, and it cannot be confirmed that the ID is a company (an empty careers page, or the check itself failed) | **No** |
| `posting-closed` | `false` | A posting that was in the list but whose page no longer exists when its description was read - not returned as an open job | **No** |
| `incomplete` | `false` | Not every page of the listing could be read; the jobs that were read are returned | **No** |
| `more-jobs-available` | `true` | `maxResultsPerCompany` cut the list; says how many were left out | **No** |
| `duplicate-input` | `false` | The same company was given twice (IDs are not case-sensitive); its jobs are returned once | **No** |
| `bad-input` | `false` | Not a SmartRecruiters company, or not a country code (for example `Germany` or `XX`) | **No** |
| `unavailable` | `false` | SmartRecruiters could not be reached after retries | **No** |
| `budget-reached` / `not-queried-time-limit` | `false` | The run hit your maximum charge or its time limit before this company | **No** |

You are charged **only for `job` rows.**

***

### One job, several ads - counted once when they really are the same ad

SmartRecruiters often publishes one job as several ads sharing a reference number - one ad per city, or a French and an English ad. With `oneRowPerJob` on (the default), **ads with the same reference number and the same title** become one row, taken from the company's default ad, with the other ads (city, language, date, ID) listed in `otherAdsForSameJob` - and you pay once. Ads whose title differs stay separate rows: companies reuse a reference number for differently titled postings (Delivery Hero, Equinox), and a translated ad ("Programmeur Outils" / "Tools Programmer") has a different title too. Turn the option off to get one row per ad.

Measured 2026-09-11:

| Company | Ads | Rows (one row per job) | Rows that merge more than one ad | Most ads in one row |
|---|---|---|---|---|
| Ubisoft (`Ubisoft2`) | 291 | 280 | 11 | 2 |
| Bosch (`BoschGroup`) | 4,839 | 4,808 | 21 | 10 |

`postedAt` is when that ad was released and `firstPostedAt` is the earliest ad with the same reference and title. SmartRecruiters' release date is the real publication date, not a "last updated" stamp: across Bosch's 1,209 oldest postings, none carries a date from the last 30 days. The "posted within days" filter uses **each ad's own date**, so a job re-posted in a new city this week is returned (from the new ad), and `firstPostedAt` tells you it is a re-post.

***

### Measured fill rates (2026-09-11, full listings)

| Column | Ubisoft (291 ads) | Bosch (4,839 ads) |
|---|---|---|
| `title`, `function`, `industry`, `employmentType`, `experienceLevel`, `city`, `countryCode` | 100% | 100% |
| `department` | 100% | **0%** |
| `region` | 99% | 74% |
| `isRemote` = true | 1 | 70 |
| `isHybrid` = true | 4 | 756 |
| Ad languages / countries | 4 / 13 | 23 / 56 |

Bosch reads 4,839 ads in 49 requests in about 17 seconds. With `includeDescription` on, the whole of Bosch - 4,843 ads, each with its description - took 301 seconds on 512 MB of memory (production run, 2026-09-11). SmartRecruiters' listing is newest first, so a job published while a large company is being read shifts every page by one; when the count comes up short the listing is read a second time, newly found ads are put back in newest-first order, and if postings are still missing an `incomplete` row says how many.

`department` depends on the company - Bosch publishes none, so a department filter on Bosch returns a `no-matching-jobs` row that says so, instead of pretending no job qualifies. The `function` column is filled for every job.

***

### Filters, and how SmartRecruiters really treats them

Every filter was checked against a count made locally from the company's full listing:

- **Country** is sent to SmartRecruiters, which only accepts **lower-case** two-letter codes (`de` returned 780 Bosch jobs, the correct number; `DE` returned **0**) and returns **0 when two countries are sent together**. This Actor lower-cases the code, asks one country at a time, then merges. Values that are not ISO country codes (`XX`, `EU`, `Germany`) are never sent - they get a `bad-input` row.
- **Keyword** is SmartRecruiters' own full-text search over title, description and location. Several words match **any** of them (`game designer` = 179 at Ubisoft, more than `game` or `designer` alone); `"game designer"` in quotes matches the phrase (29).
- **City and location** are filtered here, not by SmartRecruiters: its `city` filter is case-sensitive (`Stuttgart` 57, `stuttgart` 0) and its `location` parameter is silently ignored.
- **Title, department, remote only and posted within days** are applied here, after reading.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `companies` | array | *(prefilled: `Ubisoft2`, `CERN`)* | SmartRecruiters company IDs or careers.smartrecruiters.com / jobs.smartrecruiters.com links, up to 200. If empty, `Ubisoft2` and `CERN` are used and every row says so in `input` |
| `keyword` | string | *(empty)* | SmartRecruiters' full-text search (title, description, location). Quotes for an exact phrase |
| `countries` | array | *(empty)* | Two-letter country codes such as `DE`, `FR`, `US`, `GB`. Each is searched separately |
| `titleContains` | string | *(empty)* | Keep jobs whose title contains this text (case-insensitive) |
| `departmentContains` | string | *(empty)* | Keep jobs whose department contains this text |
| `locationContains` | string | *(empty)* | Matches city, region, country name and full location, for example `Montreal` or `Bavaria` |
| `remoteOnly` | boolean | `false` | Only jobs SmartRecruiters flags as remote (hybrid has its own column) |
| `postedWithinDays` | integer | `0` (off) | Only ads posted within N days, by each ad's own date. Ads without a date are left out, never assumed recent |
| `oneRowPerJob` | boolean | `true` | Merge ads with the same reference number and the same title into one row |
| `includeDescription` | boolean | `false` | Add the full description as plain text (one extra request per job, 8 in parallel) |
| `maxResultsPerCompany` | integer | `1000` | Newest jobs kept per company; a `more-jobs-available` row counts the rest |
| `maxRetries` | integer | `3` | Attempts per request for temporary errors (HTTP 429 / 5xx) |

**Finding a company ID:** open any of the company's job ads. The link looks like `jobs.smartrecruiters.com/BoschGroup/744000...` - `BoschGroup` is the ID. Companies with their own careers domain (such as jobs.bosch.com) still link their Apply button there.

#### Descriptions

With `includeDescription` on, `descriptionStatus` says what happened for each job:

- `ok` - the description is in `descriptionText` (`empty` if the posting has none)
- `failed: ...` - SmartRecruiters did not return it after retries; **the job is still returned and charged**, because everything else in the row is real
- `not-fetched-time-limit` - the run was close to its time limit, so descriptions were skipped to return the jobs in time (still charged); companies after it get a `not-queried-time-limit` row
- A posting whose page says it no longer exists is **not** returned as a job - it becomes a free `posting-closed` row

***

### Output columns

`rowType`, `ok`, `note`, `input`, `companyId`, `companyName`, `jobId`, `jobRefNumber`, `title`, `department`, `function`, `industry`, `employmentType`, `employmentTypeId`, `experienceLevel`, `experienceLevelId`, `city`, `region`, `countryCode`, `countryName`, `fullLocation`, `isRemote`, `isHybrid`, `workplaceType`, `postedAt`, `firstPostedAt`, `jobAdLanguage`, `isDefaultJobAd`, `adsForSameJob`, `otherAdsForSameJob`, `jobUrl`, `descriptionText`, `descriptionStatus`, `searchedKeyword`, `searchedCountries`, `checkedAt`

Every row - including note rows - has all 36 columns in the same order.

#### Things it refuses to invent or pass on

- **No people's names.** SmartRecruiters includes the name of the recruiter who created each ad; this Actor drops it. It also does **not** return the company-defined custom fields at all: they are set up by each company, and some carry people's names even in pick-list fields (one employer's "Head Officer" field names a person). E-mail addresses and phone numbers inside descriptions are replaced with `[email removed]` / `[phone removed]`.
- **`countryName`** comes from the structured country code. Companies also type a free-text country field, and it can disagree: two Ubisoft jobs located in Montreal, Canada carry "France" there.
- **`workplaceType`** is `remote` or `hybrid` only when SmartRecruiters' own flags say so. When neither flag is set it stays `null` - not "onsite" - because many companies never set the flags.
- **Empty values are `null`**, never an empty string or a `0`.

***

### How it is checked

- **60 automated checks**, most of which run the real paging, retry, merge and charging code against recorded SmartRecruiters responses (page failures, a listing that shifts while being read, a company that does not exist, closed postings, budget and time limits)
- **72 deliberate defects injected into the source, plus 8 into the charging path; every one was caught by those checks**
- Only public job postings are read. Nothing needs a login
- Every request has a 45-second timeout; temporary errors are retried with a growing pause

### Support

Found a company that behaves oddly? Open an issue on the **Issues** tab with the company ID - it will be reproduced against the live API before anything is changed.

# Actor input Schema

## `companies` (type: `array`):

One entry per company: its SmartRecruiters company ID (for example BoschGroup or Ubisoft2 - not case-sensitive) or a careers.smartrecruiters.com / jobs.smartrecruiters.com link. The ID is the part after jobs.smartrecruiters.com/ in any of the company's job links. An ID that is not a SmartRecruiters company returns a company-not-found note row, never an empty result. Up to 200 per run.

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

Optional. Sent to SmartRecruiters' own search, which looks in the title, the description and the location. Several words match any of them; put words in "double quotes" to match an exact phrase. Leave empty for all jobs.

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

Optional. Two-letter ISO 3166 country codes, for example DE, FR, US or GB. Several codes are searched one by one and merged. A value that is not a country code (for example Germany or XX) is reported in a bad-input note row and not used.

## `titleContains` (type: `string`):

Optional. Keep only jobs whose title contains this text (case-insensitive).

## `departmentContains` (type: `string`):

Optional. Keep only jobs whose department contains this text (case-insensitive). Some companies publish no department on SmartRecruiters; the no-matching-jobs row then says so instead of pretending none qualify.

## `locationContains` (type: `string`):

Optional. Keep only jobs whose city, region, country name or full location contains this text (case-insensitive), for example Montreal or Bavaria.

## `remoteOnly` (type: `boolean`):

Keep only jobs SmartRecruiters itself flags as remote. Hybrid jobs have their own isHybrid column and are not included.

## `postedWithinDays` (type: `integer`):

Keep only ads posted within this many days, judged by each ad's own posting date. 0 turns the filter off. firstPostedAt still shows the earliest ad of the same job, so you can see when a job is a re-post in a new city.

## `oneRowPerJob` (type: `boolean`):

SmartRecruiters often publishes one job as several ads sharing a reference number, for example one ad per city. On: ads with the same reference number and the same title become one row (the company's default ad), the other ads listed in otherAdsForSameJob, charged once. Ads whose title differs - including translated titles - stay separate rows. Off: one row per ad, each charged.

## `includeDescription` (type: `boolean`):

Adds the job description (company description, job description, qualifications, additional information) as plain text. This needs one extra request per job, about 0.05 seconds each with 8 in parallel. The descriptionStatus column says when a description could not be read; the job is still returned and charged. A posting whose page no longer exists is not returned as a job.

## `maxResultsPerCompany` (type: `integer`):

Upper limit of job rows per company, newest first. When a company has more matching jobs, a more-jobs-available note row says how many were left out. You are charged only for job rows returned.

## `maxRetries` (type: `integer`):

How many times to try a request in total, including the first attempt, when SmartRecruiters answers with a temporary error (HTTP 429 or 5xx).

## Actor input object example

```json
{
  "companies": [
    "Ubisoft2",
    "CERN"
  ],
  "countries": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "oneRowPerJob": true,
  "includeDescription": false,
  "maxResultsPerCompany": 1000,
  "maxRetries": 3
}
```

# Actor output Schema

## `results` (type: `string`):

One row per job posting, plus one note row when a company does not exist, has no matching jobs, or could not be read.

# 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 = {
    "companies": [
        "Ubisoft2",
        "CERN"
    ],
    "keyword": "",
    "countries": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/smartrecruiters-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 = {
    "companies": [
        "Ubisoft2",
        "CERN",
    ],
    "keyword": "",
    "countries": [],
}

# Run the Actor and wait for it to finish
run = client.actor("neverempty/smartrecruiters-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 '{
  "companies": [
    "Ubisoft2",
    "CERN"
  ],
  "keyword": "",
  "countries": []
}' |
apify call neverempty/smartrecruiters-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/smartrecruiters-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/VR6WfG3aBchNDjojX/builds/6jHaHrJ8rll5e2Ojo/openapi.json
