# HelloWork Jobs Scraper API - France Job Board (`dami_studio/hellowork-jobs-scraper`) Actor

Scrape HelloWork jobs in France by keyword and city. Each row has the title, company, city and department, the contract (CDI, CDD, intérim, stage, alternance), skills, posted date and salary. A flag says when the salary is HelloWork's own estimate. $0.49 per 1,000 postings.

- **URL**: https://apify.com/dami_studio/hellowork-jobs-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 6 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.48 / 1,000 jobs

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

**Scrape HelloWork jobs in France, with the contract type and a salary marked real or estimated**:
search by keyword and city, filter by contract, remote band, experience or salary, and get rows back
with the title, company, city and department, CDI or CDD or intérim or stage or alternance, the
salary, the posted date and the link.

The salary field needs a warning before anything else. When an employer publishes no figure,
HelloWork fills the gap with its own estimate. Both arrive in the same place, so every row carries
`salaryIsEstimate` and you should read it before you treat a number as an offer.

| | |
|---|---|
| **Input** | Job keywords and a French location, or HelloWork URLs |
| **Output** | One row per posting: title, company, city, department, contract, salary with its source, posted date, skills |
| **Ceiling** | 20,000 postings per run |
| **Account needed** | None |
| **Price** | $0.49 per 1,000 postings on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 10,000 |

### 🔍 What HelloWork Jobs Scraper does

Each keyword you give it is walked as its own search, so `["développeur", "data analyst"]` runs two.
French terms work best, and English job titles match plenty of postings too. Add a city, department
or region and a radius in kilometres to keep it local.

These filters from HelloWork's own sidebar are here: contract type, télétravail band, working time,
experience, who is hiring, posted-within window and a minimum salary. Broad searches repeat rows
across pages, and duplicates are removed before anything counts toward your maximum, so `maxItems`
means distinct postings.

By default each posting's own page is opened, which is where the exact posted date, the expiry
date, the postal code and region, the sector, the skills list and the education level come from.
Turn that off for a faster listing-only run and you keep title, company, location, contract, salary
and an approximate posted date.

### 📋 What data you get from each HelloWork posting

| What you get | Field |
|---|---|
| HelloWork's posting id, title, company and links | `jobId`, `title`, `companyName`, `jobUrl`, `companyProfileUrl` |
| Where it is | `locationLabel`, `city`, `departmentCode`, `region`, `postalCode`, `countryCode` |
| Contract, its length, working time | `contractType`, `contractDuration`, `employmentType`, `workingTime` |
| The télétravail band and two flags | `teleworkLabel`, `isRemote`, `isHybrid` |
| The salary, and whether HelloWork estimated it | `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText`, `salaryIsEstimate` |
| HelloWork's estimate on its own | `salaryEstimateMin`, `salaryEstimateMedian`, `salaryEstimateMax` |
| Posted and expiry dates | `postedAt`, `postedRelative`, `postedAtEstimated`, `expiresAt` |
| Category, sectors, skills, education, experience | `occupationalCategory`, `sectors`, `skills`, `educationLevels`, `experienceRequired` |
| The employer's reference and direct apply | `reference`, `directApply` |
| The full posting text, when you ask for it | `descriptionHtml`, `descriptionText`, `requirementsText` |
| Where the row came from | `searchQuery`, `page`, `position`, `scrapedAt` |

### ▶️ How to scrape HelloWork jobs

1. Open [HelloWork Jobs Scraper](https://apify.com/dami_studio/hellowork-jobs-scraper) and click
   **Try for free**.
2. Put your terms into **Job keywords**, one per line.
3. Add a **Location** and a radius if you want it local.
4. Set **Maximum postings**, then click **Start**.
5. Download the dataset as JSON, CSV or Excel, or read it from the Apify API.

### 💰 How much does it cost to scrape HelloWork jobs?

**$0.49 per 1,000 postings** on the Free plan. Paid plans pay a little less: $0.485 on Starter, $0.48 on Scale and $0.475 on Business. On the free plan, the $5
Apify gives you each month covers about 10,000 postings.

You pay per posting row. Duplicates across pages are dropped before they count, the sample row is
not charged, and diagnostic rows are not charged. Adding the full description does not add a second
charge, it only makes the rows bigger. A posting whose own page could not be read still arrives as a
row, with its listing fields, `detailNote` and `charged: false`, and is not charged.

### 📥 What you give it

```json
{
  "searchQueries": ["comptable"],
  "location": "Lyon",
  "radiusKm": 20,
  "contractTypes": ["cdi"],
  "onlyWithSalary": true,
  "maxItems": 200
}
```

| Field | Default | What it is |
|---|---|---|
| `searchQueries` | none | One or more searches. The box starts with `développeur` in it, which is a suggestion and not a default. |
| `location` | none | A city, department or region: `Paris`, `Lyon`, `Île-de-France`. Empty searches the whole country. |
| `radiusKm` | none | Kilometres around the location, 1 to 200. Only used when a location is set. |
| `contractTypes` | all | `cdi`, `cdd`, `interim`, `stage`, `alternance`, `freelance`, `independant`, `fonctionnaire`, `franchise`, `associe`, `stage-lycee`. |
| `telework` | all | `full`, `partial`, `occasional`, `none`. Only about 18% of postings carry any remote tag, so this one narrows hard. |
| `workingTime` | all | `full-time`, `part-time`, `unspecified`. |
| `experience` | all | HelloWork's bands: `0-1`, `2-3`, `4-5`, `6-10`, `10+`, `unspecified`. |
| `employerType` | all | `company`, `agency`, `consultancy`, `temp-agency`, `training-centre`. |
| `postedWithin` | any time | `day`, `3days`, `week`, `month`. |
| `minSalary` | none | Yearly euros. HelloWork applies this to its own estimate as well as to published figures, so pair it with the next field if you want real offers. |
| `onlyWithSalary` | `false` | Ask HelloWork for postings that display a salary. Leaving it off returns more rows, and only about 63% of them carry one. |
| `sortBy` | `relevance` | `relevance` or `date`. |
| `startUrls` | none | Paste HelloWork URLs instead of building a search. A single posting works, and so does a search page. |
| `includeDetails` | `true` | Open each posting for the full record. |
| `includeDescription` | `false` | Add the whole posting text as HTML and plain text plus the requirements section. Needs `includeDetails` on. It rides on the same request, so it adds no time and no charge. |
| `maxItems` | `50` | Stop after this many distinct postings, up to 20,000. |
| `proxyConfiguration` | Apify default | Leave it alone. Your own servers are used exactly as given. |

### 📤 What you get back

A real row from a run on 20 September 2026. Note `salaryIsEstimate`, because this one is
HelloWork's estimate rather than the employer's figure:

```json
{
  "jobId": "80369776",
  "title": "Développeur H/F",
  "companyName": "Neqo",
  "jobUrl": "https://www.hellowork.com/fr-fr/emplois/80369776.html",
  "companyProfileUrl": "https://www.hellowork.com/fr-fr/entreprises/neqo-133722.html",
  "locationLabel": "Vannes - 56",
  "city": "Vannes",
  "departmentCode": "56",
  "region": "Bretagne",
  "postalCode": "56000",
  "countryCode": "FR",
  "contractType": "CDI",
  "contractDuration": null,
  "employmentType": "FULL_TIME",
  "workingTime": "Full time",
  "teleworkLabel": "Hybrid",
  "isRemote": false,
  "isHybrid": true,
  "salaryMin": 28000,
  "salaryMax": 44000,
  "salaryCurrency": "EUR",
  "salaryPeriod": "YEAR",
  "salaryText": "28000 - 44000 EUR / year",
  "salaryIsEstimate": true,
  "salaryEstimateMin": 28000,
  "salaryEstimateMedian": 37500,
  "salaryEstimateMax": 44000,
  "postedAt": "2026-09-18T00:13:47Z",
  "postedRelative": "il y a 2 jours",
  "postedAtEstimated": "2026-09-18T07:03:19.923Z",
  "expiresAt": "2026-10-18T00:13:47Z",
  "occupationalCategory": "Informatique",
  "sectors": ["Secteur informatique", "ESN"],
  "skills": ["Service client", "SQL", "CakePHP", "Git", "HTML5", "Node.js", "PHP", "MySQL", "..."],
  "educationLevels": ["bachelor degree", "associate degree"],
  "experienceRequired": null,
  "reference": "3937186/29254901 D/56V",
  "directApply": true,
  "searchQuery": "développeur",
  "page": 1,
  "position": 2,
  "scrapedAt": "2026-09-20T07:03:35.992Z"
}
```

The skills list is cut short; the real row carries sixteen.

| Field | How to read it |
|---|---|
| `salaryIsEstimate` | `false` means the employer published the figure. `true` means it is HelloWork's own estimate for that title and region, and the same numbers are repeated in the `salaryEstimate*` fields so one filter drops them. `null` means no salary at all. **If you only want real offers, keep `salaryIsEstimate === false`.** |
| `departmentCode` | The French department number, `75` for Paris, `69` for Lyon, `56` for Morbihan. |
| `contractType` | `CDI`, `CDD`, `Intérim`, `Stage`, `Alternance`, `Freelance` and the rest. `contractDuration` carries `6 mois` and the like on fixed terms. |
| `teleworkLabel` | HelloWork's own télétravail band, and mostly empty because most employers do not set one. `isRemote` and `isHybrid` are the flags derived from it. |
| `postedAt` | The exact timestamp, and it only exists when postings are opened. On a listing-only run you get `postedRelative` and `postedAtEstimated` instead. |
| `expiresAt` | When HelloWork takes the posting down. |
| `descriptionHtml`, `descriptionText`, `requirementsText` | Only filled in when you switch the full description on. |
| `detailNote` | Present when the posting page could not be read. That row keeps its listing fields, carries `charged: false` and is not charged. |

### 🧾 Reading the output

| Row | How to spot it | Charged |
|---|---|---|
| A posting | It has a `jobId` | yes |
| A posting whose page could not be read | A `jobId`, `detailNote` and `charged: false` | no |
| The sample row | `_sample: true` and `charged: false` | no |
| A diagnostic | An `errorCode` and no `jobId` | no |

| `errorCode` | What it means |
|---|---|
| `NO_RESULTS` | The search ran and matched nothing. Loosen a filter or try a broader keyword. |
| `BAD_URL` | A pasted URL is not a HelloWork posting or search page. |
| `JOB_NOT_FOUND` | That posting is gone. Expired postings do this. |
| `JOB_UNPARSED` | The page could not be read. Re-run it. |
| `HTTP_ERROR` | HelloWork answered badly or was unreachable. |
| `TIME_LIMIT` | The run reached its time limit and stopped on its own. The jobs already returned are kept and charged; give the run more time to get the rest. The run ends as failed only when the time limit stopped it before any job came back. |

### 💡 What people use it for

- Salary research that survives scrutiny, by keeping only `salaryIsEstimate === false` so the
  numbers are the ones employers actually printed.
- Watching one city and one contract type daily, with `postedWithin` set to `day`, and deduping on
  `jobId`, which is stable.
- Mapping demand by department, since every row carries `departmentCode` and most carry a postal
  code.
- Pulling the `skills` arrays for a role across a few hundred postings to see which tools keep
  appearing.
- Telling employers apart from agencies with `employerType`, which changes what a listing count
  actually means.

### 🚧 What it does not do

- **France only.** That is HelloWork's own coverage.
- **The salary can be an estimate.** HelloWork produces one when the employer published nothing,
  and it lands in the same fields. `salaryIsEstimate` is the only thing that tells you apart.
- **`minSalary` filters estimates too.** HelloWork applies its own filter to its own estimate, so a
  high minimum still returns postings with no published figure. Combine it with **Only postings that
  show a salary**.
- **The remote band is usually empty.** Only about 18% of postings carry one, so filtering on it
  cuts results hard and drops postings that are remote but untagged.
- **No applications, no candidate data, no recruiter contact details.** Postings only.
- **A single search runs out before the cap.** Going wide means several keywords, cities or
  filters, not a bigger `maxItems`.
- **Pasted postings arrive at the end of the run.** 10,000 pasted URLs took about 29 minutes, and
  nothing from a paste is written until it finishes, so keep big lists in a few runs.
- **Postings expire.** `expiresAt` tells you when, and a pasted link to a dead posting returns
  `JOB_NOT_FOUND`.
- **Listing-only runs have no exact posted date.** Turn postings on if you need `postedAt` rather
  than `postedAtEstimated`.
- It is not an official HelloWork tool. Dami's Studio is independent and is not affiliated with or endorsed by HelloWork, or by any other company named on this page.

### 🧭 Which job scraper do you need?

| If you want | Use |
|---|---|
| French postings from HelloWork, with the salary marked real or estimated | This one |
| Public LinkedIn jobs by keyword and location | [LinkedIn Jobs Scraper](https://apify.com/dami_studio/linkedin-jobs-scraper) |
| Arbeitnow's postings for Germany, the UK, France and Switzerland, with its visa-sponsorship flag | [Arbeitnow Jobs Scraper](https://apify.com/dami_studio/arbeitnow-jobs-scraper) |
| Jobs in Germany, Austria and Switzerland, marked remote, hybrid or on-site | [XING Jobs Scraper](https://apify.com/dami_studio/xing-jobs-scraper) |
| Every open job at companies you name, read from their own hiring system | [Career Site Jobs Scraper](https://apify.com/dami_studio/career-site-jobs-scraper) |

### ❓ Questions people ask

#### Do I need a HelloWork account?

No. Everything here is on public posting pages.

#### How do I get only real salaries?

Switch on **Only postings that show a salary**, then keep rows where `salaryIsEstimate` is `false`.
The first narrows what HelloWork sends, the second catches the estimates that still come through.

#### Should I search in French or English?

French matches more. English job titles work for the technical roles that are advertised that way.

#### Can I paste a single posting?

Yes, into **HelloWork URLs**. A search page URL works there too.

#### Why is `postedAt` empty?

You ran with postings closed. The exact date lives on the posting page.

#### Can I call it from code or connect it to an AI assistant?

Yes. The [API tab](https://apify.com/dami_studio/hellowork-jobs-scraper/api/python) has ready-made
code for Python, JavaScript and the command line. For Claude, ChatGPT or another MCP client, connect
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/hellowork-jobs-scraper`. Either way the
run happens on your Apify account at the same price.

#### Is scraping HelloWork legal?

These are public job adverts, and they can carry personal data, which GDPR covers, so have a reason
for collecting it. Apify's write-up on
[the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a good starting
point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Send the keyword, the location and the run ID. If you got
a diagnostic row, its `errorCode` usually names the problem on its own.

# Actor input Schema

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

One or more searches, exactly as you would type them into HelloWork. French works best ("développeur", "comptable", "aide soignant"), English job titles also match. Each keyword is walked separately, so \["développeur", "data analyst"] runs two searches.

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

City, department or region in France, for example "Paris", "Lyon", "Bordeaux" or "Île-de-France". Leave empty to search the whole country.

## `radiusKm` (type: `integer`):

Search radius around the location, in kilometres. Only used when a location is set.

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

Restrict to one or more French contract types. Leave empty for all of them.

## `telework` (type: `array`):

Restrict to HelloWork's own télétravail bands. Note that only about 18% of postings carry any remote tag at all, so filtering here narrows results hard.

## `workingTime` (type: `array`):

Full time, part time, or postings that do not say.

## `experience` (type: `array`):

HelloWork's own experience bands.

## `employerType` (type: `array`):

Who is hiring: the company itself, a recruitment agency, a consultancy (ESN), a temp agency or a training centre.

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

Only postings published inside this window. Every row also carries the posted date, so you can filter afterwards too.

## `minSalary` (type: `integer`):

Only postings at or above this yearly salary, using HelloWork's own salary filter. Careful: HelloWork applies it to its own salary estimate as well as to employer-published figures, so most matches still have no published salary. Combine it with "Only postings that show a salary" if you want real numbers.

## `onlyWithSalary` (type: `boolean`):

Ask HelloWork for postings that display a salary. Measured on a 35,033-result search, this cut it to 27,696 and every card on the first page carried an employer-published figure. Leaving it off returns more rows but only about 63% of them have a salary.

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

HelloWork's own ordering: its relevance ranking, or newest first.

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

Paste HelloWork URLs instead of building a search. Works with a single posting (https://www.hellowork.com/fr-fr/emplois/82120782.html) and with a search page (https://www.hellowork.com/fr-fr/emploi/recherche.html?k=comptable\&l=Lyon).

## `includeDetails` (type: `boolean`):

Reads each posting's own page, which adds the exact posted date and expiry date, postal code and region, sector, required skills, education level, the company's HelloWork profile, the job reference, and HelloWork's salary estimate when the employer published none. Turn this off for a faster, listing-only run: you keep title, company, location, contract, salary and the approximate posted date.

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

Adds the complete posting text as HTML and as plain text, plus the requirements section. Needs "Open each posting" to be on. It comes from the same request, so it costs no extra time and no extra charge - it only makes the rows larger.

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

Stop after this many postings. HelloWork serves at most 74 pages (2,220 postings) per individual search, so use several keywords, cities or filters to go beyond that.

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

Optional, and not needed for a normal run. Supply your own proxy servers only if you want the traffic to leave through them.

## Actor input object example

```json
{
  "searchQueries": [
    "développeur",
    "comptable"
  ],
  "location": "Paris",
  "radiusKm": 20,
  "contractTypes": [],
  "telework": [],
  "workingTime": [],
  "experience": [],
  "employerType": [],
  "postedWithin": "",
  "minSalary": 40000,
  "onlyWithSalary": false,
  "sortBy": "relevance",
  "startUrls": [
    {
      "url": "https://www.hellowork.com/fr-fr/emploi/recherche.html?k=comptable&l=Lyon"
    }
  ],
  "includeDetails": true,
  "includeDescription": false,
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One row per job posting in the default dataset: title, company, city and department, contract type (CDI, CDD, interim, stage, alternance, freelance), employer-published salary, remote flag, exact posted date and the job URL. Empty or blocked runs return a single uncharged row instead.

# 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": [
        "développeur"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/hellowork-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": ["développeur"] }

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/hellowork-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": [
    "développeur"
  ]
}' |
apify call dami_studio/hellowork-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/hellowork-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/ZuWJvC6N9ce7JQUv1/builds/d8MjCp4Tjdfhxphgg/openapi.json
