Zocdoc Find Appointment

Site zocdoc.comTask find-appointment-ekztriVersion v1Updated Jul 13, 2026Category healthcare

Search Zocdoc for available appointment slots by specialty + location (+ optional insurance), returning provider name, specialty, distance, next-available date/time, and accepted insurance. Read-only — never books. This skill was captured from a live agent session on zocdoc.com and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

Purpose

Given a medical specialty, a location (ZIP code or city), and optionally an insurance plan, return the set of in-network providers that have the soonest available appointment slots — including provider name, specialty, distance from the search location, the next available date/time, all visible slot times on that date, and the list of accepted insurance plans for that provider-location. Read-only — never click a slot, never reach the booking confirmation page, never submit patient info.

When to Use

  • "Show me dermatologists in Brooklyn 11201 who take Aetna and have appointments this week."
  • "What's the soonest a primary care doctor near 90210 can see me?"
  • A scheduling agent comparing first-available across specialists for a referral.
  • Insurance-network discovery: "Which dentists in ZIP 10001 accept HealthFirst NY plan X?"
  • Any flow that needs slots without booking. Booking is a different skill (zocdoc.com/book-appointment) and requires PHI (name, DOB, insurance card, address) the user must explicitly supply.

Workflow

The Zocdoc consumer site at https://www.zocdoc.com/ is the only viable surface for an agent without Zocdoc developer credentials. The site is DataDome-protected — bare HTTP fetch, residential-proxy fetch, and the internal /wapi/* JSON endpoints all return 403 Please enable JS (verified across multiple URL patterns). The full search-results page renders client-side after the DataDome JS challenge passes, so you need a real browser session with stealth and a residential proxy enabled. The structured data (provider cards, first-available chip, slot grid) is rendered as accessible HTML once the page settles, so a snapshot command reads it reliably once you're past the challenge.

If you have Zocdoc developer OAuth credentials (client_id / client_secret issued via developer.zocdoc.com), prefer the official REST API — see "API path (developer-credentialed only)" at the end of this section. The consumer-site flow below is the default path for a generic agent.

1. Set up the browser call (stealth + residential proxy)

Drive the whole flow with browserless_agent, passing a residential proxy on the call:

// browserless_agent
{
  "proxy": { "proxy": "residential", "proxyCountry": "us" },
  "commands": [/* nav → settle → read, see steps 2–3 */],
}

There is no session to create, export, or release. The session persists across separate calls, keyed by the call's proxy/profile: a follow-up call carrying the same config reconnects to the same session (its DataDome cookie intact), while one that drops or changes the config lands in a different, blank session. Batching the entire flow (navigate → settle → snapshot, and any pagination) inside one call's commands array is the convenient default — it saves round-trips and keeps the DataDome cookie earned on the first navigation. Repeat the proxy arg on every call.

Both stealth and a residential proxy are mandatory. Browserless applies stealth by default; the residential proxy is set via the proxy arg above. A plain (proxy-less) call or a datacenter-proxy call gets a 403 with the DataDome cmsg HTML body (the page-source signature is <script data-cfasync="false" src="https://ct.captcha-delivery.com/i.js">). A solve command is not sufficient on its own — DataDome on Zocdoc presents an invisible JS challenge, not a clickable CAPTCHA, so the captcha-solver never triggers.

2. Construct the search URL directly — skip the homepage

https://www.zocdoc.com/search?
    address={URL-encoded-city-or-zip}
   &search_query={URL-encoded-specialty-display-name}
   &dr_specialty={specialty_id}
   &reason_visit={visit_reason_id}
   &insurance_carrier={carrier_id}     # optional
   &insurance_plan={plan_id}           # optional, pair with insurance_carrier
   &day_filter=AnyDay
   &sort_type=Default
   &visitType=inPersonAndVirtualVisits
   &latitude={float}                    # optional — site geocodes from `address` if absent
   &longitude={float}
   &offset=0

address accepts both "Brooklyn, NY 11201" and bare ZIPs ("10001"). The site geocodes server-side and populates latitude/longitude after the first navigation — you can omit them on initial entry.

dr_specialty and reason_visit are numeric IDs on the consumer site, distinct from the developer-API's string IDs (sp_153 / pc_FRO-...). Map common values from the specialty pivot table below; for unmapped specialties, use the specialty discovery flow (step 3).

insurance_carrier is the carrier ID (e.g. Aetna, HealthFirst NY) and insurance_plan is the specific plan within that carrier. The site treats insurance_carrier=-1&insurance_plan=-1 as "no insurance filter" and insurance_carrier=-2&insurance_plan=-2 as "self-pay / no insurance" — both render different result sets.

Specialty pivot (verified consumer-site values, partial)

Specialty (search_query)dr_specialtyreason_visitExample /search URL fragment
Dentist9812search_query=Dentist&dr_specialty=98&reason_visit=12
Primary Care Doctor (PCP)15375search_query=Primary+Care+Doctor+%28PCP%29&dr_specialty=153&reason_visit=75
Dermatologist106(varies — let site default)search_query=Dermatologist&dr_specialty=106

For other specialties: navigate to the homepage ({ "method": "goto", "params": { "url": "https://www.zocdoc.com/", "waitUntil": "load", "timeout": 45000 } }) and use the specialty typeahead (type into the search box, then click the matching suggestion) — the URL after submit contains both IDs. Cache discovered values; the consumer specialty ID space is undocumented and stable.

3. Open the URL and wait for the slot grid to settle

Chain these into the one browserless_agent call's commands array (goto → settle → snapshot):

[
  {
    "method": "goto",
    "params": { "url": "<search URL>", "waitUntil": "load", "timeout": 45000 }
  },
  { "method": "waitForTimeout", "params": { "time": 3500 } },
  { "method": "snapshot" }
]

The slot widget renders progressively after load fires (verified pattern across Zocdoc's Next.js + React-Query stack). The 3500 ms waitForTimeout covers the 2–3s hydration delay; snapshotting before it returns provider names but slots: [] arrays.

4. Branch on what the snapshot shows

  • Provider cards with Earliest available: <day-of-week, MMM D> + visible time-slot buttons → success path. Each card has:
    • Provider full name + credentials (h3-level heading on each card)
    • Specialty tag (sub-heading)
    • Distance text (X.X mi or X miles away)
    • "Next available" chip
    • 3–6 visible slot times for the next-available date (more behind a "Show more" expander; click the expander only if you need same-day depth)
    • Insurance row: "Accepts: <plan name>" or "In-network with: ..." or "Accepts most plans"
  • "No appointments available" / "No providers found" headersuccess: true, providers: [], reason: "no_results".
  • Top of page shows "Showing results near <DIFFERENT CITY>" → the address parameter was misparsed by Zocdoc's geocoder. Re-issue with a more-specific address string (full street or <city>, <state> <zip>).
  • DataDome interstitial (page title zocdoc.com, body Please enable JS and disable any ad blocker) → this session burned a DataDome cookie. Retry in a fresh session — change the proxy/profile so the call lands in a different session rather than reconnecting to the flagged one.

5. Extract per-provider availability

For each visible provider card on the search-results page, the next-available date and ~3–6 same-day slot buttons are sufficient for this skill's output. Do not click slot-time buttons — that initiates the booking flow, which this skill must not do. If the user requested all slots across a multi-day window, visit the provider's profile page instead:

https://www.zocdoc.com/doctor/{slug}-{provider-id}
https://www.zocdoc.com/dentist/{slug}-{provider-id}      # dental specialties use /dentist/

The profile page's calendar widget shows up to 14 days of slots in a date-paginated grid. Use a click only on the date-navigation arrows (button: Next day / button: Previous day), never on time-slot buttons.

6. Verify before emitting

  • Read the page header's location chip. If it doesn't match the requested location (e.g. requested "Brooklyn" but header says "Manhattan"), the geocoder picked a different neighborhood — flag as location_mismatch rather than silently emit.
  • Read the URL after navigation. Zocdoc occasionally redirects /search? to a specialty-by-city landing page (/dentists/<city-slug>-<code>pm) when no slot data is server-rendered; if the redirect happens, follow it — the landing page has the same provider-card structure.
  • If insurance was specified, confirm each emitted provider's card shows the requested plan in "Accepts" — otherwise mark accepts_specified_insurance: false rather than dropping the provider.

7. No session-release step

There is nothing to release. The session persists across separate calls, keyed by the call's proxy/profile. Batch the full search-and-extract flow (and any profile-page navigation) inside one call's commands array — or repeat the same proxy/profile on follow-up calls — so cookies and the DataDome challenge state persist across steps.

API path (developer-credentialed only)

If the agent has Zocdoc developer credentials, prefer the documented REST API — it returns JSON directly and bypasses DataDome entirely:

  1. POST https://auth.zocdoc.com/oauth/token with grant_type=client_credentials, your client_id, client_secret, and audience=https://api-developer.zocdoc.com/access_token.
  2. GET https://api-developer.zocdoc.com/v1/provider_locations?zip_code=<5-digit>&specialty_id=<sp_NNN>[&insurance_plan_id=<ip_NNNN>][&max_distance_to_patient_mi=<int>] with Authorization: Bearer <token> → list of provider_location_ids + first_availability_date_in_provider_local_time + accepts_patient_insurance per result.
  3. GET https://api-developer.zocdoc.com/v1/provider_locations/availability?provider_location_ids=<comma-separated>&visit_reason_id=<pc_...>&patient_type=new[&start_date_in_provider_local_time=YYYY-MM-DD] → timeslot list per provider_location with ISO-8601 start_time and a booking_url deep-link back to zocdoc.com.

Developer API specialty/visit-reason IDs use string namespaces (sp_153, pc_FRO-18leckytNKtruw5dLR) — distinct from the consumer-site numeric IDs in step 2 above. The reference-data mapping is not public — partners must email partner-devsupport@zocdoc.com to receive it. Insurance plan IDs (ip_NNNN) are obtainable via the public-on-auth GET /v1/insurance_plans endpoint.

The endpoint https://api-developer.zocdoc.com/v1/provider_locations returns 401 Unauthorized to unauthenticated callers (verified 2026-05-18) — confirming the host exists and is OAuth-gated. Do not waste time probing https://api.zocdoc.com/v1/* or https://www.zocdoc.com/api/v1/* — both return 404 (verified); the production API host is api-developer.zocdoc.com.

Site-Specific Gotchas

  • READ-ONLY. Never click a time-slot button or a "Book" CTA — both start the booking flow. Read-only stops at the search-results page or the provider profile's calendar view.
  • DataDome anti-bot is on by default. Bare HTTP fetch, residential-proxy HTTP fetch, and the internal /wapi/* and /v1/* consumer endpoints all return 403 with a <script data-cfasync="false" src="https://ct.captcha-delivery.com/i.js"> body (verified 2026-05-18 across /, /search, /dentists/brooklyn-79621pm, /robots.txt, /wapi/searchResults). You must drive a real browser via browserless_agent with a residential proxy (proxy: { proxy: "residential", proxyCountry: "us" }) plus its default stealth. A solve command does not help — DataDome's challenge here is invisible JS, not a click-CAPTCHA.
  • DataDome exceptions: /api/health/* and unmatched /* URLs are routed to the SEO 404 page (15 KB Zocdoc-branded HTML), which bypasses the DataDome challenge. This is a debugging signal — if your stealth session is mis-configured, fetching /api/health/x will succeed (404 page renders) while fetching /search will 403. Don't use this as a scrape path though — there's no useful data on the 404 page.
  • Two specialty-ID namespaces — don't cross them. The consumer site uses numeric IDs in the URL: dr_specialty=98 (Dentist), dr_specialty=153 (PCP), reason_visit=75 (PCP visit), reason_visit=12 (Dentist visit). The developer API uses string IDs: sp_153, pc_FRO-18leckytNKtruw5dLR. They are not interchangeable — passing sp_153 to the consumer URL produces a malformed search. Map by name, not by number.
  • /dentist/ vs /doctor/. Dental specialties (Dentist, Endodontist, Periodontist, Orthodontist, Pediatric Dentist, Oral Surgeon) use /dentist/{slug}-{id} for profile URLs. Everything else uses /doctor/{slug}-{id}. The /dentists/<city-slug>-<code>pm directory pages (note the trailing pm) are SEO landing pages — the pm is a Zocdoc-internal city-region code, not a meaningful suffix.
  • Slot widget renders 2–3s after load. Insert a waitForTimeout of ~3500 ms after the goto (which uses waitUntil: "load") and before the snapshot command — otherwise you get provider cards with empty slot arrays.
  • Geocoder can override the location. address=Joe%27s+City (or any ambiguous string) gets geocoded to whatever Zocdoc thinks you meant. Always verify the location header chip after navigation matches the user's intent; if not, retry with a fuller address (full street, <city>, <state> <zip>, or raw ZIP).
  • Insurance trio: -1 vs -2 vs explicit IDs. insurance_carrier=-1&insurance_plan=-1 is "no filter — show all providers". insurance_carrier=-2&insurance_plan=-2 is "I'm self-paying, show only providers who accept self-pay". insurance_carrier=<N>&insurance_plan=<N> filters to in-network only. The default if the params are omitted entirely is -1/-1 (no filter).
  • Insurance plan IDs are carrier-scoped. insurance_carrier=350 is HealthFirst NY; insurance_plan=17200 is a specific HealthFirst NY plan. Passing insurance_plan without the matching insurance_carrier parent silently drops the filter. Always send the pair together.
  • first_availability_date_in_provider_local_time is in the provider's timezone, not the user's. A New York patient searching San Francisco providers gets PT-local "earliest available" — convert before emitting if the user expects their own timezone.
  • Provider cards with Earliest available: Today + slot buttons are the success shape. Cards with Earliest available: <date 14+ days out> and no visible slot buttons mean "calendar is open but no near-term slots" — emit but flag as low_availability: true.
  • Same provider, multiple locations. A provider with practices in multiple ZIPs appears as multiple cards (one per provider_location_id). Don't dedupe by provider_id — the user may want a specific location.
  • A flagged session is single-use against DataDome. If a call gets a DataDome 403 mid-flow (cookie expired, signal flagged), do not retry against the same session — issue a call with a different proxy/profile so it lands in a fresh session rather than reconnecting to the flagged one. Fresh stealth + residential-proxy calls are cheap; recovery on a flagged session is not.
  • Don't probe /wapi/searchResults or /api/v1/search. Verified 2026-05-18: both 403 with DataDome regardless of session config. The site does not expose a consumer JSON API.
  • The official API host is api-developer.zocdoc.com, not api.zocdoc.com. api.zocdoc.com/v1/* returns 404 (verified) — this domain serves a non-API stub. Use api-developer.zocdoc.com for all OAuth-authenticated API calls.
  • OAuth audience is required and specific. When requesting a developer token from https://auth.zocdoc.com/oauth/token, you must include "audience": "https://api-developer.zocdoc.com/" in the JSON body — omitting it returns a token that the API rejects with 401.

Expected Output

Three distinct outcome shapes.

Success — providers with availability

{
  "success": true,
  "query": {
    "specialty": "Dentist",
    "specialty_id_consumer": 98,
    "location": "Brooklyn, NY 11201",
    "latitude": 40.6986772,
    "longitude": -73.9859414,
    "insurance_carrier_id": 350,
    "insurance_plan_id": 17200,
    "insurance_display": "HealthFirst (NY) — Essential Plan 1"
  },
  "result_count": 3,
  "providers": [
    {
      "provider_id": "132039",
      "name": "Dr. Beeren Gajjar, DDS",
      "specialty": "Dentist",
      "distance_mi": 0.4,
      "profile_url": "https://www.zocdoc.com/dentist/beeren-gajjar-dds-132039",
      "next_available_date": "2026-05-19",
      "next_available_date_provider_local": "2026-05-19",
      "timezone": "America/New_York",
      "slots": [
        "2026-05-19T09:00:00-04:00",
        "2026-05-19T09:30:00-04:00",
        "2026-05-19T10:00:00-04:00",
        "2026-05-19T14:15:00-04:00"
      ],
      "accepted_insurance": ["HealthFirst (NY)", "Aetna", "Cigna", "Delta Dental"],
      "accepts_specified_insurance": true,
      "low_availability": false
    }
  ]
}

Success — no providers match (empty result set)

{
  "success": true,
  "query": { "...same shape..." },
  "result_count": 0,
  "providers": [],
  "reason": "no_results",
  "note": "Either no providers in-network within search radius, or specialty/insurance combination has no in-network options."
}

Failure — geocoder rerouted the location

{
  "success": false,
  "reason": "location_mismatch",
  "requested_location": "Brooklyn",
  "page_header_location": "Manhattan, NY",
  "suggestion": "Retry with a fuller address: '<city>, <state> <zip>' or a 5-digit ZIP."
}

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=zocdoc.com&task=find-appointment-ekztri