Install

Search doctors on Doctolib by specialty and city.

Searches Doctolib Germany for practitioners and practices by specialty and city, for public (GKV) or private (PKV) insurance: name, specialty, address, link, whether new patients are accepted, video consultation, and the ids (visit motive, agendas, practice) that doctolib.de/get-slots needs.

No reports yetRead-only, no login
Site
Doctolib, Germany
Version
v1
Runs, last 7 days
No reports yet
sha256
8cb4fcf80ceb…

Requests

sent from your browser, in order
  1. 1POST/patient-health-search/api/v1/autocompleteFree text to Doctolib's specialty slug.json body
  2. 2GET/search?keyword&locationThe search page embeds the place object the search API needs (window.place = {...}; in an inline script). The single-item foreach only computes the city slug (umlauts transliterated).repeats, max 1
  3. 3POST/patient-health-search/api/v1/hcp/search?pagePractitioner search. Answers 206. The full place object is spliced in as raw JSON (a partial place silently returns nationwide results). Single-item foreach cuts the object out of the script text.text bodyrepeats, max 1

Parameters

from your agent
citystringrequiredCity, e.g. 'Berlin', 'München', 'Frankfurt am Main'
insurance_sectorenumpublic = gesetzlich (GKV), private = privat (PKV) "public" | "private" = "public"
pageinteger0-based, 20 per page = 0
specialtystringrequiredSpecialty in German, e.g. 'Hautarzt', 'Hausarzt', 'Zahnarzt', 'Kinderarzt'

Returns

to your agent
citystring
insurance_sectorstring
pageinteger
practitionersobject[]accepts_new_patients, address, agenda_ids, city, kind, languages, name, practice_id, profile_id, specialty, url, video, visit_motive_id, visit_motive_name, zip
specialtystringDoctolib specialty actually used
totalinteger

Where it can send

  • www.doctolib.de

Anything else is blocked by the extension.

Reads from your session

Nothing. Your browser attaches the site's cookies by itself.

Used only in requests to the addresses on the left. Never sent to wield.run or your agent.

Checks

a run passes when
  • output.total > `20`
  • length(output.practitioners) > `5`
  • length(output.practitioners[?city == 'Berlin']) > `10`
  • starts_with(output.practitioners[0].url, 'https://www.doctolib.de/')
  • length(output.practitioners[?visit_motive_id != `null`]) > `0`

Raw recipe

Served as canonical JSON from /api/v1/recipes/doctolib.de/search-practitioners, so its sha256 matches the ETag and the index.

Check it yourself
curl -s https://wield.run/api/v1/recipes/doctolib.de/search-practitioners | shasum -a 256
# 8cb4fcf80ceb1d0f42db1cb06d4a5b782adef145dc97eaf951915cd7229e6c1a
doctolib.de/search-practitioners.json, v1
{
  "description": "Searches Doctolib Germany for practitioners and practices by specialty and city, for public (GKV) or private (PKV) insurance: name, specialty, address, link, whether new patients are accepted, video consultation, and the ids (visit motive, agendas, practice) that doctolib.de/get-slots needs.",
  "effect": "read",
  "id": "doctolib.de/search-practitioners",
  "keywords": [
    "doctolib",
    "arzt",
    "arzttermin",
    "termin buchen",
    "hautarzt",
    "hausarzt",
    "zahnarzt",
    "facharzt",
    "praxis",
    "doctor",
    "doctor appointment",
    "book appointment",
    "medical"
  ],
  "output": {
    "schema": {
      "properties": {
        "city": {
          "type": "string"
        },
        "insurance_sector": {
          "type": "string"
        },
        "page": {
          "type": "integer"
        },
        "practitioners": {
          "items": {
            "properties": {
              "accepts_new_patients": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "agenda_ids": {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "kind": {
                "enum": [
                  "practitioner",
                  "practice"
                ]
              },
              "languages": {
                "type": "array"
              },
              "name": {
                "type": "string"
              },
              "practice_id": {
                "type": [
                  "integer",
                  "string",
                  "null"
                ]
              },
              "profile_id": {
                "type": [
                  "integer",
                  "string",
                  "null"
                ]
              },
              "specialty": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "url": {
                "format": "uri",
                "type": "string"
              },
              "video": {
                "type": "boolean"
              },
              "visit_motive_id": {
                "description": "For get-slots; null when no online booking for this specialty",
                "type": [
                  "integer",
                  "null"
                ]
              },
              "visit_motive_name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "zip": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "type": "object"
          },
          "type": "array"
        },
        "specialty": {
          "description": "Doctolib specialty actually used",
          "type": "string"
        },
        "total": {
          "type": "integer"
        }
      },
      "required": [
        "total",
        "practitioners"
      ],
      "type": "object"
    },
    "value": "{specialty: steps.kw.name, city: params.city, insurance_sector: params.insurance_sector, total: steps.search[0].total, page: params.page, practitioners: steps.search[0].items}"
  },
  "params": {
    "additionalProperties": false,
    "properties": {
      "city": {
        "description": "City, e.g. 'Berlin', 'München', 'Frankfurt am Main'",
        "maxLength": 60,
        "minLength": 2,
        "type": "string"
      },
      "insurance_sector": {
        "default": "public",
        "description": "public = gesetzlich (GKV), private = privat (PKV)",
        "enum": [
          "public",
          "private"
        ],
        "type": "string"
      },
      "page": {
        "default": 0,
        "description": "0-based, 20 per page",
        "maximum": 20,
        "minimum": 0,
        "type": "integer"
      },
      "specialty": {
        "description": "Specialty in German, e.g. 'Hautarzt', 'Hausarzt', 'Zahnarzt', 'Kinderarzt'",
        "maxLength": 60,
        "minLength": 2,
        "type": "string"
      }
    },
    "required": [
      "specialty",
      "city"
    ],
    "type": "object"
  },
  "provenance": {
    "method": "browser-inspect",
    "notes": "spikes/doctolib.md. Search API (Spring service) needs the complete window.place object from the SSR search page (~1.6 MB); a partial place gives nationwide results without error, so it is cut out of the inline script (ends with '};window.searchType') and sent as raw JSON in a text body. Unknown specialty slugs make /search redirect to / (the extension reports a redirect error). No CSRF, cookies or custom headers for reads; Cloudflare in front did not challenge. Status 206 is success.",
    "recorded_at": "2026-11-07T12:12:00+01:00",
    "recorded_by": "C4"
  },
  "requires_login": false,
  "schema": "https://wield.run/schema/recipe/v1.json",
  "site": {
    "country": "DE",
    "id": "doctolib.de",
    "language": "de",
    "login_url": "https://www.doctolib.de/auth/patient_de",
    "name": "Doctolib",
    "origin": "https://www.doctolib.de",
    "origins": [
      "https://www.doctolib.de"
    ]
  },
  "steps": [
    {
      "description": "Free text to Doctolib's specialty slug.",
      "expect": {
        "assert": "length(body.searchEntries[?kind=='SPECIALITY']) > `0`",
        "content_type": "json",
        "status": [
          200
        ]
      },
      "extract": {
        "json": {
          "name": "searchEntries[?kind=='SPECIALITY'] | [0].name",
          "slug": "searchEntries[?kind=='SPECIALITY'] | [0].slug"
        }
      },
      "id": "kw",
      "request": {
        "body": {
          "type": "json",
          "value": {
            "query": "{{params.specialty | lower}}"
          }
        },
        "headers": {
          "Accept": "application/json",
          "Content-Type": "application/json"
        },
        "method": "POST",
        "url": "/patient-health-search/api/v1/autocomplete"
      }
    },
    {
      "description": "The search page embeds the place object the search API needs (window.place = {...}; in an inline script). The single-item foreach only computes the city slug (umlauts transliterated).",
      "expect": {
        "assert": "contains(body, 'window.place = {')",
        "content_type": "html",
        "status": [
          200
        ]
      },
      "extract": {
        "html": {
          "scripts": {
            "all": true,
            "selector": "script:not([src])"
          }
        }
      },
      "foreach": {
        "as": "loc",
        "in": "[{slug: lower(replace(replace(replace(replace(replace(replace(replace(replace(trim(params.city), 'ü', 'ue'), 'Ü', 'ue'), 'ö', 'oe'), 'Ö', 'oe'), 'ä', 'ae'), 'Ä', 'ae'), 'ß', 'ss'), ' ', '-'))}]",
        "max": 1
      },
      "id": "place",
      "parse": "html",
      "request": {
        "headers": {
          "Accept": "text/html"
        },
        "method": "GET",
        "query": {
          "keyword": "{{steps.kw.slug}}",
          "location": "{{loc.slug}}"
        },
        "url": "/search"
      }
    },
    {
      "description": "Practitioner search. Answers 206. The full place object is spliced in as raw JSON (a partial place silently returns nationwide results). Single-item foreach cuts the object out of the script text.",
      "expect": {
        "assert": "body.total != null",
        "content_type": "json",
        "status": [
          200,
          206
        ]
      },
      "extract": {
        "json": {
          "items": "healthcareProviders[].{name: join(' ', [title || '', firstName || '', name][?@ != '']), kind: type == 'ORGANIZATION' && 'practice' || 'practitioner', specialty: speciality.name, address: location.address, zip: location.zipcode, city: location.city, url: join('', ['https://www.doctolib.de', link]), profile_id: references.id, practice_id: references.practiceId, visit_motive_id: matchedVisitMotive.visitMotiveId, visit_motive_name: matchedVisitMotive.name, agenda_ids: matchedVisitMotive.agendaIds || onlineBooking.agendaIds || `[]`, accepts_new_patients: matchedVisitMotive.allowNewPatients, video: onlineBooking.telehealth == `true`, languages: languages || `[]`}",
          "total": "total"
        }
      },
      "foreach": {
        "as": "p",
        "in": "[{place: join('', [split(split(steps.place[0].scripts[?contains(@, 'window.place = {')] | [0], 'window.place = ')[1], '};window.')[0], '}'])}]",
        "max": 1
      },
      "id": "search",
      "request": {
        "body": {
          "type": "text",
          "value": "{\"keyword\":\"{{steps.kw.slug}}\",\"location\":{\"place\":{{p.place}}},\"filters\":{\"insuranceSector\":\"{{params.insurance_sector}}\"}}"
        },
        "headers": {
          "Accept": "application/json",
          "Content-Type": "application/json"
        },
        "method": "POST",
        "query": {
          "page": "{{params.page}}"
        },
        "url": "/patient-health-search/api/v1/hcp/search"
      }
    }
  ],
  "task": "Search doctors on Doctolib by specialty and city",
  "tos": {
    "notes": "Doctolib's terms prohibit automated access and scraping. One user-initiated search at a time from the user's own browser (3 requests). Health-related searches are sensitive: results stay on the user's device.",
    "reviewed": false
  },
  "verify": {
    "checks": [
      "output.total > `20`",
      "length(output.practitioners) > `5`",
      "length(output.practitioners[?city == 'Berlin']) > `10`",
      "starts_with(output.practitioners[0].url, 'https://www.doctolib.de/')",
      "length(output.practitioners[?visit_motive_id != `null`]) > `0`"
    ],
    "example_params": {
      "city": "Berlin",
      "specialty": "Hautarzt"
    },
    "max_ms": 8000
  },
  "version": 1
}