# PagesJaunes Scraper: French Business Directory Leads (`hexagone-data/pagesjaunes-scraper`) Actor

Scrape pagesjaunes.fr searches (activity + place): business name, address, postal code, city, activity, rating, every phone number with its do-not-call (opposition) flag; optional SIRET, NAF code and websites. Pay per result.

- **URL**: https://apify.com/hexagone-data/pagesjaunes-scraper.md
- **Developed by:** [Hexagone Data](https://apify.com/hexagone-data) (community)
- **Categories:** Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 businesses

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PagesJaunes Scraper — French Business Directory (phones, ratings, SIRET)

Collect businesses from **PagesJaunes.fr**, the French business directory: paste a search such as *plumbers in Paris* and get one record per business with its name, address, activity, rating, **phone numbers with PagesJaunes' direct-marketing opt-out flag**, and optionally its **SIRET**, **NAF code** and **websites**.

Every search is checked against the number of results PagesJaunes announces ("465 résultats"): the run summary tells you whether you got all of them, and never reports a search as complete when businesses are missing.

### Try it with 20

1. Open a search on pagesjaunes.fr (for example *plombiers* in *Paris*) and copy the address from your browser.
2. Paste it into **Search URLs**. Leave **Maximum number of results** at `20`.
3. Start the Actor, then open **Run summary** before exporting the dataset.

```json
{
  "startUrls": [{"url": "https://www.pagesjaunes.fr/annuaire/paris-75/plombiers"}],
  "maxItems": 20
}
```

The form prefill is 20; the default for API calls is 500. Set `maxItems` explicitly in API calls; `0` removes the limit.

### What to paste

| Input | Accepted | Notes |
|---|---|---|
| `startUrls` | `https://www.pagesjaunes.fr/annuaire/<place>/<activity>` | e.g. `/annuaire/paris-75/plombiers`, `/annuaire/lyon-69/boulangeries-patisseries-artisans`. Copy them from pagesjaunes.fr: the Actor never guesses a place code. A search URL of the form `/annuaire/chercherlespros?quoiqui=plombiers&ou=paris-75…` (what the site's sort and page links produce) is turned into `/annuaire/paris-75/plombiers` when `ou` is already a PagesJaunes place code; otherwise (e.g. `ou=Paris`) it is rejected with a reason. Any other URL (profile page `/pros/…`, page parameters on an `/annuaire/…` URL, other sites) is rejected with a reason, and the other URLs still run. A URL pasted twice runs once. |
| `includeDetails` | `true` / `false` (default `false`) | Opens each delivered business's profile page: adds `siret`, `nafCode`, `websites`. One more request per business, so the run is slower; the price per result does not change. |
| `maxItems` | integer, `0` = no limit | Counted across all searches. Exact: the run stops at this number, even in the middle of a results page. |
| `proxyConfiguration` | default: Apify residential proxies, France | PagesJaunes refused Apify datacenter proxies in our tests (HTTP 403, 3 of 3); residential France was accepted (2 of 2). |

The Actor follows the result pages itself (20 businesses per page) and waits 2–3 seconds between requests.

### Output

One record per business **and per search**: a business found by two of your searches appears once in each (each line is charged).

| Field | Meaning |
|---|---|
| `code`, `url` | PagesJaunes establishment number (text) and its profile page. |
| `name`, `address`, `postalCode`, `city` | As shown in the results list. `postalCode` is the **last** 5-digit group of the address that is followed by text, `city` the text after it (so a postal box such as `Cs 58011 … 17180 Périgny` gives `17180` / `Périgny`). |
| `activity` | As shown, e.g. `plombiers +1` (main activity + number of other activities). |
| `rating`, `reviewsCount` | Overall rating out of 5 and number of reviews. `rating` is null when the list shows no rating (never 0). The list sometimes shows a number of reviews without a rating ("19 avis"): then `reviewsCount` = 19 and `rating` = null. Both are null when the list shows neither. |
| `phones` | List of `{number, optedOutOfDirectMarketing}` in the site's order (some businesses have 2 or 3 numbers). `[]` = PagesJaunes shows no number; `null` = the phone lookup failed (unknown, counted in the run summary). A number not in the usual French format is kept **as displayed** (with a warning) rather than left out. `optedOutOfDirectMarketing`: `true` = PagesJaunes marks this number as opposed to direct marketing; `null` = **unknown** (an unrecognised element or pictogram stands next to the number — treat it as opposed: do not canvass it); `false` = no opt-out mark shown, which does **not** mean canvassing is allowed (see Personal data). |
| `siret`, `nafCode`, `websites` | With `includeDetails` only (otherwise null). `websites` = external links of the "Sites et réseaux" block, in order (internal PagesJaunes links removed). |
| `detailsSource` | `profilePage` when the profile page was read, else `listing`. |
| `search`, `position`, `page` | Which search, which results page, rank on that page. PagesJaunes reorders its results from one load to the next (in our measure, 131 of 399 positions differed 10–40 minutes later): `position` and `page` describe this run only, not a stable ranking. |
| `scrapedAt`, `detailsScrapedAt` | Collection time (UTC). |

#### Real output example

Page data captured on **28 October 2026** (search `paris-75/plombiers`, with `includeDetails`). It shows the format, not today's data.

```json
{
  "code": "06549084",
  "url": "https://www.pagesjaunes.fr/pros/06549084",
  "name": "Entreprise Bruni Sanitaire",
  "address": "125 rue Picpus 75012 Paris",
  "postalCode": "75012",
  "city": "Paris",
  "activity": "plombiers +1",
  "rating": 5.0,
  "reviewsCount": 23,
  "phones": [{"number": "01 43 43 87 11", "optedOutOfDirectMarketing": false}],
  "siret": "80478483300019",
  "nafCode": "4322A",
  "websites": ["http://www.brunisanitaire.fr"],
  "detailsSource": "profilePage",
  "search": {"what": "plombiers", "where": "paris-75"},
  "position": 18,
  "page": 2,
  "scrapedAt": "2026-10-28T15:31:38.983Z",
  "detailsScrapedAt": "2026-10-28T15:31:38.983Z"
}
```

### Personal data

PagesJaunes lists professionals, many of whom are sole traders: their name, address and phone number are **personal data** under the GDPR (RGPD). You are responsible for how you use the data: you need a lawful basis, you must inform the people concerned and honour their rights (access, objection, erasure), and you must keep only what you need.

**Phone numbers flagged `optedOutOfDirectMarketing: true` must not be used for telephone prospecting.** PagesJaunes shows a pictogram on these numbers meaning the holder objects to direct marketing operations; French rules on telephone canvassing (opposition au démarchage téléphonique) do not allow calling them for prospecting. The flag is per number: a business can have one opted-out number and one that is not. In our 30-business sample (28 October 2026), 4 of 37 numbers carried it. A `null` flag means the Actor could not read the mark with certainty: do not canvass that number either.

**`false` does not mean "canvassing allowed".** It only means PagesJaunes shows no opt-out mark on that number. Other rules still apply (for example the Bloctel opposition list for consumers, and consent rules for SMS and e-mail); checking them is your responsibility. This Actor offers no "ready-to-call" export and does not guarantee that any use is lawful.

### Run summary

Open **Run summary** (key-value store record `SUMMARY`; the same data as JSON in `OUTPUT`). For each search it shows:

- the status: `done` (businesses delivered = number announced by PagesJaunes), `partial_collection` (some are missing — the summary says how many and why), `stopped_at_limit` (`maxItems` reached, or the run was about to hit its timeout — see below), `stopped_at_charge_limit` (your maximum charge per run), `not_processed`, `no_results` (PagesJaunes says there is no result for this search), `blocked_by_site`, `not_a_listing` (PagesJaunes answered with something that is not a results list — this is **not** an empty search), `not_found`, `rate_limited`, `http_error`, `network_error`;
- delivered / announced, pages read;
- failure counters: phone lookups failed, profile lookups failed, businesses skipped because their listing was malformed;
- **Checks**: warnings such as a page without results in the middle of a search, a repeated page, or a coverage gap.

The final status message lists every non-zero failure counter; it never says a run went fine when something failed. The run fails only when no search delivered anything ("No business found, check the search URLs").

**Run timeout.** When less than about 2 minutes remain before the run's timeout, the Actor stops cleanly: everything already collected stays in the dataset, the search in progress gets `stopped_at_limit` ("run time limit"), the next ones `not_processed`, and the Run summary is written. Raise the timeout and start again for the rest.

`OUTPUT.bytesReceived` counts the bytes of the responses received (decompressed bodies): an approximation of the proxy traffic, which is usually lower (compressed) plus headers.

### Pricing

Pay per result: **$1.50 per 1,000 records** (one event per business delivered; nothing is charged for rejected URLs or skipped businesses), plus a **$0.005 start fee per run** (once per run up to 1 GB of memory; the default is 256 MB). Apify subscribers pay less automatically, on both events: 10% off on the Bronze plan, 20% on Silver, 30% on Gold and above.

| Records | Price |
|---|---|
| 20 | $0.035 |
| 1,000 | $1.505 |

`includeDetails` does not change the price, but makes the run longer (one more request per business, 2–3 s apart). Measured on the platform on 28 October 2026: 402 records with details in about 40 minutes, i.e. roughly 1 h 40 min per 1,000. Raise the run timeout for large runs. The Actor needs little memory (default 256 MB: no browser).

### Known limits

- One phone request per delivered business (PagesJaunes loads phone numbers on click), plus one profile request with `includeDetails`: large runs are slow by design (2–3 s between requests).
- The search URL must be copied from pagesjaunes.fr: the Actor does not build URLs from free text, to avoid guessing a place.
- Coverage is compared with the number PagesJaunes announces; if PagesJaunes stops serving pages before that number, the search is reported as `partial_collection`.
- `rating` comes from the results list. A profile page can show a rating the list does not (pilot sample: rating on 20 of 30 profile pages, 14 of 30 in the list); the record follows the list.
- `websites` keeps external links only; links to other PagesJaunes pages are removed.
- Opening hours, photos, descriptions and reviews text are not collected.
- Behaviour after a platform migration: the Actor resumes without duplicates by reading the search again from page 1 and skipping businesses already delivered.

### Support

Open an issue on the Actor's **Issues** tab with the run ID and the search URL.

This Actor is an independent tool. It is **not affiliated with, endorsed by or sponsored by PagesJaunes or Solocal**; "PagesJaunes" is a trademark of its owner, used only to describe the data source.

### En français

**PagesJaunes Scraper** récupère les professionnels d'une recherche PagesJaunes (« plombiers à Paris ») : nom, adresse, activité, note, **numéros de téléphone avec le pictogramme d'opposition au démarchage**, et en option **SIRET**, **code NAF** et **sites web** (`includeDetails`).

- **À coller** : l'adresse d'une recherche copiée depuis pagesjaunes.fr, de la forme `https://www.pagesjaunes.fr/annuaire/paris-75/plombiers`. Les autres adresses sont refusées avec une raison, sans bloquer les autres.
- **Essai** : `maxItems` = 20 (préréglé dans le formulaire). Lisez ensuite le **Run summary** : il compare le nombre d'établissements livrés au nombre annoncé par PagesJaunes (« 465 résultats ») et ne dit jamais « terminé » s'il en manque.
- **Téléphones** : `phones` est une liste dans l'ordre du site ; `[]` = aucun numéro affiché ; `null` = la requête a échoué (inconnu, compté dans le résumé).
- **Opposition au démarchage** : un numéro marqué `optedOutOfDirectMarketing: true` porte le pictogramme PagesJaunes d'opposition aux opérations de marketing direct. **Ces numéros ne doivent pas être démarchés par téléphone** (opposition au démarchage téléphonique, droit français). `null` = marque illisible avec certitude : ne pas démarcher non plus. `false` ne veut **pas** dire « démarchage autorisé » : d'autres règles s'appliquent (Bloctel pour les particuliers, consentement pour SMS et e-mail). Vous êtes responsable de l'usage des données (RGPD) : base légale, information des personnes, respect de leurs droits.
- **Prix** : 1,50 $ pour 1 000 résultats, détails compris, plus 0,005 $ de démarrage par run (0,035 $ pour 20). Remise automatique pour les abonnés Apify : −10 % (Bronze), −20 % (Silver), −30 % (Gold et plus).
- Outil indépendant, **sans lien avec PagesJaunes ni Solocal**.

# Actor input Schema

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

PagesJaunes search URLs of the form https://www.pagesjaunes.fr/annuaire/<place>/<activity>, for example https://www.pagesjaunes.fr/annuaire/paris-75/plombiers. Open the search on pagesjaunes.fr and copy the address. Any other URL is rejected with a reason; the other URLs still run.

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

Opens each delivered business's profile page (one more request per business): adds siret, nafCode and websites. Slower; the price per result does not change.

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

Stops after this many results across all searches (one result = one business in one search). 0 = no limit.

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

Default: Apify residential proxies, country France. PagesJaunes refused datacenter proxies in our tests (HTTP 403, 3 of 3). Change only if you know what you are doing.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.pagesjaunes.fr/annuaire/paris-75/plombiers"
    }
  ],
  "includeDetails": false,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "FR"
  }
}
```

# Actor output Schema

## `businesses` (type: `string`):

All records, every field.

## `overview` (type: `string`):

Name, address, activity, rating, phones (with the opt-out flag), link.

## `company` (type: `string`):

SIRET, NAF code and websites (only filled with the includeDetails option).

## `summary` (type: `string`):

Readable per-search summary: status, businesses delivered versus the number announced by PagesJaunes, failure counters and warnings.

## `summaryJson` (type: `string`):

The same summary as JSON (OUTPUT record).

# 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 = {
    "startUrls": [
        {
            "url": "https://www.pagesjaunes.fr/annuaire/paris-75/plombiers"
        }
    ],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "FR"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("hexagone-data/pagesjaunes-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 = {
    "startUrls": [{ "url": "https://www.pagesjaunes.fr/annuaire/paris-75/plombiers" }],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "FR",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("hexagone-data/pagesjaunes-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 '{
  "startUrls": [
    {
      "url": "https://www.pagesjaunes.fr/annuaire/paris-75/plombiers"
    }
  ],
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "FR"
  }
}' |
apify call hexagone-data/pagesjaunes-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hexagone-data/pagesjaunes-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/XhgbcTBrOngOA7eMF/builds/oQCl2zpqk5P5bjWy6/openapi.json
