France Travail Jobs Scraper — 777K FR Offers + Employer Phone avatar

France Travail Jobs Scraper — 777K FR Offers + Employer Phone

Pricing

from $0.90 / 1,000 per offer returneds

Go to Apify Store
France Travail Jobs Scraper — 777K FR Offers + Employer Phone

France Travail Jobs Scraper — 777K FR Offers + Employer Phone

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.

Pricing

from $0.90 / 1,000 per offer returneds

Rating

0.0

(0)

Developer

Scrapers Delight

Scrapers Delight

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

Share

🇫🇷 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

FieldFillWhat it is
offerId100%The 7-character France Travail offer number (213KBGC) — the national identifier, and the dedupe key
offerUrl100%Direct link to the offer page
title100%Job title as published
employerName92.8% on LIST, 100% on FULLHiring company — confidential offers omit it; the offer page fills the rest in
locationText · departementCode · commune100%59 - Marcq-en-Barœul split into a departement code and a commune
contractLabel · contractType · workTime100%Intérim - 2 MoisMIS, plus temps plein / partiel
postedRelative100%Publié aujourd'hui, Publié il y a 7 jours
teaser100%The summary shown in the list — 196 characters average across all 1,000 LIST rows
offerOrigin · isPartnerOffer100%Which board the offer came from. LIST saw 34 distinct origins: France Travail, HELLOWORK, TALENTPLUG, METEOJOB, CADREMPLOI, APEC, DIRECTEMPLOI, BROADBEAN, FLATCHRThis is the field that predicts every other field's fill — see the next table
latitude · longitude23.5% on LIST, 15.3% on FULLFrance Travail's own map marker. Fill varies hard by slice — many offers carry an empty coordonnees
resultTotalForQuery100%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.

FieldDirect (n=74)Syndicated (n=423)Blended (n=497)What it is
employerPhone37.8%0.0%5.6%The recruiter's direct line, from the page's schema.org telephone. Zero of 423 syndicated offers carried one
employerContactName39.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
employerSizeBand68.9%27.0%33.2%20 à 49 salariés, 250 à 499 salariés
employerPageUrl62.2%0.7%9.9%The company's France Travail mini-site on recrute.francetravail.fr
employerAbout47.3%67.8%64.8%The company's own blurb — the one field syndicated offers carry more often
industryNaf100%71.6%75.9%NAF sector label, e.g. Activités comptables
postalCode · region100% / 100%99.8% / 100%99.8% / 100%Full postal address components (country 100% everywhere)
salaryMin · salaryMax · salaryUnit75.7%70.0%70.8%Parsed figures + YEAR / MONTH / HOUR
salaryText77.0%70.4%71.4%As printed: Salaire brut : Annuel de 40000.0 Euros à 45000.0 Euros
benefits73.0%35.9%41.4%Primes, Titres restaurant, Complémentaire santé
experienceRequired100%100%100%3 An(s)
qualification100%64.5%69.8%Employé qualifié
skills97.3%0.0%14.5%The offer's competence list — a France Travail structure that syndicated offers simply do not have
softSkills · education · languages · drivingLicences35.1% / 39.2% / 5.4% / 5.4%0% on all four5.2% / 5.8% / 0.8% / 0.8%The rest of the "Profil souhaité" block
workHours · employmentType100% / 100%1.9% / 0.0%16.5% / 14.9%35H/semaine Travail en journée · FULL_TIME
contractNature98.6%99.3%99.2%Contrat travail
travelRequirement32.4%0.0%4.8%Déplacements : Jamais
datePosted · validThrough · metierCode100%100%100%Real ISO dates, plus France Travail's internal métier id (11213)
description100%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:

sliceByWhat it doesSlices available
departementRe-runs your search once per French departement106 codes incl. 2A, 2B, 971988
regionOnce per INSEE region18
contractTypeOnce per CDI / CDD / MIS / SAI4
postedWindowOnce per 1 / 3 / 7 / 14 / 31-day bucket5
metierDepartementCrawls France Travail's own canonical métier × departement pages27,454
metierVille…and its métier × ville pages38,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:

InputParameterMeasured effect, 2026-09-06
Search termsmotsClesAccents and spaces fine; may be left empty (Paris with no keyword = 30,291)
Locationslieux75D departement (30,291) · 11R region (122,040) · 69123 commune
Contract typestypeContratCDI 1,770 · CDD 312 · MIS 61 · SAI 0 ("Aucune offre")
Posted withinemission1d 102 · 7d 612 · 31d 1,513 of 2,201
Minimum salary + periodsalaireMin+periodeSalaire30k annual → 1,218 · 2k monthly → 2,158
Full/part timedureeHebdo231 · 9
Qualification levelqualification0 → 765 · 9 → 630
Professional domaindomaineM17 → 439 (dropped by the site when there is no keyword)
RadiusrayonOnly 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 offersoffresPartenaires2,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 kmcommercial + 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):

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

Run XwAdWDHqWhHjuFyf0409 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):

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

Run Owr9Qf0yysrTSFfKs798 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:

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

Run ndLl4Bqc7Jsh7cihB77 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:

{
"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 L09QxuzBWiHhO4y0J200 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.

EventPriceWhen it fires
job-scraped$0.0009 per offerOnce per unique offer delivered to your dataset
job-detail-enriched$0.0005 per offerOnly 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:

Actor30-day usersPer rowExtra
dltik/francetravail-scraper25$0.0015+ $0.01 AI-analysis event
shahidirfan/france-travail-scraper12$0.00099+ $0.0005 run start
blackfalcondata/francetravail-scraper7$0.00095+ $0.00005 run start
santamaria-automations/france-travail-scraper2$0.003 list / $0.005 detail+ $0.001 run start
scrapeai/france-travail-scraper1$0.00199+ $0.00005 run start
studio-amba/francetravail-scraper1$0.002+ $0.005 run start
moving_beacon-owner1/france-travail-jobs-scraper1$0.00999+ $0.00005 run start
this actor$0.0009nothing

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.


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.