# APEC Jobs Scraper — French Cadre Jobs, Employer Type & NAF (`scrapersdelight/apec-jobs-scraper`) Actor

Scrape apec.fr, France's executive job board (~88,000 live offers). Filter by keyword, place, salary, contract, sector, métier, remote and 20 more. Every row names the employer TYPE - direct employer, recruitment cabinet, agency, ESN or resold job-board feed - plus its NAF industry code.

- **URL**: https://apify.com/scrapersdelight/apec-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

$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

## 🇫🇷 APEC Jobs Scraper — French cadre jobs, with the employer classified

Scrape [apec.fr](https://www.apec.fr) — the **Association pour l'emploi des cadres**, France's
national executive and manager job board — as **one clean row per offer**, with the things the
board itself only exposes as filters already stamped on the row:

- **Who is really hiring** — direct employer, recruitment cabinet, staffing agency, IT-services
  company (ESN), or a partner job-board feed resold onto APEC.
- **The employer's statutory industry code** — the French **NAF/APE code** and its label
  (`7112B — INGÉNIERIE, ÉTUDES TECHNIQUES`), decoded from APEC's own nomenclature.
- **A parsed salary** — min, max, currency, period, gross-or-net — out of a French sentence.
- **The French place hierarchy** — city, department code, department name, region, country.

**43% of APEC is resale.** In a blind 100-row sample of the newest `Partenaire` offers, taken 2026-09-04, **100 of 100** were
posted as "Hellowork" — the job board's name, not the employer's.
This actor excludes that feed by default, and when you ask for it, labels it. Verified against all
five ranked rival APEC actors' input schemas on 2026-09-04: **none of them expose the employer type
at all.**

***

### What you get

One record per offer. Every number on this page is **measured** — from a 1,000-row run on
2026-09-04, and from the offline validation gate (82 assertions, all passing) — never estimated.

```json
{
  "numeroOffre": "179364504W",
  "jobId": 179364504,
  "jobUrl": "https://www.apec.fr/candidat/detail-offre.html/detail-offre/179364504W",

  "title": "Technicien d'Usinage F/H",
  "companyName": "SELFING",
  "isConfidential": false,

  "employerClass": "direct-employer",
  "employerClassLabel": "Entreprise",
  "isAggregatorListing": false,
  "postingOrigin": "ADEP",

  "locationText": "Saint-Vincent-de-Reins - 69",
  "city": "Saint-Vincent-de-Reins",
  "departmentCode": "69",
  "department": "Rhône",
  "region": "Auvergne-Rhône-Alpes",
  "country": "France",
  "latitude": 46.0738879,
  "longitude": 4.3866474,
  "isGeolocatable": true,

  "salaryText": "40 - 42 k€ brut annuel",
  "salaryMin": 40000,
  "salaryMax": 42000,
  "salaryCurrency": "EUR",
  "salaryPeriod": "YEAR",
  "salaryBasis": "GROSS",
  "salaryIsNegotiable": false,
  "salaryIsStatutoryMinimum": false,
  "hasNumericSalary": true,

  "contractType": "CDI",
  "contractDurationMonths": null,
  "remoteWork": null,

  "nafCode": "2813Z",
  "nafActivity": "FABRICATION D'AUTRES POMPES ET COMPRESSEURS",
  "sector": "Mécanique - Métallurgie",

  "publishedAt": "2026-09-04T06:16:32.000Z",
  "validatedAt": "2026-09-04T06:16:32.000Z",

  "descriptionSnippet": "Nous recherchons un Technicien d'Usinage (H/F) pour intervenir sur des opérations complètes de fabrication de pièces mécaniques, sur machines conventionnelles et/ou à commande numérique. …",
  "descriptionSnippetLength": 283,
  "descriptionIsTruncated": true,

  "lowApplicantCount": false,
  "apecQualityFlag": true,
  "isRealClient": true,
  "logoUrl": "https://www.apec.fr/media_entreprise/525266/logo_SELFING_525266.gif",
  "relevanceScore": 1.15,

  "searchKeyword": null,
  "searchLabel": "all APEC offers",
  "sourceUrl": null,
  "scrapedAt": "2026-09-04T06:19:28.501Z",

  "contractTypeCode": 101888,
  "remoteWorkCode": null,
  "employerClassCode": 143684,
  "nafCodeId": 101272,
  "sectorCode": 101774,
  "postingOriginCode": 101866
}
```

#### 📊 Field fill — measured on a live 1,000-row Apify run, 2026-09-04

Run `qmosjXQrePLfebhUT` — the whole board, newest first, aggregator feeds excluded (the default).
1,000 rows, 1,000 unique offer numbers, 12 pages, **7.4 seconds**.

| Field | Fill |
| --- | --- |
| `numeroOffre` · `jobId` · `jobUrl` | 100% |
| `title` · `companyName` | 100% |
| `employerClass` · `employerClassLabel` · `isAggregatorListing` | 100% |
| `postingOrigin` | 100% |
| `nafCode` · `nafActivity` · `sector` | 100% |
| `contractType` | 100% |
| `publishedAt` · `validatedAt` | 100% |
| `descriptionSnippet` | 100% (exactly 283 chars on 1,000 of 1,000 — see limits) |
| `locationText` · `country` | 100% |
| `city` · `departmentCode` · `department` · `region` | 99.8% |
| `salaryText` | 100% |
| `latitude` · `longitude` | 94.0% |
| `salaryMin` | 74.9% |
| `salaryMax` | 58.4% |
| `logoUrl` | 61.9% |
| `remoteWork` | 44.4% |
| `contractDurationMonths` | 11.2% |
| `isConfidential` · `lowApplicantCount` · `apecQualityFlag` · `isRealClient` · `isGeolocatable` · `hasNumericSalary` · `salaryIsNegotiable` | 100% (always present, true or false) |

Across those 1,000 rows: **151 distinct NAF codes**, **27 sectors**, **87 departments**, and the
four non-aggregator employer classes at **250 rows each** — the crawl steps them round-robin so one
class cannot eat the whole cap. 888 CDI / 73 mission d'intérim / 30 CDD / 9 alternance · 749 with a
numeric salary (582 a real range) · 251 "à négocier" · 9 flagged as having few applicants ·
10 confidential · 998 France, 2 abroad.

***

### 🎯 Who buys this

| Buyer | What they do with it |
| --- | --- |
| **Recruitment agencies & RPOs** | `employerClass: direct-employer` is a list of French companies hiring **without** an agency — the cleanest possible prospect list, and impossible to build from the other APEC actors. |
| **Staffing / ESN sales teams** | `it-services-esn` shows exactly which competitors are winning which client work, by NAF code and department. |
| **HR-tech and ATS vendors** | Companies posting cadre roles are actively spending on hiring. Filter by sector, department and salary band. |
| **Market and salary researchers** | Parsed salary min/max against NAF code, region, experience level and remote policy across ~90,000 live offers. |
| **Job-board and aggregator operators** | Feed the whole board, deduped, with the resale rows already separated out. |
| **Sourcers and candidates** | `lowApplicantCount` marks postings that have attracted few applications — nobody else surfaces it. |

***

### ⚙️ How it works

`POST https://www.apec.fr/cms/webservices/rechercheOffre` — the site's own JSON search API. No
cookie, no API key, no CSRF header, no login. Plain HTTP through the Apify datacenter proxy, 512 MB,
**no browser**. `robots.txt` on apec.fr is `User-agent: *` with **zero `Disallow` lines**.

Two supporting routes, both the site's own XHR: `lieuautocomplete` turns "Bordeaux" into a place id,
`metierAutocomplete` turns a fuzzy job title into APEC's canonical métier labels.

Labels come from a **3,502-entry nomenclature** — sectors, all 733 NAF codes, 502 métiers, contract
types, employer classes, 124 French places, 195 countries — fetched once from APEC's own
`referentielstatique` service and **bundled with the actor**, so a label never depends on a live call.
`refreshLabels: true` re-fetches it if you want same-day nomenclature.

***

### 🔧 Input — 56 parameters, all wired and tested

#### Search scope

| Field | What it does |
| --- | --- |
| `keywords` | Free-text terms, one crawl each. Measured: "developpeur" 3,295 offers · "commercial" 23,324 · "ingenieur" 21,148. Empty = the whole board. |
| `jobTitles` | Fuzzy titles expanded through APEC's own métier autocomplete into canonical labels. |
| `searchUrls` | Paste apec.fr search URLs. Parsed, never fetched — the query params map 1:1 onto the API. |
| `offerNumbers` | Exact lookup by `179364344W` or its numeric id. Feed yesterday's numbers back to see which are still live; the run log names the ones that have gone. |
| `excludeKeywords` | Client-side NOT on title + snippet. Excluded rows are never billed. |

#### Location

`location` (names: "Paris", "75", "Ile-de-France", "Allemagne", "Bordeaux") · `lieuxIds` (raw ids) ·
`radiusKm` + `centerLat` + `centerLng` · `onlyGeolocated`.

#### Who is hiring

`employerType` (the wedge) · `excludeAggregatorFeeds` (default **on**) · `stampEmployerClass`
(default **on**) · `includeConfidential` · `companyNameContains` · `excludeCompanies` ·
`postingOrigins` · `onlyLowApplicantCount`.

#### Role, contract, pay, date

`contractTypes` · `jobFunctions` (10 families + 46 sub-functions) · `jobFunctionIds` (502 individual
métiers) · `sectors` (29) · `experienceLevels` · `positionStatus` · `remoteWork` ·
`contractDurationBands` · `internshipDuration` · `educationLevel` · `travelZones` · `startDateWithin`
· `salaryMinK` + `salaryMaxK` · `minSalaryEur` + `maxSalaryEur` (strict) · `onlyWithNumericSalary` ·
`postedWithin` · `postedAfter` + `postedBefore`.

#### Sort, volume, output, plumbing

`sortBy` (DATE · ID · SCORE · COMPANY · GEOLOC) · `sortOrder` · `maxItems` · `maxItemsPerSearch` ·
`pageSize` · `startIndex` · `dedupeAcrossSearches` · `includeRawCodes` · `includeTitleHighlight` ·
`compact` · `outputFields` · `refreshLabels` · `proxyConfiguration` · `maxConcurrency` ·
`requestDelayMs` · `maxRetries` · `requestTimeoutSecs`.

#### Example input

```json
{
  "keywords": ["développeur"],
  "location": ["Ile-de-France"],
  "employerType": ["143684"],
  "contractTypes": ["101888"],
  "salaryMinK": 50,
  "salaryMaxK": 70,
  "maxItems": 500
}
```

***

### 💰 Pricing

**$0.0009 per offer returned — $0.90 per 1,000 — and no per-run start fee.**

| | Price per offer | Start fee per run |
| --- | --- | --- |
| **This actor** | **$0.0009** | **none** |
| `easyapi/apec-jobs-scraper` (28 monthly users) | $0.00299 | $0.09 |
| `shahidirfan/APEC-Jobs-Scraper` (17) | $0.001 | $0.005 |
| `blackfalcondata/apec-scraper` (4) | $0.0008 | $0.005 |

*Rival prices read live from each actor's `pricingInfos` on 2026-09-04.* Every credible rival charges
a start fee on top of the per-row rate; this one does not, which matters most on small and scheduled
runs. Duplicates removed across pages and searches, and rows dropped by your own filters, are
**never charged** — rows are shipped through Apify's budget-aware push, so delivered always equals
billed.

Measured on Apify: **1,000 offers in 7.4 seconds over 12 requests at 512 MB — $0.0054 of platform
compute**, against $0.90 of event billing. `chargedEventCounts` came back at exactly 1,000 for 1,000
rows delivered, and at exactly 50 for the 50-row demo input.

***

### ⚠️ Honest limits — please read before buying

1. **There is no full job description.** APEC truncates `texteOffre` to **exactly 283 characters on
   100% of rows** (1,000 of 1,000 measured; 600 of 600 and 300 of 300 in two earlier samples). It
   ships as `descriptionSnippet` with `descriptionIsTruncated: true` on the row. Every per-offer
   detail route on apec.fr is behind a DataDome interstitial (`/cms/webservices/offre/public`,
   `/offre/withLieu`, `/offre/existe` all 403; `/cms/webservices/offre/{id}` is 401), and **this
   actor does not touch them**. If you need full descriptions, this is not the tool.
2. **The salary band is an intersection filter.** APEC's own `salaryMinK`/`salaryMaxK` matches any
   offer whose published range *overlaps* the band, an open-ended "A partir de 70 k€" counts as
   unbounded above, and "A négocier" offers pass every band. Use `minSalaryEur` / `maxSalaryEur` for
   a hard floor or ceiling on the printed figure.
3. **A minimum salary sent alone does nothing on APEC.** Measured: `salaireMinimum` without
   `salaireMaximum` leaves `totalCount` at the unfiltered whole-board figure, with byte-identical
   rows. The actor
   completes a lone bound for you and says so in the log.
4. **`sector` and `fonctions` are French-language labels**, straight from APEC's nomenclature. They
   are not translated.
5. **Employer class needs the partitioned crawl.** APEC never returns it on the row. With
   `stampEmployerClass: false` you get one combined query and a blank `employerClass` column.
6. **Deep paging drifts.** New offers arrive at the head of a DATE sort while you page, so contiguous
   pages overlap: measured 2.67% at the head and 4.33% mid-corpus over 600-row bands, versus 0.88%
   sorting by ID. Duplicates are removed on `numeroOffre` before billing either way; `sortBy: "ID"`
   is the steadier deep crawl.
7. **`apecQualityFlag` and `isRealClient`** are APEC's own undocumented internal booleans
   (`indicateurOqa`, `clientReel`). They are passed through honestly; we do not know their exact
   definition and do not claim one.
8. **Education level is a thin facet** — Bac+5 returned 53 offers of ~88,000. Expect small results.
9. `numeroOffre` is the stable id and the dedupe key. It is 1:1 with the numeric `id` (541 unique of
   each across 600 sampled rows, zero divergence).

***

### ❓ FAQ

**What is APEC?**
The Association pour l'emploi des cadres — France's public-interest body for executive, manager and
engineer employment. Its board carries roughly 90,000 live offers, almost all cadre-level.

**Do I need an APEC account or API key?**
No. The search API needs no cookie, no token and no login.

**How many offers are there?**
`totalCount` read live at the end of 2026-09-04: **91,002**. By employer class: Entreprise 19,090 ·
Cabinet de recrutement 17,657 · Agence d'emploi 10,027 · SSII/ESN 4,998 · Partenaire (resold feed)
39,123. The board moves all day — it read 87,843 twelve hours earlier — so treat any single figure
as a reading, not a constant.

**What does "exclude aggregator feeds" actually remove?**
The `Partenaire` convention — offers a partner job board syndicates onto APEC. 39,123 of the
91,002 live offers, and in a 100-row sample every one was posted as "Hellowork". They are real offers, but the company name is the
board's, not the employer's.

**Can I get the employer's SIREN or contact details?**
No. APEC's search response carries the trading name (`nomCommercial`), the logo and the NAF code —
not the SIREN, not an email, not a phone number. The NAF code plus the name is usually enough to
match against a company database on your side.

**Can I search by an exact job title?**
Use `jobTitles` — it runs your term through APEC's own métier autocomplete first, so "data analyst"
resolves to the canonical métier the board indexes on. Use `keywords` for the raw string.

**How do I search around a city rather than a whole department?**
Set `radiusKm` with `centerLat` / `centerLng`. Verified: 15 km around Lyon returned 120 rows, the
farthest 1.17 km out, all in department 69.

**Can I monitor for new offers?**
Yes — run on a schedule with `postedWithin: "101850"` (last 24 h; 18,680 offers when read at
20:20 UTC on 2026-09-04) or `"101851"` (last 7 days; 44,092), and dedupe on `numeroOffre` on your
side. To re-check specific postings, feed
their numbers into `offerNumbers`.

**Is it fast?**
1,000 rows in 15 requests. Median search latency 2.4–4.9 s, p95 17.6 s under parallel load. Measured
concurrency safety: 5 parallel 5/5, 10 parallel 10/10 on one session, 25 rapid sequential calls
25/25 with no rate limiting observed.

**Do I need residential proxies?**
No. The search API answered 200 from a direct IP, an Apify datacenter IP and a residential French IP
alike. The datacenter default is the tested path.

**Why is the actor only 512 MB when others in this lane run 2 GB and 4 GB?**
Because they run browsers to render HTML against DataDome for the detail route. This one reads JSON.

**What happens on an empty run?**
If APEC answered but nothing matched your filters, the run exits **clean** with an explanation of
which of your filters removed how many rows. If not a single page could be read, the run **fails
loudly** — an empty dataset must never be mistaken for an empty job board.

***

### ⚖️ Legal & fair use

- APEC's `robots.txt` was read live on 2026-09-04: `User-agent: *` with **no `Disallow` lines**, plus
  four published sitemaps. The search route this actor uses is the same one the public site calls.
- Only **public job advertisements** are collected. No login, no paywall, no anti-bot control is
  bypassed, and the DataDome-protected per-offer routes are deliberately never requested.
- Job advertisements can contain personal data (a named contact, a confidential employer). **You are
  the data controller** for anything you collect and store, and GDPR applies. Use it for legitimate
  business purposes, honour deletion requests, and do not re-publish personal data.
- Trade marks and content belong to their owners. This actor is not affiliated with, endorsed by, or
  connected to APEC.

# Actor input Schema

## `keywords` (type: `array`):

Free-text search terms, one crawl per term. Measured corpus sizes on 2026-09-04: "developpeur" 3,295 offers · "commercial" 23,324 · "ingenieur" 21,148. Leave empty to crawl the whole APEC board (~88,000 live offers) newest first.

## `jobTitles` (type: `array`):

Fuzzy job titles. Each one is sent through APEC's own métier autocomplete and expanded into up to 5 canonical métier labels, which are then searched as keywords — so "developpeur" also finds "Business developer" and "Développeur foncier" the way the site's own search box does. Use "Keywords" instead if you want the raw term and nothing else.

## `searchUrls` (type: `array`):

Paste apec.fr search URLs — e.g. https://www.apec.fr/candidat/recherche-emploi.html/emploi?lieux=75\&typesContrat=101888. Their query parameters map 1:1 onto the search API, so the URL is parsed, never fetched. Any filter the URL sets wins over the equivalent field below; the rest of your settings still apply.

## `offerNumbers` (type: `array`):

APEC offer numbers such as `179364344W` (or their bare numeric ids). Each one is looked up exactly — the search API answers with totalCount 1 — so you can feed yesterday's numbers back in to see which postings are still live. Offers that have gone are listed in the run log.

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

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

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

Departments ("Paris", "Rhône", "75", "69"), regions ("Ile-de-France", "Occitanie"), countries ("Allemagne", "Belgique"), "International", or communes ("Bordeaux", "Lyon 01"). Departments, regions and countries resolve instantly from the bundled APEC nomenclature; a commune is resolved through APEC's own place autocomplete. Verified live: 75 → 6,963 Paris offers, 69 → 5,649, Ile-de-France → 21,932, Bordeaux → 1,229, Allemagne → 18.

## `lieuxIds` (type: `array`):

Escape hatch for the same filter: department ids (75), region ids (711 = Ile-de-France), commune ids (590711 = Paris 1er) or country ids (99109 = Allemagne). Merged with the names above.

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

Keep only offers within this many km of the centre below. APEC's own geo parameter does NOT filter at any distance (measured: 87,736 offers at 10 km, 50 km and 300 km alike) — it only orders results by distance. So the crawl sets the point, sorts by distance ascending, and walks outward until it passes your radius. 0 = off.

## `centerLat` (type: `string`):

Latitude of the radius centre, e.g. 45.75 for Lyon. Required for the radius filter.

## `centerLng` (type: `string`):

Longitude of the radius centre, e.g. 4.85 for Lyon. Required for the radius filter.

## `onlyGeolocated` (type: `boolean`):

Keep only offers APEC can place on a map (measured: 84,804 of 87,843). Useful when you are going to plot or distance-sort the results downstream.

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

THE differentiating filter, and no rival APEC actor exposes it. The corpus partitions cleanly: Entreprise 18,308 · Cabinet de recrutement 17,108 · Agence d'emploi 9,670 · SSII/ESN 4,826 · Partenaire (job-board feed resold onto APEC) 37,858. Leave empty to use the "exclude aggregator feeds" switch below.

## `excludeAggregatorFeeds` (type: `boolean`):

43% of APEC is resale from partner job boards. In a blind 300-row sample the top "companies" were Hellowork (145) and Meteojob (58) — i.e. two thirds of rows named a job board, not the employer. On by default so "which company is recruiting" actually means something. Ignored when you pick employer types explicitly.

## `stampEmployerClass` (type: `boolean`):

APEC never returns the employer class on the row, only as a filter. With this on, the crawl runs one query slice per class and stamps each row — costing no extra requests, because the classes partition the corpus (18,308 + 17,108 + 9,670 + 4,826 = 49,912, exactly what the four return together). Turn it off for a single combined query with a blank `employerClass` column.

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

Measured live: CDI 81,393 · CDD 5,113 · Intérim 820 · Alternance 517. The level-2 entries (apprentissage, professionnalisation, mission d'intérim, stage, CDI intérimaire) are narrower slices of the same facet.

## `jobFunctions` (type: `array`):

APEC's own job-family tree: 10 families and their 46 sub-functions. Measured: Commercial/Marketing 19,089 · Administration des ventes et SAV 1,253. Several are OR-ed together.

## `jobFunctionIds` (type: `array`):

The same filter accepts APEC's 502 individual métier ids as well as the families above — e.g. 600174 (Acheteur) returns 377 offers. Merged with the selection above.

## `sectors` (type: `array`):

The hiring company's broad sector. Measured: Conseil et gestion des entreprises 21,325 · Activités informatiques 7,787. Every row also carries the company's precise NAF/APE code and label, decoded from APEC's nomenclature.

## `experienceLevels` (type: `array`):

Measured: Débutant 58,417 · 3-5 ans 58,195 · 6-9 ans 38,493 · 10 ans et + 36,985. These overlap — an offer open to 3-5 and 6-9 years counts in both.

## `positionStatus` (type: `array`):

Measured: Cadre du secteur privé 76,742 · Agent de maîtrise / Technicien 9,274 · Cadre du secteur public / Armées 1,827.

## `remoteWork` (type: `array`):

Measured: télétravail partiel possible 11,861 · ponctuel autorisé 5,855 · total possible 972. Only 27.7% of offers declare a policy at all, so this filter is strict by nature.

## `contractDurationBands` (type: `array`):

For fixed-term work. Measured: 1-6 mois 3,764 · 7-12 mois 1,862 · 13 mois et + 835.

## `internshipDuration` (type: `array`):

For internships (stage). Measured: 3 mois 672 · 1 mois 677 · 4 mois 455.

## `educationLevel` (type: `string`):

A thin facet — APEC only records it on a few thousand offers (Bac+5 returned 53). Expect small result sets, and expect most offers to be excluded rather than matched.

## `travelZones` (type: `array`):

How far the role travels. Measured: Régionale 6,834. Not on the public search UI, but the search API accepts it.

## `startDateWithin` (type: `string`):

When the post opens. Measured: dès que possible 86,715 · < 2 mois 966 · > 2 mois 175.

## `salaryMinK` (type: `integer`):

In thousands of euros, gross annual. APEC ignores a minimum sent on its own — sending only a floor leaves totalCount at the unfiltered 87,775 with byte-identical rows — so if you set just one bound the other is completed for you (0 or 1000). Measured: 50-70 → 38,254 offers, 90-1000 → 6,262. Note this is an INTERSECTION filter — an offer whose own range overlaps the band matches, open-ended "A partir de N k€" offers count as unbounded above, and "A négocier" offers pass every band. Use the strict euro bounds below for a hard floor. 0 = off.

## `salaryMaxK` (type: `integer`):

In thousands of euros, gross annual. The site's own bands are 0-25, 25-35, 35-50, 50-70, 70-91 and 90-1000. 0 = off.

## `minSalaryEur` (type: `integer`):

A hard floor applied on our side to the salary APEC actually printed, normalised to gross annual euros (a monthly figure is multiplied by 12). Use this when the band above is not strict enough: the APEC band is an INTERSECTION filter, so "A partir de 70 k€" — which has no upper bound — matches the 90-1000 k€ band, and "A négocier" offers match every band. Setting this also drops offers with no published figure. 0 = off.

## `maxSalaryEur` (type: `integer`):

A hard ceiling on the printed salary, in gross annual euros, applied on our side. Also drops offers with no published figure. 0 = off.

## `onlyWithNumericSalary` (type: `boolean`):

Drops "A négocier", "Minimum légal" and "Non renseignée". Measured on 1,418 rows: 53% carry a numeric figure (41% a range, 12% a floor), 33% say "A négocier", 9% "Minimum légal", 3% "Non renseignée", 2% a monthly figure. Runs on our side; dropped rows are never billed.

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

APEC's own date facet. Read live on 2026-09-04 at 20:20 UTC: last 24h 18,680 · last 7 days 44,092 · last 30 days = the whole board (APEC expires offers at 30 days). Both counts move through the day.

## `postedAfter` (type: `string`):

A tighter window than the four buckets above, applied on our side to the publication date. Rows outside it are never billed.

## `postedBefore` (type: `string`):

Upper bound on the publication date, applied on our side.

## `companyNameContains` (type: `array`):

Keep only offers whose advertised company name contains one of these (case-insensitive).

## `excludeCompanies` (type: `array`):

Drop offers from these company names. Handy for muting a bulk poster the aggregator filter does not catch — e.g. a single ESN posting hundreds of near-identical roles.

## `includeConfidential` (type: `boolean`):

APEC hides the employer on 497 offers corpus-wide; they post as "ZZ\_Confidentiel" and every row carries `isConfidential`. Switch off to drop them (measured: 87,843 → 87,239).

## `onlyLowApplicantCount` (type: `boolean`):

APEC flags postings that have attracted few applications. Rare — 2 of 300 rows in a blind sample — but it is a genuine recruiter signal, and no other APEC actor surfaces it at all.

## `postingOrigins` (type: `array`):

How the offer reached APEC. "Agrégation" (20382) is the aggregated-feed marker and is orthogonal to the employer type, so it catches resale the type filter misses. Applied on our side; leave empty for all.

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

All five sorts are live. A note worth knowing before a deep crawl: paging by DATE while new offers arrive at the head shifts the window — measured 4.88% duplicate rows over 800 rows, versus 0.88% paging by ID. Duplicates are removed before billing either way, but ID is the steadier deep crawl. GEOLOC is chosen automatically when you set a radius.

## `sortOrder` (type: `string`):

DESCENDING with "Publication date" gives newest first.

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

Hard stop across every search, applied BEFORE rows are pushed and billed. 0 = no limit — the whole board is ~88,000 offers, so set this deliberately.

## `maxItemsPerSearch` (type: `integer`):

Per keyword / per pasted URL, so one broad term cannot eat the whole budget. 0 = no per-search limit.

## `pageSize` (type: `integer`):

APEC caps this at 100 and does it silently: ask for 101 or 150 and it returns HTTP 200 with only 20 rows, quietly costing 5x the requests. Values above 100 are clamped here.

## `startIndex` (type: `integer`):

Resume a crawl part-way in. APEC has no offset ceiling — verified to the tail of the corpus (offset 87,700 returned the last 28 rows).

## `dedupeAcrossSearches` (type: `boolean`):

One offer, one row, however many of your keywords match it. Switch off to get one row per matching search instead (each still deduped within its own search). Duplicates are dropped before billing in both modes.

## `includeRawCodes` (type: `boolean`):

Adds the numeric nomenclature ids next to the decoded labels (contract, remote, employer class, NAF, sector, posting origin) so you can feed them straight back into a later run.

## `includeTitleHighlight` (type: `boolean`):

Adds `titleHighlighted`, APEC's title with `<span class="highlightResult">` markup around your keyword. Off by default — most buyers want the clean title.

## `compact` (type: `boolean`):

Ship 13 core columns instead of ~40: offer number, title, company, employer class, city, department, contract, salary, remote, published, URL.

## `outputFields` (type: `array`):

Exact column selection, overriding "Compact". `numeroOffre` is always kept so rows stay joinable on your side.

## `refreshLabels` (type: `boolean`):

The 3,502-entry label nomenclature (sectors, NAF codes, métiers, contract types, places) is bundled with the actor, so labels never depend on a live call. Switch this on to re-fetch it from APEC at the start of the run — one extra request — if you need same-day nomenclature.

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

The Apify datacenter pool is the measured-correct default: APEC's search API answered 200 from a direct IP, a datacenter IP and a residential FR IP alike, and 17 of 18 varied calls on one pinned session came back clean. No browser and no residential proxy are needed.

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

Query slices run in parallel. Measured safe: 5 parallel 5/5, 10 parallel 10/10 on one session, and 25 rapid sequential calls with no delay 25/25 — no rate limiting observed.

## `requestDelayMs` (type: `integer`):

Optional politeness pause after every request. 0 in testing caused no throttling.

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

Retries rotate the proxy session. A HTTP 500 is never retried — it means the search body itself was rejected, and no amount of retrying fixes that.

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

Per request. Median search latency measured at 2.4-4.9 s, p95 17.6 s under parallel load.

## Actor input object example

```json
{
  "keywords": [],
  "jobTitles": [],
  "searchUrls": [],
  "offerNumbers": [],
  "excludeKeywords": [],
  "location": [],
  "lieuxIds": [],
  "radiusKm": 0,
  "centerLat": "",
  "centerLng": "",
  "onlyGeolocated": false,
  "employerType": [],
  "excludeAggregatorFeeds": true,
  "stampEmployerClass": true,
  "contractTypes": [],
  "jobFunctions": [],
  "jobFunctionIds": [],
  "sectors": [],
  "experienceLevels": [],
  "positionStatus": [],
  "remoteWork": [],
  "contractDurationBands": [],
  "internshipDuration": [],
  "educationLevel": "",
  "travelZones": [],
  "startDateWithin": "",
  "salaryMinK": 0,
  "salaryMaxK": 0,
  "minSalaryEur": 0,
  "maxSalaryEur": 0,
  "onlyWithNumericSalary": false,
  "postedWithin": "",
  "postedAfter": "",
  "postedBefore": "",
  "companyNameContains": [],
  "excludeCompanies": [],
  "includeConfidential": true,
  "onlyLowApplicantCount": false,
  "postingOrigins": [],
  "sortBy": "DATE",
  "sortOrder": "DESCENDING",
  "maxItems": 50,
  "maxItemsPerSearch": 0,
  "pageSize": 100,
  "startIndex": 0,
  "dedupeAcrossSearches": true,
  "includeRawCodes": true,
  "includeTitleHighlight": false,
  "compact": false,
  "outputFields": [],
  "refreshLabels": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 5,
  "requestDelayMs": 0,
  "maxRetries": 3,
  "requestTimeoutSecs": 45
}
```

# Actor output Schema

## `jobs` (type: `string`):

The dataset of scraped APEC offers (one item per offer).

# 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 = {
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/apec-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 = { "maxItems": 50 }

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/apec-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 '{
  "maxItems": 50
}' |
apify call scrapersdelight/apec-jobs-scraper --silent --output-dataset

```

## MCP server setup

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