Doctolib Profiles (France)
Doctolib practitioner profiles — specialty, consultation fees, sector, payment means, opening hours, education, skills and practice addresses — keyed by national practitioner ID.
4 rows in the dataset
Download the dataset
Get every row this scraper has collected as one CSV or JSONL file. If the data is older than maxAgeHours, the scraper is re-run with its configured settings first. The key needs the datasets:export scope.
DOWNLOAD_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/datasets/doctolib-profiles/downloads" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{"maxAgeHours":24,"format":"csv"}' | jq -r '.id')while true; do STATUS=$(curl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY" | jq -r '.status') [ "$STATUS" != "refreshing" ] && break sleep 5donecurl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID/file" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -o dataset.csvAt its current 4 rows, this download costs 1 credit when the data is fresh enough, or 2 credits when it has to be refreshed first.
The rate is 1 credit per 100 rows cached and 2 credits live, charged on the rows in the dataset when the download becomes ready. A failed refresh is not charged. The file stays downloadable for 24 hours. See pricing.
Start a run
To choose the targets yourself, send the parameters below as input. The call returns 202 with the run; the key needs the scrapers:run scope and the account a positive credit balance.
curl -X POST "https://api.pandorlabs.com/v1/scrapers/doctolib-profiles/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "professions": [ "10" ], "specialties": [ "SM10" ], "cities": [ "paris" ], "departments": [ "75", "92", "06" ] } }'A run is billed at the live rate once it finishes: 2 credits per 100 rows it collected. A run that collects nothing costs nothing.
Parameters
Every parameter is optional. Anything you leave out uses the default shown here; an unknown name or an invalid value is rejected with a 400 that lists each problem.
RPPS enrichment
professionsenum[]
Professions
RPPS profession codes to look up on Doctolib. Empty means every profession in the scraped register — narrow it, a lookup costs two requests.
- Default
- none
- Allowed values
10405070609371919280959694
specialtiesenum[]
Specialties
RPPS `Code savoir-faire`. Other codes can be typed in through the JSON options.
- Default
- none
- Allowed values
SM10SM15SM06SM07SM68SM39SM19
citiesenum[]
Cities
- Default
- none
- Allowed values
parismarseillelyontoulousenicenantesmontpellierstrasbourgbordeauxlillerennesreimstoulonsaint-etiennele-havregrenobledijonangersnimesvilleurbanneaix-en-provenceclermont-ferrandbresttoursamienslimogesannecyperpignanmetzbesanconorleansrouencaennancypaucannesantibesboulogne-billancourtneuilly-sur-seine
departmentsstring[]
Départements
Combined with cities as an OR.
- Default
- none
- Example
75, 92, 06
liberalOnlyboolean
Private practice only
Hospital-salaried professionals are rarely bookable on Doctolib under their own name.
- Default
true
recheckAfterDaysnumber
Recheck after (days)
Professionals looked up more recently than this are skipped, found or not. 0 rechecks everyone.
- Default
30- Range
- 0 to 3650
maxPractitionersnumber
Max professionals per run
- Default
500- Range
- 1 to 3000000
Direct lookup
queriesstring[]
Search queries
Free-text Doctolib searches instead of the RPPS register. Every practitioner returned is scraped, and linked back to RPPS when the profile carries an RPPS number.
- Default
- none
- Example
denis liguori, cohen paris
profilesstring[]
Profile ids, slugs or URLs
- Default
- none
- Example
253433, denis-liguori
maxProfilesPerQuerynumber
Max profiles per query
- Default
20- Range
- 1 to 500
Advanced
modeenum
Mode
Auto uses the profile list, then the queries, then falls back to enriching the RPPS register.
- Default
auto- Allowed values
autorppssearchprofiles
maxCandidatesnumber
Profiles opened per professional
Name matches are opened in order of city until one carries the same RPPS number.
- Default
3- Range
- 1 to 10
concurrencynumber
Concurrency
Only raise this with rotating proxies — Doctolib rate-limits a single IP at roughly 30 requests a minute.
- Default
1- Range
- 1 to 16
requestDelayMsnumber
Delay between requests (ms)
Per worker.
- Default
2000- Range
- 0 to 60000
blockedCooldownMinutesnumber
Cooldown after a block (minutes)
A 429 from Doctolib holds the IP for several minutes. Without proxies the run pauses this long before retrying.
- Default
10- Range
- 1 to 120
Input as JSON Schema
The same schema the API validates against. Use it as a tool input schema or to generate a typed client.
{ "type": "object", "additionalProperties": false, "properties": { "professions": { "type": "array", "items": { "type": "string", "enum": [ "10", "40", "50", "70", "60", "93", "71", "91", "92", "80", "95", "96", "94" ] }, "uniqueItems": true, "title": "Professions", "description": "RPPS profession codes to look up on Doctolib. Empty means every profession in the scraped register — narrow it, a lookup costs two requests.", "default": [] }, "specialties": { "type": "array", "items": { "type": "string", "enum": [ "SM10", "SM15", "SM06", "SM07", "SM68", "SM39", "SM19" ] }, "uniqueItems": true, "title": "Specialties", "description": "RPPS `Code savoir-faire`. Other codes can be typed in through the JSON options.", "default": [] }, "cities": { "type": "array", "items": { "type": "string", "enum": [ "paris", "marseille", "lyon", "toulouse", "nice", "nantes", "montpellier", "strasbourg", "bordeaux", "lille", "rennes", "reims", "toulon", "saint-etienne", "le-havre", "grenoble", "dijon", "angers", "nimes", "villeurbanne", "aix-en-provence", "clermont-ferrand", "brest", "tours", "amiens", "limoges", "annecy", "perpignan", "metz", "besancon", "orleans", "rouen", "caen", "nancy", "pau", "cannes", "antibes", "boulogne-billancourt", "neuilly-sur-seine" ] }, "uniqueItems": true, "title": "Cities", "default": [] }, "departments": { "type": "array", "items": { "type": "string" }, "maxItems": 1000, "title": "Départements", "description": "Combined with cities as an OR.", "default": [], "examples": [ "75, 92, 06" ] }, "liberalOnly": { "type": "boolean", "title": "Private practice only", "description": "Hospital-salaried professionals are rarely bookable on Doctolib under their own name.", "default": true }, "recheckAfterDays": { "type": "number", "minimum": 0, "maximum": 3650, "title": "Recheck after (days)", "description": "Professionals looked up more recently than this are skipped, found or not. 0 rechecks everyone.", "default": 30 }, "maxPractitioners": { "type": "number", "minimum": 1, "maximum": 3000000, "title": "Max professionals per run", "default": 500 }, "queries": { "type": "array", "items": { "type": "string" }, "maxItems": 1000, "title": "Search queries", "description": "Free-text Doctolib searches instead of the RPPS register. Every practitioner returned is scraped, and linked back to RPPS when the profile carries an RPPS number.", "default": [], "examples": [ "denis liguori, cohen paris" ] }, "profiles": { "type": "array", "items": { "type": "string" }, "maxItems": 1000, "title": "Profile ids, slugs or URLs", "default": [], "examples": [ "253433, denis-liguori" ] }, "maxProfilesPerQuery": { "type": "number", "minimum": 1, "maximum": 500, "title": "Max profiles per query", "default": 20 }, "mode": { "type": "string", "enum": [ "auto", "rpps", "search", "profiles" ], "title": "Mode", "description": "Auto uses the profile list, then the queries, then falls back to enriching the RPPS register.", "default": "auto" }, "maxCandidates": { "type": "number", "minimum": 1, "maximum": 10, "title": "Profiles opened per professional", "description": "Name matches are opened in order of city until one carries the same RPPS number.", "default": 3 }, "concurrency": { "type": "number", "minimum": 1, "maximum": 16, "title": "Concurrency", "description": "Only raise this with rotating proxies — Doctolib rate-limits a single IP at roughly 30 requests a minute.", "default": 1 }, "requestDelayMs": { "type": "number", "minimum": 0, "maximum": 60000, "title": "Delay between requests (ms)", "description": "Per worker.", "default": 2000 }, "blockedCooldownMinutes": { "type": "number", "minimum": 1, "maximum": 120, "title": "Cooldown after a block (minutes)", "description": "A 429 from Doctolib holds the IP for several minutes. Without proxies the run pauses this long before retrying.", "default": 10 } }}Run, poll, read rows
The run is asynchronous. Poll it until it reaches a terminal status and its billing is paid, then page through its rows. Until then the rows endpoint answers 409 (still queued or running) or 402 (payment required).
RUN_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/scrapers/doctolib-profiles/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "professions": [ "10" ], "specialties": [ "SM10" ], "cities": [ "paris" ], "departments": [ "75", "92", "06" ] } }' | jq -r '.id')while true; do RUN=$(curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY") STATUS=$(echo "$RUN" | jq -r '.status') BILLING=$(echo "$RUN" | jq -r '.billing.status') case "$STATUS" in queued|running) ;; *) [ "$BILLING" != "pending" ] && break ;; esac sleep 5doneecho "$RUN" | jq '{status, rowCount, billing}'curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID/rows?page=1&limit=100" \ -H "Authorization: Bearer $PANDORLABS_KEY"See the API reference for pagination, cancelling and listing runs.
Output fields
Rows have no fixed schema; these are the fields observed in the rows collected so far. Coverage is the share of rows where the field is present.
| Field | Type | Coverage |
|---|---|---|
| accepts_carte_vitale | boolean | 50% |
| address | string | 100% |
| avatar_url | string | 100% |
| bio | string | 50% |
| city | string | 100% |
| freemium | boolean | 100% |
| home_visit | boolean | 100% |
| is_organization | boolean | 100% |
| latitude | number | 100% |
| longitude | number | 100% |
| match_method | string | 100% |
| name | string | 100% |
| name_with_title | string | 100% |
| online_booking | boolean | 100% |
| payment_means | array | 50% |
| phone | string | 75% |
| places | array | 100% |
| postal_code | string | 100% |
| practitioner_id | string | 100% |
| profile_id | string | 100% |
| regulation_sector | string | 50% |
| skills | array | 50% |
| slug | string | 100% |
| speciality | string | 100% |
| speciality_id | number | 100% |
| speciality_slug | string | 100% |
| subtitle | string | 100% |
| telehealth | boolean | 100% |
| third_party_payer | string | 50% |
| created_at | date | 100% |
| first_seen_at | date | 100% |
| last_seen_at | date | 100% |
| updated_at | date | 100% |
| _id | string | 100% |
