# France Travail Jobs Scraper — 777K FR Offers + Employer Phone (`scrapersdelight/france-travail-jobs-scraper`) Actor

Scrape France Travail (candidat.francetravail.fr), the French state job board: 777,220 live offers, 2026-09-06. Filter by keyword, departement, contract, salary, posting age. Offer pages add pay, NAF sector, full text and the recruiter's phone: 37.8% on France Travail's own offers, 0% on syndicated.

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

## Pricing

from $0.90 / 1,000 per offer returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🇫🇷 France Travail Jobs Scraper — the French state job board, with the recruiter's phone

Scrape **candidat.francetravail.fr**, the public vacancy board of France Travail (the French state
employment service, formerly Pôle emploi). Every vacancy an employer files with the state is
published here on an open page — **777,220 live offers measured on 2026-09-06** by summing all 18
region totals off the site's own result headlines.

This actor returns the result rows *and*, optionally, the offer page behind each one — which is the
only place the **recruiter's direct phone number**, the **employer headcount band**, the **NAF
industry label**, the **pay figures** and the **full offer text** exist.

> **Why a job board is a lead list.** A live vacancy is a dated, public statement that a French
> company has budget and a hiring problem *right now*. Filter to "posted in the last 24 hours" and
> you get the freshest trigger event in the French B2B market.
>
> **Read this before you price the phone into a plan.** France Travail publishes two kinds of offer
> and they are not the same product. Its **own** offers carry the recruiter's direct line **37.8%**
> of the time; the **syndicated** ones it republishes from HelloWork, Meteojob, APEC and ~30 other
> boards carry it **0.0%** of the time — 0 of 423 measured. And a *deep* slice is about **92%**
> syndicated. So a phone-fill number quoted without a cohort is measuring whichever mix the sample
> happened to have. Every fill figure on this page is given **per cohort**, and
> `onlyDirectEmployerOffers: true` is how you select the rich one.

***

### 📊 What you get, and how full each field really is

Every percentage below is from a run taken on **2026-09-06 against the code that ships**.
Two runs, both cited by run id:

- **`LIST`** — run **`OMOJ5VkckkbcB5PLV`**: comptable · Nord (59), uncapped. 50 pages, **1,000 rows,
  1,000 unique offer numbers, 3 min 09 s**, no page-zero failure. Of those 1,000: **77 direct
  France Travail offers, 923 syndicated**.
- **`FULL`** — run **`e82xg4ftZkb8KqWOR`**: the same query crawled **25 pages deep** with
  `fetchOfferDetails: true`. **500 delivered, 500 unique, 497 enriched, 506 detail requests, 3
  failures (0.6%), 9 min 11 s.** Of the 497 enriched: **74 direct, 423 syndicated**.

`FULL` is deliberately a *deep* sample. The earlier README quoted a 6-query sweep capped at 50 rows
each, which never left the first two or three pages of any query — and France Travail lists its own
offers first, so that sample was **0% syndicated** and its fill table described the direct cohort
while calling it the corpus. That is the defect this table fixes.

A flat row is **51 columns** (52 when `includeRawHtml` adds `rawDetailHtml`).

#### From the result list — no extra request

| Field | Fill | What it is |
|---|---|---|
| `offerId` | 100% | The 7-character France Travail offer number (`213KBGC`) — the national identifier, and the dedupe key |
| `offerUrl` | 100% | Direct link to the offer page |
| `title` | 100% | Job title as published |
| `employerName` | **92.8%** on `LIST`, 100% on `FULL` | Hiring company — confidential offers omit it; the offer page fills the rest in |
| `locationText` · `departementCode` · `commune` | 100% | `59 - Marcq-en-Barœul` split into a departement code and a commune |
| `contractLabel` · `contractType` · `workTime` | 100% | `Intérim - 2 Mois` → `MIS`, plus temps plein / partiel |
| `postedRelative` | 100% | `Publié aujourd'hui`, `Publié il y a 7 jours` |
| `teaser` | 100% | The summary shown in the list — **196 characters average** across all 1,000 `LIST` rows |
| `offerOrigin` · `isPartnerOffer` | 100% | Which board the offer came from. `LIST` saw **34 distinct origins**: `France Travail`, `HELLOWORK`, `TALENTPLUG`, `METEOJOB`, `CADREMPLOI`, `APEC`, `DIRECTEMPLOI`, `BROADBEAN`, `FLATCHR` … **This is the field that predicts every other field's fill** — see the next table |
| `latitude` · `longitude` | **23.5%** on `LIST`, 15.3% on `FULL` | France Travail's own map marker. Fill varies hard by slice — many offers carry an empty `coordonnees` |
| `resultTotalForQuery` | 100% | How many offers the whole query matched (**1,093** for comptable/Nord at the moment `LIST` ran). It is the *register's* total, not a promise about how many rows this run read — a query that stopped early is reported as `truncatedQueries` in `RUN_SUMMARY`, never as a clean success |

#### From the offer page — `fetchOfferDetails: true`, one extra request per offer

**Split by cohort, because that is the only honest way to state it.** All three columns are the same
run, `FULL` = **`e82xg4ftZkb8KqWOR`**, 497 enriched offers, 2026-09-06. "Direct" = France Travail's
own offers (`isPartnerOffer: false`, n = 74). "Syndicated" = offers republished from another board
(n = 423). "Blended" is the whole 497, whose mix was 84.6% syndicated.

| Field | Direct (n=74) | Syndicated (n=423) | Blended (n=497) | What it is |
|---|---|---|---|---|
| `employerPhone` | **37.8%** | **0.0%** | 5.6% | The recruiter's direct line, from the page's schema.org `telephone`. **Zero of 423 syndicated offers carried one** |
| `employerContactName` | 39.2% | 0.0% | 5.8% | The named recruiter as the employer printed it (`M. David TUROTTE`) — **personal data**, see the legal note. Sometimes a team or agency name |
| `employerSizeBand` | 68.9% | 27.0% | 33.2% | `20 à 49 salariés`, `250 à 499 salariés` … |
| `employerPageUrl` | 62.2% | 0.7% | 9.9% | The company's France Travail mini-site on `recrute.francetravail.fr` |
| `employerAbout` | 47.3% | 67.8% | 64.8% | The company's own blurb — the one field syndicated offers carry *more* often |
| `industryNaf` | 100% | 71.6% | 75.9% | NAF sector label, e.g. `Activités comptables` |
| `postalCode` · `region` | 100% / 100% | 99.8% / 100% | 99.8% / 100% | Full postal address components (`country` 100% everywhere) |
| `salaryMin` · `salaryMax` · `salaryUnit` | 75.7% | 70.0% | 70.8% | Parsed figures + `YEAR` / `MONTH` / `HOUR` |
| `salaryText` | 77.0% | 70.4% | 71.4% | As printed: `Salaire brut : Annuel de 40000.0 Euros à 45000.0 Euros` |
| `benefits` | 73.0% | 35.9% | 41.4% | `Primes`, `Titres restaurant`, `Complémentaire santé` … |
| `experienceRequired` | 100% | 100% | 100% | `3 An(s)` |
| `qualification` | 100% | 64.5% | 69.8% | `Employé qualifié` |
| `skills` | **97.3%** | **0.0%** | 14.5% | The offer's competence list — a France Travail structure that syndicated offers simply do not have |
| `softSkills` · `education` · `languages` · `drivingLicences` | 35.1% / 39.2% / 5.4% / 5.4% | 0% on all four | 5.2% / 5.8% / 0.8% / 0.8% | The rest of the "Profil souhaité" block |
| `workHours` · `employmentType` | 100% / 100% | 1.9% / 0.0% | 16.5% / 14.9% | `35H/semaine Travail en journée` · `FULL_TIME` |
| `contractNature` | 98.6% | 99.3% | 99.2% | `Contrat travail` |
| `travelRequirement` | 32.4% | 0.0% | 4.8% | `Déplacements : Jamais` |
| `datePosted` · `validThrough` · `metierCode` | 100% | 100% | 100% | Real ISO dates, plus France Travail's internal métier id (`11213`) |
| `description` | 100% | 100% | 100% | Full offer text — **1,990 characters average** on direct offers, **1,580** on syndicated (162 – 4,662 overall) |

**How to read this table.** Twelve of these fields are France Travail's own structured blocks, and a
syndicated offer is a re-post that never had them. If you are buying *lead data* — phone, named
contact, employer page, skills — set `onlyDirectEmployerOffers: true` and you are buying the left
column. If you are buying *market data* — titles, salaries, dates, locations, full text — the
blended column is fine and the syndicated rows are most of your volume.

#### Two things other listings promise that this one does not ship

- **There is no `employerEmail` column.** The offer page carries an `itemprop="email"` tag on every
  offer and its `content` attribute is empty on all of them — **0 of 300** enriched offers on
  `FULL`. A column that is null in 100% of rows is not a field, so it is not in the schema at all.
  If you need e-mail, take `employerName` + `employerPageUrl` into a company-enrichment step.
- **There is no ROME code on this surface.** A search for an `[A-N]\d{4}` token across every detail
  page returned zero hits. What exists is France Travail's *internal* `metierCode`, and that is what
  the field is called.

***

### 🔪 The 1,000-row ceiling, and the slicing that beats it

**France Travail hard-caps every single query at 1,000 rows** — 50 pages of 20. `LIST` asked for an
uncapped crawl of comptable/Nord (1,093 matching offers) and received exactly 50 pages / 1,000 rows
before the "Afficher les 20 offres suivantes" link disappeared. That is the site's limit, not this
actor's, and every rival is subject to it.

Since one departement alone can hold 30,291 offers (Paris, no keyword, 2026-09-06), coverage needs
*partitioning*. `sliceBy` does it, and every slice is deduplicated on the offer number **before
anything is billed**:

| `sliceBy` | What it does | Slices available |
|---|---|---|
| `departement` | Re-runs your search once per French departement | **106** codes incl. `2A`, `2B`, `971`–`988` |
| `region` | Once per INSEE region | 18 |
| `contractType` | Once per CDI / CDD / MIS / SAI | 4 |
| `postedWindow` | Once per 1 / 3 / 7 / 14 / 31-day bucket | 5 |
| `metierDepartement` | Crawls France Travail's own canonical métier × departement pages | **27,454** |
| `metierVille` | …and its métier × ville pages | **38,850** |

*(Both sitemap figures are `<loc>` counts re-read from the site on 2026-09-06.)*

**Measured proof that dedupe holds across slices**, run `QH9XE2ffhxyVhK59H` (2026-09-06): a
`postedWindow` sweep of comptable/Nord over 5 windows saw **260 candidates, dropped 153 as
duplicates and delivered 100** — the overlap between the 1-, 3-, 7-, 14- and 31-day buckets was
never charged twice.

**"Max slices per run" bounds the queries, not the place list.** Run `xa0KdP4wf4qSmmZPM`
(2026-09-06): 2 keywords × `sliceBy: departement` at `maxSlices: 6` expanded to exactly **6 queries
/ 120 rows**, not 12.

***

### ⚙️ Every filter, and whether it actually works

Measured live on `commercial` + Paris (75) on **2026-09-06** unless noted; the unfiltered baseline
that day was **2,201** offers. **The filters this actor does *not* expose are as important as the
ones it does** — France Travail accepts a dozen parameters and then silently ignores them, and
shipping those as features would ship a lie.

**Works, server-side, verified:**

| Input | Parameter | Measured effect, 2026-09-06 |
|---|---|---|
| Search terms | `motsCles` | Accents and spaces fine; may be left empty (Paris with no keyword = 30,291) |
| Locations | `lieux` | `75D` departement (30,291) · `11R` region (122,040) · `69123` commune |
| Contract types | `typeContrat` | CDI 1,770 · CDD 312 · MIS 61 · SAI 0 ("Aucune offre") |
| Posted within | `emission` | 1d 102 · 7d 612 · 31d 1,513 of 2,201 |
| Minimum salary + period | `salaireMin`+`periodeSalaire` | 30k annual → 1,218 · 2k monthly → 2,158 |
| Full/part time | `dureeHebdo` | 231 · 9 |
| Qualification level | `qualification` | 0 → 765 · 9 → 630 |
| Professional domain | `domaine` | M17 → 439 (dropped by the site when there is no keyword) |
| Radius | `rayon` | **Only with a commune code**: Lyon 69123 → 503 / 1,554 / 1,877 / 2,164 / 2,769 at 0 / 10 / 20 / 30 / 50 km. Inert at `*D` and `*R` (comptable+59D answers 1,097 with and without it) |
| Partner offers | `offresPartenaires` | 2,201 with · 155 without |

> **A radius trap this actor closes for you.** France Travail's own default for a commune search
> with no `rayon` parameter is **10 km** — `commercial` + `69123` with no radius returns the same
> **1,554** offers as an explicit `rayon=10`. So "0 km — the commune only" is only true if `rayon=0`
> is actually sent, and this actor sends it: run `QSayG4Wc47wdR56De` logs
> `…&lieux=69123&rayon=0` and the headline **503 offres**, against **1,554** on the paired
> `radiusKm: "10"` run `ymql3VTaHUJcdqz85`.

**Measured INERT and therefore deliberately NOT offered:** `experience`, `natureContrat`
(alternance), `tempsPlein`, `offresMema`, `offresPeuCandidat`, `entreprisesAdaptees`,
`secteurActivite`, `dureeContratMin/Max`, `accesTravailleurHandicape` — all of them returned the
unfiltered result count in every value shape. **Sort order (`tri`) is also inert** on this route. A
rival's store page advertises experience filtering on this site; it does not work.

**Three quirks this actor handles that break naive scrapers:** `typeContrat` and `lieux` take only
the FIRST value when repeated, so several selections are crawled as separate queries and merged; a
search with neither a keyword nor a place resolves to a link hub with zero rows, so a completely
empty input falls back to a documented sample instead of failing; and **the site lists its own
offers before syndicated ones**.

That third one is not a curiosity, it is the single most important thing to know about this data.
On `LIST` (`OMOJ5VkckkbcB5PLV`) the **first 60 rows were 60/60 direct** France Travail offers while
the full 1,000-row sweep was **77 direct to 923 syndicated**. The ordering means the direct pool is
served first and only runs out when it is exhausted: `XwAdWDHqWhHjuFyf0` (Rhône, posted within 7
days, 9,821 matching offers) hit the site's 1,000-row query ceiling while still inside the direct
pool and returned **1,000 of 1,000 direct**, whereas `DaPhQaiDHjc6zJwvd` (same place, posted within
24 hours, 1,766 matching) exhausted a direct pool of ~22 on page 2 and the other ~978 rows were
syndicated.

So **`isPartnerOffer` share — and with it the fill of twelve enriched fields — is a property of how
deep your slice goes relative to its direct pool, not a constant.** Any fill percentage measured on
a shallow sample is a measurement of the direct cohort. That is why the table above has three
columns instead of one.

***

### 🚀 Example inputs

Each example below was run **exactly as printed**, on **2026-09-06**, on the build that ships, and
**what it returned is printed with it** — rows, offer-page requests and wall time, not just "it was
accepted". Copy them as printed: the types matter.

**1 · Fresh hiring triggers with the recruiter's phone** *(note the quotes — `postedWithinDays` is
a string enum)*:

```json
{
  "location": ["69D"],
  "postedWithinDays": "7",
  "fetchOfferDetails": true,
  "onlyWithPhone": true,
  "onlyDirectEmployerOffers": true,
  "maxResults": 500
}
```

Run **`XwAdWDHqWhHjuFyf0`** → **409 rows delivered, 409 enriched, 6 min 06 s**, 50 pages, 1,001
offer-page requests, 0 failures. Every delivered row carries a phone (**100%**, it is the filter)
and **95.6%** carry a named contact. It stopped at 409 rather than 500 because the query matched
9,821 offers and France Travail caps any one query at 1,000 rows — 409 of those 1,000 had a phone.

> **Why `onlyDirectEmployerOffers` is in there.** Syndicated offers never carry a phone (0 of 423,
> `e82xg4ftZkb8KqWOR`), and that flag drops them **on the result list, before their pages are ever
> fetched**. Same-day A/B on one identical slice (Rhône, posted within 24 hours, 2026-09-06):
> **without** the flag, run `JaEKXczVgVOrIBXGT` made **1,004** offer-page requests in **12 min 44 s**
> and delivered **8** rows; **with** it, run `DaPhQaiDHjc6zJwvd` read the same 1,000 candidates,
> dropped 991 on the result list and made **23** requests in **3 min 07 s** for **9** rows. Fewer
> requests, more rows. Leave the flag out and this actor warns you, in the log, with those numbers.

**1b · The same recipe at scale, across four departements.** Add `"sliceBy": "departement"`,
`"location": ["69D","13D","31D","33D"]`, `"maxResultsPerQuery": 1000`, `"maxResults": 0` → run
**`onCfiyabSbMyodWT5`**: **1,585 rows, all 1,585 enriched, 200 pages, 4,003 offer-page requests, 0
failures, 29 min 29 s**. **100%** phone fill, **94.2%** named contact, 0 duplicates across the four
slices. The phone yield on the direct cohort here was 1,585 of 4,000 = **39.6%**, which is the third
independent measurement of that ~38–40% figure on this page.

**2 · Sweep a trade across departements (beats the 1,000-row cap):**

```json
{
  "searchTerms": ["electricien"],
  "sliceBy": "departement",
  "maxSlices": 8,
  "maxResultsPerQuery": 200,
  "maxResults": 0
}
```

Run **`Owr9Qf0yysrTSFfKs`** → **798 rows over 8 departement slices, 43 pages, 9 min 57 s**, 0
duplicates, 0 failures. Two of the eight slices stopped on your own `maxResultsPerQuery: 200` while
the site still had pages left, and the run says so explicitly in `RUN_SUMMARY.boundedQueries`
(`electricien · Departement 01`, 10 pages / 200 rows of 274 matching). Raise `maxSlices` to **106**
for all of France and `maxResultsPerQuery` to **1000** for full depth — that is the same input at a
larger scale, and it is hours of crawling, not minutes.

**3 · Only companies hiring directly, not job-board syndication:**

```json
{
  "searchTerms": ["comptable"],
  "location": ["59D"],
  "includePartnerOffers": false,
  "onlyDirectEmployerOffers": true,
  "fetchOfferDetails": true
}
```

Run **`ndLl4Bqc7Jsh7cihB`** → **77 rows, 76 enriched, 4 pages, 2 min 46 s**, 81 offer-page requests,
1 failure. `employerPhone` fill **37.7%**, `isPartnerOffer` false on 77 of 77. That 77 is the whole
direct pool for this query: the 1,000-row unfiltered sweep of the same search
(`OMOJ5VkckkbcB5PLV`) found **77 direct offers among 1,000** — the server-side filter and this
actor's own labelling agree to the row.

**4 · Paste a URL from the site (both shapes work).** Through the API this field takes objects, not
bare strings:

```json
{
  "startUrls": [
    { "url": "https://candidat.francetravail.fr/offres/emploi/comptable/nord/s12m1d59" },
    { "url": "https://candidat.francetravail.fr/offres/recherche?motsCles=plombier&lieux=13D" }
  ],
  "maxResults": 200
}
```

Run **`L09QxuzBWiHhO4y0J`** → **200 rows in 38 s** off 10 pages. Note that `maxResults: 200` was
reached inside the *first* URL, so the plombier/13D URL was never crawled; raise `maxResults`, or
set `maxResultsPerQuery`, if you want both.

***

### 💵 Pricing

Pay per event. **No start fee, no monthly rental, no platform-usage surcharge.**

| Event | Price | When it fires |
|---|---|---|
| `job-scraped` | **$0.0009** per offer | Once per unique offer delivered to your dataset |
| `job-detail-enriched` | **$0.0005** per offer | Only when `fetchOfferDetails` is on, and only for rows that really came back enriched |

That is **$0.90 per 1,000 offers** list-only, **$1.40 per 1,000** with the phone, headcount, NAF,
salary and full text on every row.

The live lane on **2026-09-06**, read off each rival's own pricing and user stats on the Apify API:

| Actor | 30-day users | Per row | Extra |
|---|---|---|---|
| `dltik/francetravail-scraper` | 25 | $0.0015 | + $0.01 AI-analysis event |
| `shahidirfan/france-travail-scraper` | 12 | $0.00099 | + $0.0005 run start |
| `blackfalcondata/francetravail-scraper` | 7 | $0.00095 | + $0.00005 run start |
| `santamaria-automations/france-travail-scraper` | 2 | $0.003 list / $0.005 detail | + $0.001 run start |
| `scrapeai/france-travail-scraper` | 1 | $0.00199 | + $0.00005 run start |
| `studio-amba/francetravail-scraper` | 1 | $0.002 | + $0.005 run start |
| `moving_beacon-owner1/france-travail-jobs-scraper` | 1 | $0.00999 | + $0.00005 run start |
| **this actor** | — | **$0.0009** | **nothing** |

**Your "Max total charge" is a hard ceiling, and this actor stays under it.** Rows are delivered and
billed atomically through `Actor.pushData(items, 'job-scraped')`, and the paired enrichment event is
*reserved in dollars before a single offer page is fetched* — so a run cannot deliver a row it
cannot bill, and cannot bill past your cap. Measured on three caps, all with `maxResults: 0`:
`SQJFwVRkIr5qXeTSy` at **$0.05** with details on stopped at 35 rows + 35 enrichments = **$0.0490**;
`zmPuAquM7mJGQpNgJ` at **$0.03** stopped at 21 + 21 = **$0.0294**; list-only `dlSM3uZs08HqB5qc7` at
**$0.02** stopped at 22 rows = **$0.0198**.
Duplicates and rows your own filters remove are never billed.

**And when the cap stops the run at zero rows, the run says THAT — it does not blame the market.**
Run `8rcT0ZZwt4EVmITN3` was launched with `maxTotalChargeUsd: $0.0005`, below the $0.0009 price of a
single row. It delivered 0 rows, charged **$0.00**, and its status message reads:

> Nothing was delivered because this run hit YOUR "Max total charge" ceiling of $0.0005 before a
> single row could be billed — one row costs $0.0009 here. The source was fine: 1 quer(y/ies) ran,
> the site reported 1093 matching offer(s) and 20 candidate(s) were read off the result list. Raise
> "Max total charge" to at least $0.0009 (and higher for more rows) and re-run — this is not an
> empty market.

Every zero-row ending is branded that way: the charge cap, a **truncated crawl**, a page-0 failure,
your own filters, all-duplicates, or a slice that really is empty each get their own sentence with
their own numbers. A run that stopped for one reason will never tell you it stopped for another.

***

### 🛡️ Reliability, transport and cost

- **Plain HTTP. No browser.** 512 MB. Measured 2026-09-06 on the escalation ladder against the same
  search: a bare home IP answers **HTTP 200 with a 143,571-byte result page**; the default Apify
  **datacenter** pool delivered 40 rows in 7 s (run `gGOIUb3VhoSRbYfsW`); `proxyCountry: "FR"`,
  which switches the run to RESIDENTIAL, delivered 20 rows in 21 s (run `ZpO0tPNcEAaWeHPPf`).
- **You do not need residential proxies here.** One rival's store page claims this site "requires
  residential proxies to bypass geo-restrictions". That is measurably false, and it is why this
  actor defaults to the cheap datacenter pool. Setting `proxyCountry` switches to RESIDENTIAL,
  because Apify's shared datacenter group has no country selection — but nothing measured needs it.
- **Failure profile, 2026-09-06:** across **41 platform runs, 71 queries, 303 result-list pages and
  1,179 offer-page fetches** — exactly **one** offer-page fetch failed after both of its attempts
  (**0.07%**), and it was a socket-level status 0. Never a 403, never a CAPTCHA, never a rate limit.
  Every request retries on a **fresh proxy exit IP** (`Max request retries`, default 2), because
  rotating the exit is what actually recovers a dead tunnel; retrying the same one does not.
- **A transport error never fails your run.** One of those 71 queries lost its page 0 to a transient
  HTTP 500 and then a proxy `590 UPSTREAM502` on all three attempts (run `uWOtYcQXq8ijPQ3DE`). It
  was reported as a warning, the *other* query in that run crawled and delivered normally, and the
  run finished SUCCEEDED. A run is only marked failed when *not one page* of the site could be
  read.
- **A URL this actor cannot use stops the run — it never quietly crawls something else.** Give it
  only start URLs it has to reject and it fails naming each one and why, with nothing charged (run
  `Lc6NUHEScuiLu9uJn`, $0.00). A completely empty input is the only case that falls back to the
  documented comptable/Nord sample.
- **A mistyped canonical URL is called out, not silently empty.** France Travail answers an unknown
  `/offres/emploi/<métier>/<lieu>/s..m..` code with a redirect to its own 404 page and **HTTP 200**,
  which otherwise looks exactly like a slice with no vacancies. Run `TdzhYcHOsuTDd5vEG` shows the
  message. Copy start URLs from the site's own address bar after searching.
- **A crawl that truncates is reported as a truncation, never as a clean success.** If a load-more
  hop dies, the query restarts once on a fresh identity; if it cannot be recovered, the query is
  recorded in `RUN_SUMMARY.truncatedQueries` with the page it reached, the rows it got and the
  result total it was working against, and the run's status message carries the warning. A query
  that stops on a limit *you* set is recorded separately as `boundedQueries` — that is not a
  failure, and the two are never conflated. Both were exercised today: `Owr9Qf0yysrTSFfKs` reports
  two `boundedQueries` (`"Max offers per query" = 200`, 10 pages / 200 rows of 274 matching) and
  zero truncations; `hBaKobomIam6sunaC` reports one (`"Max pages per query" = 3`, 3 pages / 60 rows
  of 1,093 matching).
- **Speed:** 1,000 offers list-only in **74 s** at its fastest and **3 min 09 s** on the run that
  ships this README (`OMOJ5VkckkbcB5PLV`); 500 offers with full detail enrichment 25 pages deep in
  **9 min 11 s** at concurrency 5 (`e82xg4ftZkb8KqWOR`, 506 detail calls, 3 failures); a 40-offer
  list run in **7 s** (`gGOIUb3VhoSRbYfsW`). Offer-page latency is the site's and it varies by slice and by
  hour: a same-moment A/B on infirmier/Rhône for 30 enriched offers took **4 min 37 s at
  concurrency 10** (`Dhj55MdPvDdWEQrnl`) against **4 min 52 s at concurrency 5**
  (`IJz32jZThTIWRGfEM`) — raising concurrency did not buy speed there, which is why the default
  stays 5 and 10 is the cap. The result-list crawl is always sequential; its pagination is
  server-side session state that concurrency would break.

***

### ❓ FAQ

**Do I need a France Travail account or an API key?** No. Every page this actor reads is public and
unauthenticated.

**Can I get more than 1,000 offers from one search?** Not from one search — that is the site's own
ceiling. Use `sliceBy` to partition the corpus; there are 27,454 métier × departement slices, each
worth up to 1,000 rows.

**Does it return offers from other job boards?** Yes, and it labels them. France Travail republishes
syndicated listings from HelloWork, TalentPlug, Meteojob, Cadremploi, APEC, DirectEmploi and ~30
others — 34 distinct origins on the 1,000-row `LIST` sweep, **923 of those 1,000 rows**. But it
shows its own offers first: the first 60 rows of that same sweep were 60/60 direct.
`isPartnerOffer` and `offerOrigin` are on every row, and `includePartnerOffers: false` or
`onlyDirectEmployerOffers: true` removes the syndicated ones.

**So which rows actually carry the phone?** The direct ones, and only the direct ones — **37.8%** of
France Travail's own offers (28 of 74) against **0.0%** of syndicated ones (0 of 423), on run
`e82xg4ftZkb8KqWOR`; confirmed at **37.7%** on the 77 direct rows of `ndLl4Bqc7Jsh7cihB`, which
selected the same cohort through the site's own `offresPartenaires=false` parameter instead. Eleven
other enriched fields split the same way — see the three-column table above. If the phone is what
you are buying, set `onlyDirectEmployerOffers: true`.

**How fresh is the data?** `postedWithinDays: "1"` returns only offers filed in the last 24 hours —
102 of 2,201 for commercial/Paris on 2026-09-06.

**Does "Max offers per query" mean exactly that?** Yes. It counts rows *delivered* by that query and
is enforced inside the page, so a cap of 50 delivers 50, not the whole 60-row page that contains the
50th. Run `iJ051hyiCALkCOMbn`: 2 queries at `maxResultsPerQuery: 50` saw 120 candidates and delivered
exactly **100**.

**Will an empty or partly-filled input fail?** No. An input with no keyword, place or URL falls back
to a documented sample and runs (run `h9GpCFV3glPMrJ7Rf`, 100 rows); a slice that is genuinely empty
— there really are no astrophysicist jobs in Loire-Atlantique, the site itself answers "Aucune
offre" — exits cleanly with zero rows and says so, rather than reporting a transport failure (run
`M4A0E3dLAHOfC1qZD`).

**Can I get the applicant's or candidate's data?** No. This actor reads employer-published vacancy
pages only. It never touches a candidate profile, a login-walled page or a private endpoint.

**Is there an apply URL?** No, and deliberately. `/candidature/postulerenligne/<offerId>` answers
HTTP 200 but with a 4 KB login shell, not an application form — shipping it as an "apply link" would
be a field that is 100% populated and 100% useless. Use `offerUrl`.

**What about the recruiter's e-mail?** It does not exist on this surface, so there is no column for
it — see above.

***

### ⚖️ Legal, robots and fair use

**robots.txt, quoted verbatim.** `https://candidat.francetravail.fr/robots.txt` contains **no
`Allow:` directives at all**. Two of its `Disallow` lines are the ones that matter here:

```
Disallow: *motsCles=*
Disallow: *t:ac=*
```

Read that honestly: **page 0 of a canonical `/offres/emploi/<métier>/<lieu>/s..m..` path is outside
every Disallow rule and is robots-clean. Every route past row 20 is not** — the canonical pagination
route carries `?t:ac=`, and the raw route carries `motsCles=`. There is no robots-clean way to read
past the first 20 rows of a query on this site. The four sitemaps this actor reads for slicing
(`secteurs-metiers.xml`, `regions-departements-villes.xml`,
`croisements-metiers-departements.xml`, `croisements-metiers-villes.xml`) are the ones robots.txt
itself advertises.

**Data.** Everything returned is published by employers for public consumption on a state-run
service. This actor reads no login-walled page, forges no signature, solves no CAPTCHA and bypasses
no anti-abuse control. `employerPhone` and `employerName` are business contact details published by
the employer on the offer itself.

**Your responsibility.** `employerContactName` and `employerPhone` name and reach a real person at
the hiring company. They appear on **39.2%** and **37.8%** of France Travail's own offers and on
**0%** of syndicated ones (run `e82xg4ftZkb8KqWOR`, 74 direct / 423 syndicated, 2026-09-06). If you
process them you are the data controller under the GDPR: have a lawful basis, honour access and
erasure requests, and respect France Travail's own terms. Nothing here is legal advice.

**Rate.** Default concurrency 5 with a sequential list crawl — deliberately gentle. Please leave it
there unless you have a reason.

# Actor input Schema

## `searchTerms` (type: `array`):

Free-text keywords, one crawl per term — sent to France Travail as `motsCles`. Accents and spaces work ("agent de sécurité" resolves fine). MAY BE LEFT EMPTY: with a location alone you get the whole geographic slice, which is the sweep a staffing agency actually wants — measured 30,291 live offers for Paris (75) with no keyword at all (2026-09-06).

## `location` (type: `array`):

Maps to `lieux`. Three verified forms: `75D` departement (all 106 codes shipped, incl. `2AD`, `2BD`, `971D`…`988D`), `11R` INSEE region, or a 5-digit commune INSEE code (`69123` = Lyon). Region supply measured 2026-09-06: 11 Île-de-France 122,040 · 84 Auvergne-Rhône-Alpes 115,549 · 75 Nouvelle-Aquitaine 73,189 · 76 Occitanie 63,014 · 52 Pays de la Loire 62,501 · 44 Grand Est 62,178 · 93 PACA 59,444 · 53 Bretagne 55,906 · 32 Hauts-de-France 47,847 · 28 Normandie 38,778 · 27 Bourgogne-Franche-Comté 33,025 · 24 Centre-Val de Loire 31,885 · 04 La Réunion 4,959 · 94 Corse 2,214 · 02 Martinique 1,563 · 01 Guadeloupe 1,345 · 03 Guyane 1,239 · 06 Mayotte 544. France Travail IGNORES a repeated `lieux`, so each entry here is crawled as its own query and the results merged.

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

Paste France Travail result URLs. BOTH shapes are accepted and both paginate: the raw search `https://candidat.francetravail.fr/offres/recherche?motsCles=comptable&lieux=59D` and the canonical SEO path `https://candidat.francetravail.fr/offres/emploi/comptable/nord/s12m1d59`. (Through the API this field takes objects, `[{"url": "https://…"}]`, not bare strings.) Canonical paths are enumerable from France Travail's own sitemaps — 27,454 métier × departement and 38,850 métier × ville `<loc>` entries counted 2026-09-06 — which is what the métier slicing modes below use. A `/rechercheoffre/emploi/...` URL is a link hub with no result list and is skipped; if every URL you give is skipped the run stops and says which ones and why, rather than crawling something else.

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

Maps to `typeContrat`. Measured on commercial/Paris 2026-09-06: CDI 1,770 · CDD 312 · MIS (intérim) 61 · SAI (saisonnier) 0 ("Aucune offre"). France Travail takes only the FIRST `typeContrat` when it is repeated, so several selections are crawled as separate queries and merged.

## `postedWithinDays` (type: `string`):

Maps to `emission` — the hiring-TRIGGER lever. Measured on commercial/Paris 2026-09-06: 1 day 102 · 7 days 612 · 31 days 1,513 of 2,201 total. Set this to 1 or 3 and schedule the run to get a clean "new roles since yesterday" feed. 0 = no age filter.

## `radiusKm` (type: `string`):

Maps to `rayon`, and it WORKS ONLY with a 5-digit commune INSEE code in "Locations". Measured on commercial + Lyon 69123, 2026-09-06: 503 offers at 0 km, 1,554 at 10 km, 1,877 at 20 km, 2,164 at 30 km, 2,769 at 50 km. Note that France Travail's own default when no `rayon` is sent is 10 km (1,554, identical to an explicit rayon=10), so this actor always sends the value — including `rayon=0` — for a commune search, otherwise "0 km" would silently return the 10 km result (runs QSayG4Wc47wdR56De at 0 km and ymql3VTaHUJcdqz85 at 10 km). It is INERT at departement (`75D`) and region (`11R`) level (comptable+59D answers 1,097 with and without it), so it is not sent for those.

## `salaryMin` (type: `integer`):

Maps to `salaireMin`, always paired with the period below. Measured on commercial/Paris 2026-09-06: 30,000 annual → 1,218 offers; 2,000 monthly → 2,158. 0 = off.

## `salaryPeriod` (type: `string`):

The period the minimum salary above is expressed in — maps to `periodeSalaire`.

## `weeklyHours` (type: `string`):

Maps to `dureeHebdo`. Measured on commercial/Paris 2026-09-06: full time 231 · part time 9 (the site only sets this flag on a minority of offers, so it is a narrowing filter, not a split of the whole list).

## `qualificationLevel` (type: `string`):

Maps to `qualification`, France Travail's own qualification ladder from 0 (manœuvre) to 9 (cadre). Measured on commercial/Paris 2026-09-06: 0 → 765 offers, 9 → 630. Leave empty for any level.

## `professionalDomain` (type: `string`):

Maps to `domaine`, e.g. `M17` (commerce/vente). Measured 439 offers on commercial/Paris 2026-09-06. NOTE — France Travail DROPS this parameter when there is no keyword, and you then get the whole board back, so it is only sent alongside a search term.

## `includePartnerOffers` (type: `boolean`):

Maps to `offresPartenaires`. France Travail republishes offers syndicated from HelloWork, APEC, Meteojob, Cadremploi, TalentPlug, DirectEmploi and ~30 other boards. High impact and measured: commercial/Paris returns 2,201 with partners and 155 without (2026-09-06). Every row is stamped with `offerOrigin` and `isPartnerOffer` either way, so you can tell them apart downstream.

## `onlyDirectEmployerOffers` (type: `boolean`):

Keep only rows whose origin is France Travail itself — filed with the state service directly, not syndicated from a commercial job board. Applied on our side over the rows, so nothing you drop is billed. The site lists its OWN offers first: on a full 1,000-row comptable/Nord sweep (run dEcliLfEWocFWlpN4, 2026-09-06) the first 60 rows were 60/60 direct and the sweep as a whole was 78 direct to 922 syndicated, across 34 origin boards. THIS IS THE COHORT SELECTOR FOR EVERY RICH FIELD. Measured 2026-09-06 on run e82xg4ftZkb8KqWOR (497 enriched): direct offers carry a recruiter phone 37.8% of the time against 0.0% for syndicated ones, a named contact 39.2% against 0.0%, skills 97.3% against 0.0% and an employer page URL 62.2% against 0.7%. Because this filter runs on the RESULT LIST, syndicated offers are dropped before their pages are ever fetched: run DaPhQaiDHjc6zJwvd made 23 offer-page requests where run n5kXSn6PLfM1hj2tN, the same recipe without it, made 1,000.

## `excludeKeywords` (type: `array`):

Drop offers whose title, teaser or employer name contains any of these (case-insensitive). France Travail has no server-side NOT, so this runs on our side — excluded rows are never billed.

## `fetchOfferDetails` (type: `boolean`):

One extra request per offer that unlocks the fields the result list does not carry. FILL DEPENDS ON WHO PUBLISHED THE OFFER, and the gap is not small. Measured 2026-09-06, run e82xg4ftZkb8KqWOR (comptable/Nord 59D crawled 25 pages deep: 500 delivered, 497 enriched, 506 detail calls, 3 failures) — France Travail's OWN offers (n=74) / SYNDICATED partner offers (n=423) / the blend over all 497: recruiter PHONE 37.8% / 0.0% / 5.6% · named contact 39.2% / 0.0% / 5.8% · skills 97.3% / 0.0% / 14.5% · employer page URL 62.2% / 0.7% / 9.9% · headcount band 68.9% / 27.0% / 33.2% · NAF sector 100% / 71.6% / 75.9% · pay figures 75.7% / 70.0% / 70.8% · experience, qualification level, métier code, dates and the full offer text 100% on both. A full-depth 1,000-row sweep of that query (run OMOJ5VkckkbcB5PLV) is 92.3% syndicated, so a deep crawl lands near the blended column and a shallow one near the direct column. Use "Only direct-employer offers" to select the rich cohort. Bills the separate `job-detail-enriched` event, only for rows that really came back enriched.

## `onlyWithPhone` (type: `boolean`):

Keep only rows that carry `employerPhone` — the direct line of the person recruiting. THE PHONE IS ON DIRECT OFFERS ONLY: measured 2026-09-06 on run e82xg4ftZkb8KqWOR (497 enriched offers), France Travail's own offers carry one 37.8% of the time (28 of 74) and syndicated partner offers 0.0% of the time (0 of 423); the blend over all 497 was 5.6%, because a full-depth slice is ~92% syndicated (923 of 1,000, run OMOJ5VkckkbcB5PLV). So turn "Only direct-employer offers" ON with this: it drops the syndicated offers BEFORE their pages are fetched. Measured on the same recipe with and without that flag — run DaPhQaiDHjc6zJwvd made 23 offer-page requests in 3 min 07 s and delivered 9 rows; run n5kXSn6PLfM1hj2tN made 1,000 requests in 9 min 48 s and delivered 6. Turns "Fetch offer details" on automatically, because the phone only exists on the offer page. Dropped rows are never billed.

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

Keep only rows with a numeric `salaryMin`. Measured 2026-09-06 on run e82xg4ftZkb8KqWOR (497 enriched offers): 70.8% overall, 75.7% on France Travail's own offers and 70.0% on syndicated ones — the one enriched field that barely differs between the two. Verified on run QYDNpVtamg1eRM4Y4 (100 delivered of 140 candidates, 28 dropped): 0 of 100 delivered rows had a null `salaryMin`, including the one row whose offer-page fetch failed — a failed fetch can no longer satisfy this filter. Turns "Fetch offer details" on automatically, because the figures only exist on the offer page. Dropped rows are never billed.

## `sliceBy` (type: `string`):

France Travail hard-caps EVERY query at 1,000 rows — 50 pages of 20, verified by asking for 160 pages and receiving exactly 50. Coverage past that needs partitioning, which is what this does. `departement` and `region` re-run your search once per French place code; `contractType` once per CDI/CDD/MIS/SAI; `postedWindow` once per 1/3/7/14/31-day bucket; `metierDepartement` and `metierVille` pull France Travail's own sitemaps (27,454 and 38,850 canonical métier × place pages, counted 2026-09-06) and crawl the ones matching "Métier filter" and "Locations". Every slice is deduplicated on the offer number before anything is billed, so overlapping sweeps never double-charge.

## `sliceMetiers` (type: `array`):

Substrings matched against the métier slug of France Travail's canonical URLs — e.g. `infirmier`, `chauffeur`, `comptable`, `electricien`. Accent- and case-insensitive. Leave empty and the "Search terms" above are used instead. Only read by the `metierDepartement` / `metierVille` slicing modes.

## `maxSlices` (type: `integer`):

Upper bound on how many QUERIES a sweep expands into — places × keywords × contract types, not places alone — so a métier × ville run cannot quietly become 38,850 crawls. Run xa0KdP4wf4qSmmZPM: 2 keywords × departement at maxSlices 6 expanded to exactly 6 queries / 120 rows, and the log says so when it caps. Each slice is up to 1,000 rows.

## `maxResults` (type: `integer`):

Hard stop across every query and slice, counted in rows DELIVERED. 0 = no limit. Note the site's own ceiling: any single query tops out at 1,000 rows, so going past that needs a slicing mode above.

## `maxResultsPerQuery` (type: `integer`):

Per-query stop, counted in rows DELIVERED by that query (not rows read), useful when you sweep many slices and want an even sample from each. It is enforced inside the page, so a cap of 50 delivers exactly 50, not the whole 60-row page that contains the 50th — run iJ051hyiCALkCOMbn saw 120 candidates across 2 queries and delivered exactly 100. 0 = the site's own ceiling of 1,000.

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

Cheap early exit for buyers who only want the freshest page or two. The site itself stops at 50 pages; higher values are clamped to 50.

## `deduplicateBy` (type: `string`):

The 7-character France Travail offer number (e.g. `213KBGC`) is the national identifier the vacancy is published under, and it is the dedupe key. Verified 1,000 unique offer numbers across a contiguous 1,000-row / 50-page crawl (run dEcliLfEWocFWlpN4, 2026-09-06); the real collision risk is BETWEEN slices — a 5-window postedWindow sweep saw 260 candidates and dropped 153 as duplicates (run QH9XE2ffhxyVhK59H) — which is why deduplication is global to the run and applied before anything is billed.

## `outputFormat` (type: `string`):

`flat` gives one level of plain columns, which is what a CRM / Clay / Sheets import wants, and it is the shape the dataset's field types and table views describe. `nested` groups the same values under employer / location / contract / salary / profile / dates objects for API consumers — the Console's table views do not apply to that shape.

## `includeRawHtml` (type: `boolean`):

Debug lever — attaches the offer page's raw HTML to each enriched row. Makes the dataset very large; off by default.

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

Apify DATACENTER is the tested default and it is enough. Measured 2026-09-06: a bare home IP answers HTTP 200 with a 143,571-byte result page, the default datacenter pool delivered 40 rows in 5 s (run VcVf9ITvpfYIAeA58) and RESIDENTIAL-FR delivered 20 rows in 21 s (run ZpO0tPNcEAaWeHPPf) — same rows, no geo-block on any rung. A rival actor's claim that this site "requires residential proxies to bypass geo-restrictions" is measurably false — do not pay for residential here.

## `proxyCountry` (type: `string`):

Two-letter country for the Apify proxy, e.g. `FR`. Left EMPTY on purpose — no geo-block was measured from any exit IP on 2026-09-06, so pinning a country only narrows the pool. Note that Apify's shared DATACENTER group has no country selection (it answers 407), so setting this switches the run to RESIDENTIAL proxies, which cost more. Both returned rows the same minute (runs VcVf9ITvpfYIAeA58 datacenter, ZpO0tPNcEAaWeHPPf RESIDENTIAL-FR).

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

Parallel offer-page fetches. Offer-page latency is the site's and it varies by slice and by hour, so more parallelism does not reliably buy speed: a same-moment A/B on infirmier/Rhône for 30 enriched offers took 4 min 37 s at 10 (run Dhj55MdPvDdWEQrnl) against 4 min 52 s at 5 (run IJz32jZThTIWRGfEM), while a 500-offer deep run at 5 finished in 9 min 11 s (run e82xg4ftZkb8KqWOR, 497/500 enriched, 506 detail calls, 3 failures) — all 2026-09-06. 5 is the default, 10 the cap. The result-list crawl is always sequential — its pagination is server-side session state that concurrency would break.

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

Across 2026-09-06's 41 platform runs — 71 queries, 303 result-list pages and 1,179 offer-page fetches — exactly one fetch failed after both of its attempts (0.07%), and it was a socket-level status 0. Never a 403, never a challenge, never a rate limit. A retry on a FRESH proxy exit is what clears them; 2 is the default.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout. 60 s is comfortable for the biggest result pages — the comptable/Nord page 0 measured 143,571 bytes on 2026-09-06.

## Actor input object example

```json
{
  "searchTerms": [
    "comptable"
  ],
  "location": [
    "59D"
  ],
  "startUrls": [],
  "contractTypes": [],
  "postedWithinDays": "0",
  "radiusKm": "0",
  "salaryMin": 0,
  "salaryPeriod": "A",
  "weeklyHours": "0",
  "qualificationLevel": "",
  "professionalDomain": "",
  "includePartnerOffers": true,
  "onlyDirectEmployerOffers": false,
  "excludeKeywords": [],
  "fetchOfferDetails": false,
  "onlyWithPhone": false,
  "onlyWithSalary": false,
  "sliceBy": "none",
  "sliceMetiers": [],
  "maxSlices": 25,
  "maxResults": 40,
  "maxResultsPerQuery": 0,
  "maxPagesPerQuery": 50,
  "deduplicateBy": "offerId",
  "outputFormat": "flat",
  "includeRawHtml": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "proxyCountry": "",
  "maxConcurrency": 5,
  "maxRequestRetries": 2,
  "requestTimeoutSecs": 60
}
```

# Actor output Schema

## `offers` (type: `string`):

The dataset of scraped France Travail offers (one item per offer, deduplicated on the offer number).

# 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 = {
    "searchTerms": [
        "comptable"
    ],
    "location": [
        "59D"
    ],
    "maxResults": 40,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/france-travail-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 = {
    "searchTerms": ["comptable"],
    "location": ["59D"],
    "maxResults": 40,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/france-travail-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 '{
  "searchTerms": [
    "comptable"
  ],
  "location": [
    "59D"
  ],
  "maxResults": 40,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/france-travail-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/france-travail-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/yCucAkTLt3RybuRbn/builds/aaLhJJtgrZwFYkHM3/openapi.json
