# Doctoralia Scraper - Doctors & Clinics by Speciality (`dami_studio/doctoralia-scraper`) Actor

Pull practitioner and clinic listings off Doctoralia in 12 countries. One row per listing: name, speciality and sub-specialities, clinic, street address, city, coordinates, rating, review count and the published consultation price. No phone numbers, no emails, no review text.

- **URL**: https://apify.com/dami\_studio/doctoralia-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.90 / 1,000 listing 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?

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

## Doctoralia Scraper

Reads Doctoralia's speciality-and-city listing pages and gives you one row per practitioner or
clinic: the name, what they practise, where they practise, the rating and review count Doctoralia
shows, and the consultation price when the professional has published one.

It covers twelve country sites, including the ones that do not live where you would guess:
Argentina is `doctoraliar.com` and Peru is `doctoralia.pe`.

### Read this before you buy it

**There are no phone numbers and no email addresses in the output. That is on purpose.**

These rows describe named healthcare professionals. The listing pages this Actor reads carry no
`tel:` link, no `mailto:` and no telephone or email microdata at all (317 listing cards across all
twelve country sites, zero of each), and the Actor does not go looking for contact details anywhere
else. Every row also passes a filter that strips anything shaped like an email address, a phone
number or a national identity number before it is written, so a field that changes shape on
Doctoralia's side cannot quietly turn into a contact export.

If what you need is a call list, this is the wrong purchase. It will not become one by running it
longer.

There is no review text either. Doctoralia publishes patient reviews with the patient's first name
attached; those are somebody else's words about their own medical care and they are not in these
rows. You get the star rating and how many reviews there are. Nothing else from the review section.

### Countries

| Country | Site | Speciality slug looks like |
|---|---|---|
| Spain | doctoralia.es | `dentista`, `psicologo`, `pediatra` |
| Brazil | doctoralia.com.br | `dentista`, `ginecologista` |
| Mexico | doctoralia.com.mx | `psicologo`, `dentista` |
| Colombia | doctoralia.co | `odontologo`, `psicologo` |
| Chile | doctoralia.cl | `dentista`, `psicologo` |
| Argentina | doctoraliar.com | `psicologo`, `odontologo` |
| Peru | doctoralia.pe | `dentista`, `psicologo` |
| Portugal | doctoralia.com.pt | `dentista`, `psicologo` |
| Italy | miodottore.it | `dentista`, `ginecologo` |
| Poland | znanylekarz.pl | `stomatolog`, `psycholog` |
| Czechia | znamylekar.cz | `zubar`, `psycholog` |
| Turkey | doktortakvimi.com | `dis-hekimi`, `psikolog` |

Speciality names are in the local language. `dentysta` returns nothing in Poland; the word
Doctoralia uses there is `stomatolog`. If your spelling does not match a real listing page, the
Actor falls back to the site's own search box rather than failing, so a near miss usually still
works, but the exact slug is faster and goes deeper.

### Input

```json
{
  "speciality": ["dentista"],
  "city": "madrid",
  "country": "es",
  "maxItems": 50
}
```

Or paste listing pages and skip the fields entirely:

```json
{
  "startUrls": [
    "https://www.doctoralia.com.br/dentista/sao-paulo",
    "https://www.miodottore.it/dentista/milano",
    "https://www.znanylekarz.pl/stomatolog/warszawa"
  ],
  "maxItems": 90
}
```

| Field | What it does |
|---|---|
| `speciality` | One or more speciality terms, one per line |
| `city` | City to search in. Leave empty for the whole country |
| `country` | Which country site to read |
| `maxItems` | Hard cap on rows for the whole run |
| `startUrls` | Listing pages, if you would rather paste URLs |
| `insurance` | Return only professionals listed under one insurer, by that insurer's slug, for example `adeslas` or `sanitas` |
| `includeClinics` | Doctoralia mixes clinics into most speciality listings. Off gives you individuals only |

`maxItems` is shared across your searches, not spent in order. Paste six cities with a cap of 90 and
you get fifteen from each, not ninety from the first one and nothing from the rest. A search that
runs out early hands its share to the others.

### Output

One real row, from a run against `doctoralia.com.br`:

```json
{
  "entityType": "practitioner",
  "id": "382464",
  "locationId": "567727",
  "name": "Dra. Carmen Lucia Calixto",
  "speciality": "Dentista",
  "subSpecialities": ["Ortodontia", "Dentística", "Ortodontia e ortopedia facial", "Harmonização facial"],
  "specialityIds": ["64"],
  "clinicName": "Dra. Carmen Calixto Odontologia",
  "street": "rua Madre de Deus, 1603",
  "city": "São Paulo",
  "region": "São Paulo SP",
  "country": "Brazil",
  "latitude": -23.5671577,
  "longitude": -46.5889091,
  "rating": 5,
  "reviewCount": 5,
  "consultationService": "Consulta Odontológica",
  "consultationPrice": "R$ 180",
  "consultationPriceAmount": 180,
  "consultationPriceCurrency": "R$",
  "consultationPriceIsFrom": false,
  "consultationPriceIsFree": false,
  "insuranceFilter": null,
  "onlineConsultationOnly": false,
  "onlineBooking": true,
  "slotsAvailable": true,
  "hasPhoto": true,
  "profileUrl": "https://www.doctoralia.com.br/carmen-lucia-calixto/dentista/sao-paulo",
  "countryCode": "br",
  "searchSpeciality": null,
  "searchCity": null,
  "sourceUrl": "https://www.doctoralia.com.br/dentista/sao-paulo",
  "listingHeading": "Dentistas em São Paulo",
  "page": 1,
  "position": 1,
  "scrapedAt": "2026-09-20T01:41:42.243Z"
}
```

`searchSpeciality` and `searchCity` echo back what you typed, so you can tell which of your searches
a row came from. They are null above because that run pasted URLs instead.

`entityType` is `practitioner` or `clinic`. Clinics are the same shape, with the clinic's own name in
`name` and `clinicName` left empty.

`consultationPrice` keeps whatever Doctoralia printed, because the phrasing carries meaning:
`da 90 €` and `90 €` are not the same offer. `consultationPriceAmount` is the number pulled out of
it, `consultationPriceIsFrom` tells you it was a starting price, and `consultationPriceIsFree` is
set when the listing says the first visit costs nothing.

`listingHeading` is the H1 Doctoralia rendered for the page the row came from, for example "Dentistas em São
Paulo" above. Useful when you asked for one city and want to check what the site actually answered.

### What you actually get, measured

From one run across six country sites (Brazil, Italy, Poland, Turkey, Peru and Argentina), fifteen
rows from each:

| Field | Filled |
|---|---|
| name, speciality, city, coordinates | 90 of 90 |
| street address | 89 of 90 |
| online-booking flag | 88 of 90 |
| rating and review count | 86 of 90; the gaps are professionals with no reviews yet |
| region or province | 78 of 90 |
| consultation service | 75 of 90 |
| clinic name | 74 of 90 |
| consultation price | 70 of 90 |
| sub-specialities | 58 of 90 |

### Limitations, plainly

**Turkey publishes no consultation prices on its listing pages.** Zero of fifteen rows from
`doktortakvimi.com` carried a price or a service name, where Italy and Poland carried fifteen of
fifteen. The other fields are all there. This is how that site is built, not something a retry fixes.

**Sub-specialities depend on the country template.** Spain, Brazil, Italy, Poland, Peru, Argentina
and Czechia list them in brackets after the main speciality and the Actor splits them out. Turkey
runs them together into one comma-separated string, which lands whole in `speciality` and leaves
`subSpecialities` mostly empty there. That is what the page shows.

**One address per row.** A professional practising at three clinics appears once, with the first
address Doctoralia shows. The others are on their profile page, which this Actor does not open.

**"Price not specified" is common and it is real data.** Plenty of professionals list a service
without a fee. Those rows come back with `consultationPrice` set to the site's own wording and
`consultationPriceAmount` null, rather than being silently dropped.

**Ratings are Doctoralia's, not a judgement.** A five with two reviews and a five with three hundred
are both a five. `reviewCount` is in the row for exactly that reason.

**This reads listing pages, not profiles.** Paste a profile URL and you get a diagnostic row telling
you so, uncharged. Paste the listing it appears on instead.

**Deep pages repeat.** A big city listing runs about a hundred pages: dentists in Madrid advertises
ninety-eight. Ask for more and Doctoralia keeps answering with HTTP 200 rather than a 404: pages
105, 115 and 125 all came back with three cards and not one professional that had not already
appeared. The Actor stops at the first page that adds nobody new, so you are not billed for laps.

### Billing

You are charged per row returned. Sample rows and diagnostic rows are free: an empty input, a
speciality the site does not recognise, a URL it cannot read, a page that failed to load: all of
those write an explanatory row that costs you nothing and the run still ends as succeeded rather
than failing on you. The current per-row figure is on the Pricing tab.

A run that finds nothing costs you the run fee and no more.

### FAQ

**Does this return doctors' phone numbers or emails?**
No. Not on any country site, not with any input. See the top of this page. It is a deliberate
design choice, and every row is filtered before it is written.

**Does it return the patient reviews?**
No. You get the star rating and the number of reviews. The review text and the reviewer names stay
where they are.

**Do I need a Doctoralia account or an API key?**
No. It reads the same signed-out pages anyone can open in a browser.

**Which speciality name do I type?**
The one that country's site uses. `dentista` in Spain, Brazil, Italy and Portugal; `stomatolog` in
Poland; `dis-hekimi` in Turkey; `odontologo` in Colombia. Open the site, do the search once, and copy
the two words out of the address bar: `doctoralia.es/dentista/madrid` means speciality `dentista`,
city `madrid`. If you get it slightly wrong the Actor falls back to the site's search.

**Can I get every dentist in a country rather than one city?**
Leave `city` empty and the search runs nationally. Expect a lot of rows, so set `maxItems` to
something you actually want to pay for.

**Can I filter by insurer?**
Yes, where the country site supports it. Put the insurer's slug in `insurance`, for example `adeslas`,
`sanitas`, `dkv-seguros` in Spain. It narrows the listing to professionals Doctoralia lists for that
insurer, and the slug you used is echoed back in `insuranceFilter` on every row.

**Are clinics included?**
By default yes, because Doctoralia mixes them into speciality listings and leaving them out would
quietly lose you results you can see on the page. Set `includeClinics` to false for individuals only.

**How many rows per page?**
Between eighteen and thirty-four depending on the country and how deep you are. Portugal and
Czechia came back with eighteen, Mexico with thirty-four. The Actor pages until it hits your cap or
the listing stops producing anyone new.

**Why did I get fewer rows than maxItems?**
Either the search ran out of professionals, or the listing started repeating. Both are logged. The
cap is a ceiling, not a target, and you are only charged for rows you actually received.

**Does it need a proxy?**
No. It runs straight from its container. If you want it to exit from your own network, put your proxy
servers in `proxyConfiguration` and it will use them.

**What happens if Doctoralia is down or slow?**
Each request has a time limit and a small number of retries on a second browser profile. If a page
still will not load you get an uncharged diagnostic row naming the URL and the status, and the run
ends as succeeded so the rest of your searches still deliver.

# Actor input Schema

## `speciality` (type: `array`):

The speciality as Doctoralia spells it on that country's site, one per line. Spanish sites use "dentista", "psicologo", "pediatra"; Poland uses "stomatolog"; Turkey uses "dis-hekimi". If the exact slug does not exist the Actor falls back to the site's own search, so a close spelling still works.

## `city` (type: `string`):

City to search in, for example "madrid", "sao-paulo", "milano". Leave empty to search the whole country.

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

Which Doctoralia country site to read. Each one is a separate directory with its own practitioners and its own speciality names.

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

Hard cap on rows across every search in this run. You are charged per row returned.

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

Optional. Paste Doctoralia listing pages instead of filling the fields above, for example https://www.doctoralia.es/dentista/madrid or https://www.miodottore.it/dentista/milano. Single profile pages are not supported.

## `insurance` (type: `string`):

Optional. Return only practitioners Doctoralia lists under one insurer, using that insurer's slug from the site, for example "adeslas" or "sanitas" in Spain. Leave empty for everyone.

## `includeClinics` (type: `boolean`):

Doctoralia mixes clinics into most speciality listings. Leave this on to get both; turn it off for individual practitioners only.

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

Optional. Leave this alone unless you need the run to go out through a particular network. Your own proxy servers are used exactly as given.

## Actor input object example

```json
{
  "speciality": [
    "dentista"
  ],
  "city": "madrid",
  "country": "es",
  "maxItems": 50,
  "startUrls": [],
  "includeClinics": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One dataset row per Doctoralia listing card: name, speciality and sub-specialities, the clinic the entry practises at, street, city and region of that practice, coordinates, rating and review count, the consultation service and its published price, online-booking and online-consultation flags, and the profile URL. Rows carry no phone number, no email and no review text. Empty input, an unrecognised URL or a speciality the site does not know writes an uncharged sample or diagnostic row instead.

# 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": [
        "dentista"
    ],
    "city": "madrid",
    "country": "es",
    "maxItems": 50,
    "startUrls": [],
    "insurance": "",
    "includeClinics": true,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/doctoralia-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": ["dentista"],
    "city": "madrid",
    "country": "es",
    "maxItems": 50,
    "startUrls": [],
    "insurance": "",
    "includeClinics": True,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/doctoralia-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": [
    "dentista"
  ],
  "city": "madrid",
  "country": "es",
  "maxItems": 50,
  "startUrls": [],
  "insurance": "",
  "includeClinics": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call dami_studio/doctoralia-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/doctoralia-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/jls2sLswCZMY1koss/builds/9BHHst2Ajh6C6wsu8/openapi.json
