France Travail Jobs Scraper — 777K FR Offers + Employer Phone
Pricing
from $0.90 / 1,000 per offer returneds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
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: trueis 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— runOMOJ5VkckkbcB5PLV: 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— rune82xg4ftZkb8KqWOR: the same query crawled 25 pages deep withfetchOfferDetails: 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
employerEmailcolumn. The offer page carries anitemprop="email"tag on every offer and itscontentattribute is empty on all of them — 0 of 300 enriched offers onFULL. 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, takeemployerName+employerPageUrlinto 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 internalmetierCode, 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
rayonparameter is 10 km —commercial+69123with no radius returns the same 1,554 offers as an explicitrayon=10. So "0 km — the commune only" is only true ifrayon=0is actually sent, and this actor sends it: runQSayG4Wc47wdR56Delogs…&lieux=69123&rayon=0and the headline 503 offres, against 1,554 on the pairedradiusKm: "10"runymql3VTaHUJcdqz85.
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 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
onlyDirectEmployerOffersis 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, runJaEKXczVgVOrIBXGTmade 1,004 offer-page requests in 12 min 44 s and delivered 8 rows; with it, runDaPhQaiDHjc6zJwvdread 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 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:
{"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:
{"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 (runZpO0tPNcEAaWeHPPf). - 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
proxyCountryswitches 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 UPSTREAM502on all three attempts (runuWOtYcQXq8ijPQ3DE). 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. RunTdzhYcHOsuTDd5vEGshows 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.truncatedQuerieswith 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 asboundedQueries— that is not a failure, and the two are never conflated. Both were exercised today:Owr9Qf0yysrTSFfKsreports twoboundedQueries("Max offers per query" = 200, 10 pages / 200 rows of 274 matching) and zero truncations;hBaKobomIam6sunaCreports 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.