# Doctolib Practitioners Scraper (FR, DE, IT) (`nice_dev/doctolib-practitioners-scraper`) Actor

Scrape healthcare professionals from Doctolib France, Germany and Italy by speciality and city, or the whole directory: name, speciality, address with GPS, phone, RPPS, sector, payment means, languages, fees, opening hours, visit motives, photo. Public professional data only.

- **URL**: https://apify.com/nice_dev/doctolib-practitioners-scraper.md
- **Developed by:** [Nice Dev](https://apify.com/nice_dev) (community)
- **Categories:** Lead generation, Business, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.59 / 1,000 practitioner profiles

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

### 🔎 What is Doctolib Practitioners Scraper?

**Doctolib Practitioners Scraper** extracts the **public directory of healthcare professionals on [Doctolib](https://www.doctolib.fr)** — **France, Germany and Italy** — : **name, specialities, practice address with GPS coordinates and Google Maps link, phone number, RPPS / ADELI legal ids, sector (conventionnement), Carte Vitale, payment means, languages spoken, fees, opening hours, skills, diplomas, presentation, photo, every bookable visit motive, and the next available appointment with the open slots of the week**, for individual practitioners and establishments (clinics, health centres, laboratories).

Pick one or **several specialities** (`dentiste`, `Médecin généraliste`, `zahnmedizin`, `odontoiatra`…) and one or **several cities** (`Paris`, `Lyon`, `paris-75009`, `Berlin`, `Milano`, or the whole country), or paste Doctolib search / profile URLs, click **Start**, and download the results in JSON, CSV or Excel. No login: **about 100 full profiles a minute**, at a low price.

> **Scope:** what Doctolib shows to any anonymous visitor — the professional profile and, on request, the open appointment slots of its booking calendar. Never patient data, and nothing behind a login.

### 📋 What data can you extract from Doctolib?

One item per Doctolib profile (a practitioner or an establishment, deduplicated), 78 fields, listed one by one under **Output**:

| Category | What you get |
| --- | --- |
| 👤 **Who** | name, title, gender, practitioner or establishment (clinic, health centre, laboratory), team of an establishment |
| 🩺 **Speciality** | main speciality and every other one, skills and procedures, diplomas, career, presentation |
| 📍 **Where** | practice address, postcode, city, department, GPS, Google Maps link, distance from the searched city, every practice location |
| 📞 **Contact** | practice phone (also in international format), website, practice name |
| 🪪 **Legal ids** | RPPS, ADELI, SIRET (France, when published) |
| 💶 **Fees and payment** | sector (conventionnement) and its label, Carte Vitale, third-party payer, insurance sector (Germany, Italy), payment means, fees per act |
| 🗣️ **Practice** | languages spoken, opening hours, home visits, telehealth, accessibility, floor, lift, public transport, parking, photos |
| 📅 **Booking** | online booking or directory only, accepting new patients, visit motives and agendas, next available appointment, days to wait, open slots of the week |
| 🔎 **Search** | the search that found the profile, its results page, scrape date |

Detail fields (`phone`, `rpps`, `bio`, `fees`, `openingHours`, `skills`, `specialities`, `places`, `practitioners`, accessibility) are filled when **Extract details** is on (default, one request per profile). Turn it off for a search-card-only scrape that is much faster (one request per 20 profiles): name, speciality, address, GPS, sector, payment means, languages, photo. `visitMotives`, `visitMotiveNames` and `agendas` need **Include every visit motive** (one more request per bookable profile, same price). The slot fields need **Next available appointment** (one more request per bookable profile, two without a search card; `appointment-slots` per profile saved with an open slot) and stay `null` without it.

### ✅ Why use Doctolib Practitioners Scraper?

- 🚀 **Fast**: about 100 profiles with full details in a minute at the default rate, a few thousand per hour.
- 🌍 **France, Germany and Italy** in the same Actor: pick the **Country**, or paste URLs of the three sites together.
- 🗂️ **Several searches in one run**: specialities × cities, with a cap per search so that one big city cannot eat the whole budget.
- 🔔 **Monitoring built in**: tick **Only new profiles**, schedule the Actor, and each run returns (and charges) only the practitioners it has never delivered before — new doctors in your area, week after week.
- 📚 **Whole directory**: the **sitemaps** source walks Doctolib's public sitemaps (≈ 615 000 French profiles), in several runs if you want.
- 🧾 **Complete profiles**: RPPS, GPS, fees, opening hours, languages, payment means, all practice locations, accessibility and public transport, establishment teams, every visit motive — fields other scrapers leave empty because they read the HTML.
- 📅 **Next available appointment**: the first open slot (even beyond the week, when Doctolib names it), the days to wait and the open slots of the week — waiting times by speciality and city, or only the practitioners with a slot within N days.
- 🎯 **Filters**: sector (secteur 1, secteur 2, OPTAM, non conventionné, establishments…), **insurance sector in Germany** (public / private), languages spoken, **with a phone number only**, **accepting new patients only**, **available within N days**, excluded words, and a **suppression list** of profiles never to deliver (by id, slug or URL). Sector, insurance and language filters are applied by Doctolib's own search engine (also on the search URLs you paste: they add to the filters written in the URL, and the URL's own value wins), so you only pay for the profiles you asked for; on a pasted profile URL, sector and language are checked by the Actor instead (insurance sector cannot be judged on a profile and is ignored); the other filters are applied by the Actor on every profile it reads, and each profile they check is charged a filter fee, kept or not.
- 🔌 API, scheduling, integrations (Make, Zapier, n8n, Google Sheets…) and JSON/CSV/Excel export via the Apify platform.

### 🚀 How to scrape Doctolib

1. Create a free Apify account.
2. Open **Doctolib Practitioners Scraper**, choose the **Country**, type a **Speciality** (e.g. `dentiste`) and a **Location** (e.g. `Paris`) — add more in **More specialities** / **More locations**.
3. Or paste your own URLs into **Start URLs**: Doctolib results pages (filters set on the site are kept) or single profile pages, from doctolib.fr, doctolib.de or doctolib.it.
4. Set **Max practitioners** (100 by default, 0 = no limit) — and **Max profiles per search** when you run several searches — then click **Start**.
5. Download the dataset in JSON, CSV, Excel or via API. The **Contact sheet** view gives one clean line per practitioner with legal id, practice phone and address.

### 💰 How much does it cost to scrape Doctolib?

This Actor uses **pay per event** pricing: you pay for each profile, and for the appointment slots or a filter only when you ask for them.

| Apify plan | Profile (details and visit motives included) | Next available appointment | Card checked by a filter | Profile read for a filter |
| --- | --- | --- | --- | --- |
| Free | **$0.62** | **$0.99** | **$0.058** | **$0.32** |
| Bronze | $0.61 | $0.98 | $0.048 | $0.31 |
| Silver | $0.60 | $0.97 | $0.038 | $0.30 |
| Gold | $0.59 | $0.96 | $0.028 | $0.29 |

Prices per 1,000, plus **$0.01 per run start** ($1 per 100 runs). Platform usage (compute, proxy) is included in the price. Example: all dentists in Paris (about 8 100 profiles) ≈ $5; a weekly run that returns the 50 new GPs of a city ≈ $0.04.

- Profiles already delivered (**Only new profiles**) are not charged.
- **Next available appointment** (event `appointment-slots`) is charged per profile saved with an open slot (a next slot, or a slot in the week); a profile without any slot costs nothing more. *Available within (days)* switches it on: the profiles it keeps come with their slots and are charged the same way.
- The filters the Actor applies itself are charged on every profile they check, kept or not: event `filter-check` per search card checked by *Exclude keywords*, *Accepting new patients only* or the suppression list; event `detail-filter-check` per profile read for *With a phone number only*, *Available within (days)* (and the sector / language filters of the sitemaps source or of a pasted profile URL), or for any filter on a profile without a search card (sitemaps source, pasted profile URL).

### ⚙️ Input

```json
{
    "speciality": "Ophtalmologue",
    "location": "Lyon",
    "regulationSector": ["CONTRACTED_1", "CONTRACTED_2_WITH_OPTAM"],
    "languages": ["gb"],
    "maxItems": 200,
    "extractDetails": true
}
```

Several specialities and cities, a cap per search, phone numbers only, every visit motive, only the practitioners not delivered before:

```json
{
    "speciality": "dentiste",
    "searchQueries": ["medecin-generaliste", "pediatre"],
    "location": "Paris",
    "locations": ["Lyon", "Marseille"],
    "maxItemsPerQuery": 50,
    "requirePhone": true,
    "includeVisitMotives": true,
    "onlyNew": true,
    "stateKey": "big-cities-monthly"
}
```

General practitioners of Bordeaux with an appointment slot within 3 days, with the next slot and the open slots of the week:

```json
{
    "speciality": "medecin-generaliste",
    "location": "Bordeaux",
    "availabilitiesBefore": 3,
    "maxItems": 50
}
```

Germany, through the public sitemaps, every speciality of a city:

```json
{
    "country": "de",
    "source": "sitemaps",
    "speciality": "",
    "location": "Berlin",
    "maxItems": 500
}
```

Or with your own URLs (any of the three countries):

```json
{
    "startUrls": [
        { "url": "https://www.doctolib.fr/dentiste/paris-75009" },
        { "url": "https://www.doctolib.fr/dentiste/paris/vincent-bourasset?pid=practice-320361" },
        { "url": "https://www.doctolib.it/medico-di-medicina-generale/milano/barbara-riva" }
    ],
    "maxItems": 0
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `startUrls` | — | Doctolib search or profile pages (fr / de / it); when set, the search fields below are ignored; the sector, language and insurance filters still apply (added to those written in a search URL) |
| `country` | `fr` | `fr`, `de` or `it` |
| `speciality`, `searchQueries` | `dentiste` (France) | Slug or name of a Doctolib speciality / skill; several = one search each |
| `location`, `locations` | `Paris` (France), whole country elsewhere | City, district (`paris-75009`, `Lyon 3`), department or region (search source), or empty for the whole country. With Germany or Italy, leaving the form's `Paris` untouched also means the whole country |
| `maxItems` | `100` | Whole run; 0 = no limit |
| `maxItemsPerQuery` | `0` | Cap for EACH search; 0 = none |
| `source` | `search` | `search` (Doctolib's search engine) or `sitemaps` (public sitemaps) — see the FAQ |
| `extractDetails` | `true` | Open each profile for phone, RPPS, fees, hours… (always on with `sitemaps`) |
| `includeVisitMotives` | `false` | One more request per bookable profile: `visitMotives`, `visitMotiveNames`, `agendas`, all `specialities` |
| `scrapeAvailability` | `false` | Next available appointment: `nextAvailableSlot`, `nextAvailableDate`, `waitTimeDays`, `totalAvailableSlots` (7 days), `availabilityVisitMotive`; one more request per bookable profile, two without a search card; event `appointment-slots` per profile saved with an open slot |
| `availabilitiesBefore` | `0` | Keep only the profiles with an open slot within N days (0 = off); switches `scrapeAvailability` on (its slots and their `appointment-slots` event come with every profile kept) |
| `includeOrganizations` | `true` | Keep establishments (clinics, centres, labs) |
| `regulationSector`, `languages` | `[]` | France sectors, the exact code Doctolib filters on: doctors in `CONTRACTED_1` (secteur 1), `CONTRACTED_2`, `CONTRACTED_2_WITH_OPTAM`, `CONTRACTED_1_WITH_EXTRA`, `NON_CONTRACTED`; `CONTRACTED` (no sector) is for dentists and physiotherapists — it keeps 2 general practitioners of Paris out of about 4 500 (2026-09-18); `ORGANIZATION_*` for establishments. Doctolib language codes (`fr`, `gb`, `es`, `de`…) |
| `insuranceSector` | `any` | Germany: `public` (gesetzlich) or `private` (privat) — Doctolib France and Italy return every practitioner whatever you pick |
| `exactMatchOnly` | `false` | Stop before Doctolib's "broad match" results (search source) |
| `requirePhone`, `excludeKeywords` | `false`, `[]` | Phone number required; drop names / specialities / establishment types containing a word |
| `onlyAcceptingNewPatients` | `false` | Only practitioners whose visit motive in the search results accepts new patients (search source) |
| `suppressedDoctorIds`, `suppressedDoctorSlugs` | `[]` | Profiles never to deliver (e.g. a practitioner who opted out): Doctolib ids, or slugs / profile URLs |
| `onlyNew`, `stateKey`, `resetState` | `false`, `default`, `false` | Only the profiles no previous run with the same memory key delivered |
| `proxyConfiguration` | Apify Proxy | Keep the default: it is included in the price (the residential Apify proxy is not available in this Actor) |
| `maxConcurrency`, `maxRequestsPerMinute`, `maxRequestRetries`, `debugLog` | `8`, `120`, `4`, `false` | Advanced |

### 📦 Output

```json
{
    "id": 1537577,
    "url": "https://www.doctolib.fr/dentiste/paris/vincent-bourasset?pid=practice-320361",
    "slug": "vincent-bourasset",
    "practiceId": 320361,
    "type": "practitioner",
    "organizationType": null,
    "title": null,
    "firstName": "Vincent",
    "lastName": "BOURASSET",
    "fullName": "Vincent BOURASSET",
    "gender": null,
    "speciality": "Chirurgien-dentiste",
    "specialitySlug": "dentiste",
    "specialities": [{ "id": 1, "name": "Chirurgien-dentiste", "slug": "dentiste", "kind": "medical" }],
    "address": "26 Rue Marbeuf",
    "zipcode": "75008",
    "city": "Paris",
    "country": "fr",
    "fullAddress": "26 Rue Marbeuf, 75008 Paris",
    "department": "75",
    "distanceMeters": 3659,
    "latitude": 48.8688803,
    "longitude": 2.3044131,
    "googleMapsUrl": "https://www.google.com/maps/search/?api=1&query=48.8688803,2.3044131",
    "phone": "01 43 43 48 87",
    "phoneE164": "+33143434887",
    "website": null,
    "practiceName": "Cabinet 26 Rue Marbeuf, 75008 Paris",
    "rpps": "10100726651",
    "adeli": null,
    "siret": null,
    "regulationSector": "contracted",
    "regulationSectorLabel": "Conventionné",
    "acceptsInsuranceCard": true,
    "thirdPartyPayer": "national",
    "homeVisit": false,
    "paymentMeans": ["check", "cash", "credit_card", "bank_transfer"],
    "languages": ["gb", "fr", "it"],
    "hasOnlineBooking": true,
    "isDirectoryOnly": false,
    "telehealth": false,
    "allowsNewPatients": true,
    "visitMotive": "Première consultation dentaire nouveau patient",
    "visitMotiveFee": null,
    "minimumFee": null,
    "visitMotives": [
        { "id": 11858391, "name": "Première consultation dentaire", "telehealth": false, "specialityId": 1, "practiceIds": [320361] },
        { "id": 16080775, "name": "Consultation d'implantologie", "telehealth": false, "specialityId": 1, "practiceIds": [807766] }
    ],
    "visitMotiveNames": ["Première consultation dentaire", "Contrôle annuel et/ou détartrage", "Urgence dentaire", "Consultation d'implantologie"],
    "agendas": [
        { "id": 2563820, "practiceId": 320361, "specialityId": 1, "practitionerId": 81680262, "bookingDisabled": false, "phone": "01 43 43 48 87", "visitMotiveCount": 6 },
        { "id": 2578019, "practiceId": 807766, "specialityId": 1, "practitionerId": 81680262, "bookingDisabled": false, "phone": "09 86 24 76 46", "visitMotiveCount": 7 }
    ],
    "agendaIds": [2563820, 2578019],
    "availabilityVisitMotive": "Première consultation dentaire nouveau patient",
    "nextAvailableDate": "2026-09-24",
    "nextAvailableSlot": "2026-09-24T09:00:00.000+02:00",
    "waitTimeDays": 7,
    "totalAvailableSlots": 7,
    "fees": [
        { "name": "Contrôle et détartrage", "minPrice": 140, "maxPrice": null, "isFixed": true },
        { "name": "Consultation", "minPrice": 50, "maxPrice": 140, "isFixed": false }
    ],
    "bio": "Le Dr Bourasset offre à ses patients un panel complet de soins dentaires : esthétique, soins, prothèse, chirurgie, endodontie, pédodontie, parodontie.",
    "skills": [],
    "formations": [{ "title": "Contention Fibrée", "establishment": "Eumeria", "year": 2025 }],
    "experience": [],
    "associations": [],
    "openingHours": ["Mon 09:00-13:00, 14:00-17:00", "Tue 08:30-13:00, 14:00-18:30", "Wed 08:30-13:00, 14:00-18:30", "Thu 08:30-13:00, 14:00-18:30", "Fri 09:00-13:00, 14:00-18:30"],
    "floor": "2ème étage avec ascenseur",
    "elevator": true,
    "handicapAccess": false,
    "accessInformation": ["2ème étage avec ascenseur"],
    "stations": ["metro 1 Georges V", "metro 9/1 Franklin D. Roosevelt", "rer C Pont De L'Alma"],
    "parking": "Parking Marbeuf Champs-Elysées",
    "places": [
        { "id": "practice-320361", "name": "Cabinet 26 Rue Marbeuf, 75008 Paris", "address": "26 Rue Marbeuf", "zipcode": "75008", "city": "Paris", "latitude": 48.8688803, "longitude": 2.3044131, "phone": "01 43 43 48 87" },
        { "id": "practice-807766", "name": "Cabinet Monceau Courcelles", "address": "38 Boulevard de Courcelles", "zipcode": "75017", "city": "Paris", "latitude": 48.8808058, "longitude": 2.3107689, "phone": "09 86 24 76 46" }
    ],
    "placesCount": 2,
    "practitioners": [],
    "photo": "https://media.doctolib.com/image/upload/q_auto:eco,f_auto/yfp5k9ij8dwqv2ff0arl.jpg",
    "pictures": ["https://media.doctolib.com/image/upload/q_auto:eco,f_auto/yfp5k9ij8dwqv2ff0arl.jpg", "https://media.doctolib.com/image/upload/q_auto:eco,f_auto/vkelgwhtaud4iaoebhcc.jpg"],
    "exactMatch": true,
    "searchSpeciality": "dentiste",
    "searchLocation": "Paris",
    "searchUrl": "https://www.doctolib.fr/dentiste/paris",
    "scrapedAt": "2026-09-17T09:00:00.000Z"
}
```

Requests that fail for good (a profile removed between the search and the detail request, a location Doctolib does not know among several searches, a network error after every retry) are listed in the **FAILED_REQUESTS** record of the run's key-value store, never in the dataset. A run that saves nothing while requests failed ends as **FAILED**, so a broken run is never mistaken for an empty search.

#### All 78 fields

| Fields | What you get |
| --- | --- |
| `id`, `url`, `slug`, `practiceId`, `country` | `1537577`, `https://www.doctolib.fr/dentiste/paris/vincent-bourasset?pid=practice-320361`, `fr` |
| `type`, `organizationType` | `practitioner` / `organization`, `Clinique privée` |
| `title`, `firstName`, `lastName`, `fullName`, `gender` | `Dr`, `Vincent`, `BOURASSET`, `Vincent BOURASSET`, `male` (search results) |
| `speciality`, `specialitySlug`, `specialities` | `Chirurgien-dentiste`, `dentiste`, every speciality with its id and kind |
| `address`, `zipcode`, `city`, `department`, `fullAddress`, `latitude`, `longitude`, `googleMapsUrl` | `26 Rue Marbeuf`, `75008`, `Paris`, `75` (France), `48.8688803`, `2.3044131`, map link |
| `distanceMeters` | `3659`: metres from the searched city or district to the practice, as the crow flies (search source) |
| `phone`, `phoneE164`, `website`, `practiceName` | `01 43 43 48 87`, `+33143434887` (`+49…` Germany, `+39…` Italy) |
| `rpps`, `adeli`, `siret` | `10100726651` (French legal ids, when published) |
| `regulationSector`, `regulationSectorLabel`, `acceptsInsuranceCard`, `thirdPartyPayer`, `homeVisit` | `contracted`, `Conventionné secteur 2`, `true`, `national` |
| `insuranceSector` | `public` / `private` — Germany and Italy only (always `null` in France) |
| `paymentMeans`, `languages` | `["cash","check","credit_card","bank_transfer"]`, `["fr","gb","it"]` |
| `hasOnlineBooking`, `isDirectoryOnly`, `telehealth`, `allowsNewPatients`, `visitMotive`, `visitMotiveFee` | booking status, first bookable visit motive and its price when Doctolib shows one |
| `visitMotives`, `visitMotiveNames`, `agendas` | **every** bookable visit motive (with the practices that offer it) and every agenda — option **Include every visit motive** |
| `agendaIds` | Doctolib ids of the booking agendas (search results; all of them with **Include every visit motive**) |
| `nextAvailableSlot`, `nextAvailableDate`, `waitTimeDays`, `totalAvailableSlots`, `availabilityVisitMotive` | `2026-09-24T09:00:00.000+02:00`, `2026-09-24`, `7` days to wait, `7` open slots this week, for `Première consultation dentaire` — option **Next available appointment** |
| `fees`, `minimumFee` | `[{ "name": "Consultation", "minPrice": 50, "maxPrice": 140, "isFixed": false }]` |
| `bio`, `skills`, `formations`, `experience`, `associations` | presentation text, procedures, diplomas, career |
| `openingHours`, `floor`, `elevator`, `handicapAccess`, `accessInformation`, `stations`, `parking` | `["Mon 09:00-13:00, 14:00-17:00", …]`, `metro 1 Georges V` |
| `places`, `placesCount` | every practice location of the profile (address, GPS, phone) |
| `practitioners` | team of an establishment (name, speciality, profile URL) |
| `photo`, `pictures` | Doctolib media URLs (never Doctolib's grey default avatar) |
| `exactMatch`, `searchSpeciality`, `searchLocation`, `searchUrl`, `scrapedAt` | how the profile was found |

### 💡 Tips

#### How to get more results

- **Big cities**: Doctolib returns at most 10 000 profiles per search. `dentiste` + `Paris` is fine (≈ 8 100); for `medecin-generaliste` in the whole country, put the departments or cities in **More locations**, or use the **sitemaps** source.
- **Directory-only profiles** (`isDirectoryOnly: true`) are practitioners who are not Doctolib customers: address, phone and RPPS are usually present, but no fees, hours, presentation or visit motives.
- **Broad matches**: after the exact matches, Doctolib appends practitioners from nearby towns or close specialities (`exactMatch: false`). Keep them for wider lists or set `exactMatchOnly`.

#### How to reduce costs

The price is per profile, so `maxItems` and `maxItemsPerQuery` are the cost levers. With `extractDetails` off (search source) a run needs one request per 20 profiles: about 150 profiles in a few seconds. **Only new profiles** never charges the profiles it skips; a filter applied by the Actor charges each profile it checks, kept or not (see the pricing above).

#### Several searches in one run

Every speciality of **Speciality** + **More specialities** is searched in every city of **Location** + **More locations** (up to 500 searches). **Max profiles per search** caps each of them; **Max practitioners** caps the whole run. A practitioner found by two searches is saved once, for the search that found them first (`searchUrl`). A location Doctolib does not know is reported in the log and in FAILED_REQUESTS; the other searches go on.

#### Monitoring: only the new profiles

Tick **Only new profiles** and schedule the Actor (daily, weekly…): each run returns only the practitioners that no previous run with the same **Memory key** delivered — they are not even requested, and not charged. Give each schedule its own key. The memory lives in the named key-value store `doctolib-practitioners-scraper-seen` of your account (up to 700 000 profiles per key); **Reset the memory** starts over. Two runs at the same time on the same key may deliver a profile twice.

#### Whole directory with the sitemaps source

`source: "sitemaps"` reads Doctolib's public sitemaps (France: 123 files, ≈ 615 000 profiles; Germany and Italy too) one file at a time and keeps the profiles whose URL names the speciality and the city you asked for — leave **Speciality** and **Location** empty for everything. Combine it with **Only new profiles**: every run continues where the previous one stopped. Differences with the search source: exact city only (no department, no radius, no skill), no `allowsNewPatients` / `visitMotive` from a search card, and sector / language filters are checked on each profile. A district (`paris-75009`) reads the Paris profiles and keeps those with a practice in the district: about 50 profiles read per profile kept.

#### Germany and Italy

Choose **Country** `de` or `it` and use that site's speciality slugs (`zahnmedizin`, `allgemeinmedizin`, `odontoiatra`, `medico-di-medicina-generale`: an unknown value lists the closest ones). The sector filter (conventionnement) and the RPPS / ADELI ids only exist in France; phone numbers are converted to `+49` / `+39`.

### ⚖️ Legal and privacy

This Actor collects only what Doctolib shows to any anonymous visitor: the professional identity, practice address and phone, legal ids (RPPS/ADELI are public identifiers), fees, hours, visit motives and presentation that healthcare professionals and establishments publish, and — with **Next available appointment** — the open slots of their public booking calendar, as a count and a first date, never who booked them. It does not access patient data or any authenticated area, and it does not download photos. Practitioners' names, phones and calendars are personal data under GDPR: you are responsible for using the data in compliance with GDPR and Doctolib's terms of use, in particular for direct marketing to professionals.

### 🔌 Integrations and API

Connect the Actor to Google Sheets, Slack, Make, Zapier, n8n, webhooks or your own code with the [Apify API](https://docs.apify.com/api/v2) and the JavaScript / Python clients. Schedule it with **Only new profiles** to keep a practitioner database fresh, or call it on demand from your CRM.

### 🤖 Use with AI agents (MCP)

AI agents (Claude, ChatGPT, Cursor…) can find and run this Actor through the [Apify MCP server](https://mcp.apify.com), billed to their Apify account like any run. It returns one item per Doctolib profile: a practitioner or an establishment. Actor id: `nice_dev/doctolib-practitioners-scraper`; MCP server with this Actor only: `https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/doctolib-practitioners-scraper`.

Smallest input, for a cheap first call:

```json
{
    "speciality": "dentiste",
    "location": "Paris",
    "maxItems": 10
}
```

Key output fields: `url`, `fullName`, `speciality`, `fullAddress`, `city`, `phone`, `rpps`, `nextAvailableDate`.

Cost: $0.62 per 1,000 profiles plus $0.01 per run start ($0.59 per 1,000 on the Gold plan); the appointment slots (`appointment-slots`) and the Actor's own filters are charged on top, see the pricing section above. Cap each call with `maxItems` and, through the API, with the run option `maxTotalChargeUsd`.

### ❓ FAQ

#### Is it legal to scrape Doctolib?

The Actor only reads what Doctolib shows publicly to any anonymous visitor about **professionals**: identity, speciality, practice address and phone, public legal ids, fees, hours, visit motives and, on request, the open slots of their booking calendar. It logs in to nothing, and reads no patient data. Results contain personal data, which is protected by GDPR: do not store it without a legitimate reason, and keep the practitioners who ask to be left out in **Suppressed profile ids** / **Suppressed profile slugs**. You are responsible for using the data in compliance with Doctolib's Terms of Use and applicable law. This Actor is not affiliated with Doctolib.

#### What is the difference between the two sources?

- **search** (default) returns what Doctolib's own results page shows: nearest practitioners around a place, relevance order, cities, districts, departments and regions, and the sector / insurance / language filters applied by Doctolib itself. Measured on 2026-09-17: 20 dentists of Paris 9ᵉ in 3 seconds, all 20 really in the 75009 zipcode.
- **sitemaps** walks the sitemaps Doctolib publishes for crawlers and the profile pages they list. It has no notion of distance or filter: it matches the city written in each profile URL, so a district search reads the whole city and throws most of it away (95 profiles read for 2 kept on Paris 9ᵉ, 2026-09-17), and one sitemap file weighs about 450 KB. Use it for the **whole directory** (≈ 615 000 French profiles) or to walk a country in successive runs with **Only new profiles**.
- The two sources give the same fields, **not the same profiles**. On a partial run they barely overlap, because they walk Doctolib in a different order (distance and relevance for search, file order for sitemaps): 20 dentists of Paris 9ᵉ from each on 2026-09-18, 1 in common. On a complete list they mostly agree: for the dentists of Vannes, search returned 65 practitioners (48 in Vannes, then 17 from nearby towns flagged `exactMatch: false`) and sitemaps 59 profiles in Vannes, 47 of them in both. The 12 found only by sitemaps are profiles Doctolib's own search no longer shows (4 with no practice at all, most without a phone number); the one found only by search is filed under another speciality.
- With the sitemaps source, the fields that come from the search card stay `null` (`allowsNewPatients`, `visitMotive`, `visitMotiveFee`, `minimumFee`, `insuranceSector`, `gender`, `distanceMeters`) and `exactMatch` is always `true`. With **Next available appointment**, the slots are those of the first in-person visit motive of the practice instead of the motive the search matched (`availabilityVisitMotive` says which), for one more request per profile.

#### Can it return appointment slots or availabilities?

Yes, with **Next available appointment** (`scrapeAvailability`). For each bookable profile the Actor asks Doctolib for the open slots of one visit motive, the way the profile page does: the motive the search matched (`Première consultation dentaire`…), or — for a profile found without a search card — the first in-person motive of its practice. You get `nextAvailableSlot` (first open slot; beyond the week when Doctolib names it), `nextAvailableDate`, `waitTimeDays` (0 = a slot today) and `totalAvailableSlots` over the 7 days from the run day. **Available within (days)** keeps only the practitioners with a slot within N days. It costs one more request per bookable profile (two without a search card), charged apart: event `appointment-slots` per profile saved with an open slot, nothing for a profile without any (see the cost section). Directory-only profiles (not Doctolib customers) have no calendar and keep these fields `null`. In Germany and Italy, a motive open to private patients only is asked for them (`insurance_sector=private`, as the results page does): without it Doctolib answers "not bookable". Slots change by the minute: a run is a snapshot. Reviews do not exist on Doctolib France, Germany or Italy.

#### Does it need a login or a proxy?

No login. The proxy is included in the price: leave the default setting (the residential proxy is not available). A request Doctolib turns away (HTTP 403 / 429) is retried at once on a new proxy session — without a proxy, after a pause of 5 seconds, doubled at each retry up to 20 seconds.

#### Is the data safe to open in Excel or to show on a web page?

Every `phoneE164` starts with `+`, and a name, a speciality or a presentation is the professional's own words, copied as they are: a text can begin with `-`, `+`, `=` or `@`. Excel and Google Sheets may read such a cell of a CSV file as a formula or as a number. The Actor leaves the text as it is, so that the JSON and the API give the real value: when you open a CSV, import these columns as text. Every URL field (`url`, `photo`, `pictures`, `website`, `googleMapsUrl`, the profile links of `practitioners`) holds an `http(s)` URL or `null`, so none of them can carry a script. On a web page, escape every field like any text written by a stranger.

#### Known limitations

- Doctolib caps one search at 10 000 results: split big searches by city, or use the sitemaps source.
- `insuranceSector` only filters on Doctolib Germany (Berlin general practitioners, 2026-09-17: 2 125 total, 796 public, 887 private). France and Italy accept the filter and return everybody. Even in Germany the field itself is filled on about half of the cards. It needs the search source: the sitemaps source refuses it, since a profile read from a sitemap carries no insurance sector.
- The sitemaps source matches the city of the profile URL exactly and checks that a practice is still there. A profile without any practice (a practitioner who left Doctolib's search) is kept for a city search — its URL is the only place it gives — and dropped for a district search.
- A filter that matches (almost) nothing reads profiles until it stops: after 300 profiles in a row rejected, 2 000 extra requests, or when the run's maximum cost is reached, whichever comes first (each profile checked is charged, see Pricing).
- Detail fields are `null` or empty when `extractDetails` is off; visit motives need `includeVisitMotives`; slots need `scrapeAvailability` and `extractDetails` (they are not read in the search-card-only mode).
- Slots are those of ONE visit motive (`availabilityVisitMotive`), not of every motive of the practitioner; an establishment's slots cover at most 100 of its agendas. A query Doctolib refuses leaves the slots `null` (0 means: asked, none this week).
- An establishment read without its search card (sitemaps source, pasted profile URL) has no `languages` and its `speciality` is its type (`Laboratoire`): Doctolib's profile of an establishment has neither the speciality it was found for nor languages. A practitioner gives the same values with or without the card.
- `onlyNew` remembers which profiles were delivered, not their content: a profile whose phone changed is not returned again. Two runs sharing the same `stateKey` at the same time may both return the same new profile.
- Doctolib may change its pages without notice: see below what happens then.

**A run the platform stops without warning** (out of memory, run timeout)

- Resurrect it: it goes on from where it stood at most a minute before the stop. What it had read since is read again, and the profiles already saved are skipped: none is delivered or charged twice, and **Max practitioners** still counts them.
- With **Only new profiles**, the memory is saved after each write (at most once every 2 seconds): resurrect the stopped run and the profiles it had saved meanwhile join the memory; leave it stopped for good, and the next run may return the profiles of its last 2 seconds once more.

#### Something doesn't work?

The last line of the log counts the profiles saved, filtered out, already delivered by a previous run and no longer on Doctolib (removed while the run was reading them), and the requests that failed after every retry. Those requests and the removed profiles are listed, with the reason, in the `FAILED_REQUESTS` record of the run's key-value store. A run that saved nothing and had failed requests fails, and its last message gives the cause (a speciality Doctolib does not know says so, instead of "run it again"). A run that saved some profiles fails too when at least as many requests failed for good as were read (page 1 read, the next pages or the profile pages blocked): a green run with a short dataset would hide the outage. One failed request among many is only a warning.

If Doctolib changes its pages, you are told instead of paying for blank rows. A results page that counts practitioners but gives none the Actor can read is an error (listed in `FAILED_REQUESTS`), never a quiet "No profiles found". If the first 20 profiles read all lack their last name or speciality (practitioners), their city and GPS position, the visit motive of their search card, or — for a profile page — its list of practices, or — with **Next available appointment** — an answer to their slots query, the run saves nothing more, stops and fails, and its last message names the missing field: at most those first profiles are charged.

### 🛟 Support

Open an issue in the **Issues** tab of the Actor with a link to your run: the run log and the `FAILED_REQUESTS` record of the key-value store show exactly which URLs failed and why.

# Actor input Schema

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

Doctolib search pages (`https://www.doctolib.fr/dentiste/paris`, filters `regulationSector[]=` / `languages[]=` and `?page=` are kept) or single profile pages (`https://www.doctolib.fr/dentiste/paris/<slug>`), on doctolib.fr, doctolib.de or doctolib.it (each URL keeps its own country). When this list is not empty, the Country / Speciality / Location / Source fields are ignored; the Sector, Languages and Insurance filters still apply (added to the filters written in a search URL, whose value wins; the sector filter is for doctolib.fr URLs only, and a profile page is checked for sector and language, not for insurance). Max 1 000 URLs.

## `country` (type: `string`):

Doctolib site to search: France (doctolib.fr), Germany (doctolib.de) or Italy (doctolib.it). Specialities use that site's slugs (`dentiste` / `zahnmedizin` / `odontoiatra`).

## `speciality` (type: `string`):

Doctolib speciality slug or name: `dentiste`, `medecin-generaliste`, `Ophtalmologue`, `Kinésithérapeute`… Doctolib skills work too with the search source (`implantologue`). Germany / Italy: the site's slugs (`zahnmedizin`, `allgemeinmedizin`, `odontoiatra`, `medico-di-medicina-generale`). An unknown value gives a clear error with suggestions. Leave empty with the sitemaps source to take every speciality.

## `searchQueries` (type: `array`):

Several specialities (or skills) in one run: one search per speciality, times each location below (3 specialities × 4 locations = 12 searches, max 500). Added to **Speciality**; a profile found by several searches is saved once.

## `location` (type: `string`):

City (`Paris`, `Lyon`, `Saint-Étienne`, `Berlin`, `Milano`), Paris/Lyon/Marseille district (`paris-75009`, `Paris 9e`), department or region as Doctolib names them (search source), or empty / `France` for the whole country (capped at 10 000 results per search by Doctolib: split big searches by city). Unknown places are reported, never silently widened to the whole country. Default: Paris for France, the whole country for Germany / Italy (the form shows `Paris` for every country: with Germany or Italy, leaving it as `Paris` searches the whole country).

## `locations` (type: `array`):

Several locations in one run: every speciality is searched in every location. Added to **Location**.

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

Maximum number of practitioners / establishments to save in the whole run (one row per Doctolib profile, deduplicated). 0 = no limit. For reference: dentists in Paris ≈ 8 100, GPs in Lyon ≈ 1 500.

## `maxItemsPerQuery` (type: `integer`):

Cap for EACH search (speciality × location, or search URL), so that the first search cannot use up the whole **Max practitioners** budget. 0 = no per-search cap.

## `source` (type: `string`):

`search` = Doctolib's own search engine (nearest practitioners around the place, relevance order, departments / regions, skills, sector, insurance and language filters sent to the site): 20 profiles per request, and only the profiles you asked for. `sitemaps` = Doctolib's public sitemaps: every profile whose URL names the speciality and the city (exact city, no radius; whole directory with an empty speciality and location; sector / language filters applied on the profile), much heavier for one city. See the FAQ.

## `extractDetails` (type: `boolean`):

Open each profile (1 extra request per profile) for phone number, RPPS / ADELI, presentation, website, fees, opening hours, skills, diplomas, specialities, all practice locations with accessibility and transport, establishment team. Off = search-card fields only (name, speciality, address, GPS, sector, payment means, languages, photo): much faster, same price. Always on with the sitemaps source.

## `includeVisitMotives` (type: `boolean`):

One more request per bookable profile: every visit motive (`visitMotives`, `visitMotiveNames`, with the practices that offer each), every agenda (`agendas`: practice, speciality, booking switched off, phone) and every speciality. Slots: see **Next available appointment**. About 2× slower, same price.

## `scrapeAvailability` (type: `boolean`):

Asks Doctolib for the open appointment slots of each bookable profile, the way its profile page does: `nextAvailableSlot` (first open slot, even beyond the week), `nextAvailableDate`, `waitTimeDays` (days to wait from the run day, 0 = today), `totalAvailableSlots` (open slots over the 7 days from the run day) and `availabilityVisitMotive` (the visit motive the slots are for: the one the search matched, else the first in-person motive of the practice). One more request per bookable profile, two for a profile found without a search card (sitemaps source, profile URL). Charged apart: event `appointment-slots` per profile saved with an open slot (a next slot, or a slot in the week); a profile without any slot (no online booking, refused query, empty agenda) is not charged for it (see Pricing). Needs **Extract details**. Slots change by the minute: they are a snapshot of the run.

## `includeOrganizations` (type: `boolean`):

Doctolib search results mix individual practitioners and establishments (clinics, health centres, laboratories). Turn off to keep individual practitioners only.

## `regulationSector` (type: `array`):

France only. Keep only practitioners in these sectors (Doctolib filters on the exact code). Empty = all. Doctors are in sector 1 (`CONTRACTED_1`) or 2 (`CONTRACTED_2`, `CONTRACTED_2_WITH_OPTAM`); `CONTRACTED` (no sector) is the code of dentists and physiotherapists: for the general practitioners of Paris it keeps 2, `CONTRACTED_1` keeps 3 740 (2026-09-18). `ORGANIZATION_*` = establishments.

## `languages` (type: `array`):

Keep only practitioners speaking one of these languages (Doctolib codes). Empty = all.

## `insuranceSector` (type: `string`):

Germany only. Keep only practitioners who take patients of this insurance sector (Berlin general practitioners on 2026-09-17: 2 125 total, 796 public, 887 private). Doctolib France and Italy accept the filter but return every practitioner, so leave it on `Any` there. Search source only: a profile read from a sitemap carries no insurance sector.

## `exactMatchOnly` (type: `boolean`):

Doctolib appends 'broad match' results (related specialities and skills) after the exact matches. Turn on to stop at the last exact match.

## `requirePhone` (type: `boolean`):

Keep only the profiles that show a practice phone number (read on the profile: needs **Extract details**).

## `availabilitiesBefore` (type: `integer`):

Keep only the profiles with an open appointment slot within this many days of the run day (1 = today or tomorrow). 0 = no filter. Switches **Next available appointment** on: every profile kept comes with its slots, charged like that option (`appointment-slots` per profile saved). Profiles without online booking are dropped. A dropped profile is not saved; each profile checked is charged a filter fee, kept or not (see Pricing).

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

Drop the profiles whose name, speciality or establishment type contains one of these words (case and accents ignored), e.g. `centre`, `laboratoire`.

## `onlyAcceptingNewPatients` (type: `boolean`):

Keep only the practitioners whose visit motive in the search results accepts new patients (`allowsNewPatients`). Judged on the search results, before any profile request. Needs the search source; pasted profile URLs are kept (their page does not say).

## `suppressedDoctorIds` (type: `array`):

Doctolib ids (the `id` field) of profiles never to deliver — e.g. practitioners who asked to be left out of your files. Skipped before their profile is requested.

## `suppressedDoctorSlugs` (type: `array`):

Same, by slug (the `slug` field, e.g. `vincent-bourasset`) or by profile URL.

## `onlyNew` (type: `boolean`):

Skip the profiles that a previous run (same **Memory key**) already delivered: they are not saved, not charged, and not even requested. First run = everything is new.

## `stateKey` (type: `string`):

Name of the memory used by **Only new profiles**. Give each schedule / task its own key (e.g. `paris-dentists`) so that they do not share their memory. Letters, digits, `-` and `_`.

## `resetState` (type: `boolean`):

Forget everything remembered under this **Memory key** before the run: this run returns (and charges) every profile again. Untick it afterwards.

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

Apify Proxy or your own proxies. Keep the default: it is included in the price. The residential Apify proxy is not available in this Actor.

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

Maximum number of requests processed in parallel.

## `maxRequestsPerMinute` (type: `integer`):

Global request rate. Lower it if the site answers HTTP 429 / 403 in the log.

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

Retries per request before it is marked as failed.

## `debugLog` (type: `boolean`):

Include debug messages in the run log.

## Actor input object example

```json
{
  "country": "fr",
  "speciality": "dentiste",
  "searchQueries": [],
  "location": "Paris",
  "locations": [],
  "maxItems": 100,
  "maxItemsPerQuery": 0,
  "source": "search",
  "extractDetails": true,
  "includeVisitMotives": false,
  "scrapeAvailability": false,
  "includeOrganizations": true,
  "regulationSector": [],
  "languages": [],
  "insuranceSector": "any",
  "exactMatchOnly": false,
  "requirePhone": false,
  "availabilitiesBefore": 0,
  "excludeKeywords": [],
  "onlyAcceptingNewPatients": false,
  "suppressedDoctorIds": [],
  "suppressedDoctorSlugs": [],
  "onlyNew": false,
  "stateKey": "default",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 8,
  "maxRequestsPerMinute": 120,
  "maxRequestRetries": 4,
  "debugLog": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "speciality": "dentiste",
    "location": "Paris",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("nice_dev/doctolib-practitioners-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 = {
    "speciality": "dentiste",
    "location": "Paris",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("nice_dev/doctolib-practitioners-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 '{
  "speciality": "dentiste",
  "location": "Paris",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call nice_dev/doctolib-practitioners-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/doctolib-practitioners-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/jdTZcTFOmHf4QvjZg/builds/83wmxmfg2E4a4OKyA/openapi.json
