Vaccines.gov Find Appointment
Purpose
Given a ZIP code, return nearby pharmacies that administer vaccines — name, address, phone, website, rating, and a per-chain scheduler deep-link — and forward booking to each chain's own scheduler. Vaccines.gov no longer surfaces vaccine inventory, vaccine-type filters, date windows, or appointment slot times (the page banner reads "The functionality of this website may be impacted while it is being updated" and the footer reads "Users should contact the pharmacy for vaccine availability and appointments"). The skill returns the directory portion vaccines.gov provides, plus brand-specific scheduler URLs for the three requested chains. Read-only — never books.
When to Use
- "What pharmacies near {zip} can administer a vaccine?" — vaccines.gov + Google Places is the canonical answer.
- A scheduling agent that needs to discover CVS / Walgreens / Costco locations near a user, then hand off to each chain's own slot-finder skill.
- Any flow that previously called the deprecated VaccineFinder API (
vaccinefinder.org/findvax-direct-api.castlight.com) and needs a current-day replacement.
Do not use this skill if you need real appointment slot times. Slot data is not on vaccines.gov — it lives on each pharmacy chain's scheduler. After this skill returns the per-chain scheduler URL, dispatch to a chain-specific skill: cvs.com/schedule-vaccine, walgreens.com/schedule-vaccine, etc. (Costco does not expose an online scheduler — the user must phone the store.)
Workflow
The optimal path is hybrid: call the same Google Places searchText endpoint that vaccines.gov calls (one HTTP POST, no auth beyond a publicly-embedded API key, ~0.5s), filter results to the requested chains, then construct per-chain scheduler URLs. The browser path is a fallback when the API key has been rotated or rate-limited; it pays ~50× the cost and still returns the same data (vaccines.gov's frontend is a thin React wrapper over the exact API call below).
1. Geocode the ZIP → lat/lng bounding box
Vaccines.gov uses a getBoundsOfDistance({latitude, longitude}, radiusMeters) helper to convert a ZIP centroid + radius into a {low, high} rectangle. Any geocoding source works; the simplest is the Google Geocoding API or a static USPS ZCTA centroid table. Default radius in the vaccines.gov UI is 8047 meters (~5 miles); raise to 16093 m (~10 mi) or 40234 m (~25 mi) for sparse rural ZIPs.
Example for ZIP 10001 (NYC, centroid 40.7506 / -74.0014, 5-mile radius):
{ "low": { "latitude": 40.6783, "longitude": -74.0966 },
"high": { "latitude": 40.8228, "longitude": -73.9062 } }
2. Call Google Places searchText
POST https://places.googleapis.com/v1/places:searchText
Content-Type: application/json
X-Goog-Api-Key: AIzaSyCVvG6ZXIlxy76PCbqGnf9cP37XtA3HE-M
X-Goog-FieldMask: places.id,places.displayName.text,places.nationalPhoneNumber,
places.formattedAddress,places.addressComponents,places.location,
places.websiteUri,places.rating,places.primaryType,
places.regularOpeningHours.openNow,nextPageToken
{
"textQuery": "pharmacy zip code 10001",
"languageCode": "en",
"includedType": "pharmacy",
"rankPreference": "RELEVANCE",
"strictTypeFiltering": true,
"regionCode": "US",
"pageSize": 20,
"locationRestriction": { "rectangle": { "low": {...}, "high": {...} } }
}
The key, request shape, and field mask are exactly what vaccines.gov ships in /_next/static/chunks/app/[locale]/results/page-*.js (verified 2026-05-18). Paginate with pageToken from nextPageToken — vaccines.gov caps at 3 pages × 20 results = 50 max. Iterate pageSize=20 × up to c<3 page tokens, accumulating until o.length >= 50 (the literal cap from the bundle).
3. Filter to the three target chains
Match places.displayName.text (or places.addressComponents) against:
- CVS —
displayName.textcontainsCVS(case-insensitive). Also matchesCVS Pharmacy,CVS y más,Target(CVS-inside-Target — only ifformattedAddresscontains "CVS pharmacy"; otherwise skip). - Walgreens —
displayName.textcontainsWalgreensorDuane Reade(Duane Reade is a NYC-region Walgreens subsidiary and shares the same scheduler). - Costco —
displayName.textcontainsCostco. Pharmacy is in-warehouse, so only Costco warehouses with a pharmacy department return useful results — Google Places does not distinguish, so verify via thewebsiteUri(membership warehouses link tocostco.com/warehouse-locations/...).
Chains not in scope but commonly appearing in results: Walmart, Rite Aid (most stores closed in 2023-2024 chapter 11), Sam's Club, supermarket pharmacies (Kroger, Publix, H-E-B). Drop them.
4. Construct per-chain scheduler deep-link
Vaccines.gov itself emits only a Google Maps directions URL (https://www.google.com/maps/dir/?api=1&destination={addr}&destination_place_id={places.id}) — useful for navigation, useless for booking. For booking, build a chain-specific URL:
| Chain | Scheduler URL pattern | Notes |
|---|---|---|
| CVS | https://www.cvs.com/vaccine/intake/store/ | Real-time slots after store + vaccine-type selection. No useful ZIP/store deep-link query param — the page hydrates client-side then prompts. Must hand off to the cvs.com/schedule-vaccine skill with {zip, vaccine_type, date_window}. |
| Walgreens | https://www.walgreens.com/findcare/vaccination/our-services | Requires Walgreens.com sign-in before slots are shown — anonymous fetch returns the sign-in wall (page title literally is "Sign In or Register to Get Started Using Walgreens.com"). Duane Reade uses the same scheduler. |
| Costco | https://www.costco.com/pharmacy.html | No online scheduler. Page is informational only. Return the pharmacy's phone number (places.nationalPhoneNumber) and instruct the user to call. |
5. Assemble output
Return one record per matching pharmacy with the fields below — leave slots, next_available, and booking_url_with_slot explicitly null and set slot_data_available: false. The next agent in the chain (e.g. cvs.com/schedule-vaccine) fills those in.
Browser fallback
If the Google Places key is rotated or returns 403, drive vaccines.gov in a session with --verified --proxies (the site sits behind Akamai — akamai_visit_id cookie is set on first response, and bare sessions get challenged):
browse open https://www.vaccines.gov/en/ --remote --session "$sid"browse fill <zip-input ref> "10001"andbrowse press Enterbrowse wait networkidle --timeout 20000(Places call resolves in ~500ms but the React render lags ~3s)browse get markdown body— pharmacy cards render as<h3>{name}</h3>+<address>{address}</address>+<a href="https://www.google.com/maps/dir/?api=1...">Directions</a>+<a href="{websiteUri}">Website</a>- Filter + emit as above
Network capture (browse network on) will record the exact places.googleapis.com/v1/places:searchText request — useful for reconfirming the API key if the bundle hash changes.
Site-Specific Gotchas
- Vaccines.gov is now a pharmacy directory, not a vaccine finder. As of mid-2024 the site shed its appointment-finder backend (formerly powered by Castlight VaccineFinder) and re-launched as a Google-Places-backed pharmacy locator. The home page literally banners: "The functionality of this website may be impacted while it is being updated." Do not promise users slot times from this surface.
- No vaccine-type input. The UI accepts only ZIP. The request body sent to Google has
textQuery: "pharmacy zip code {zip}"— vaccine type is not in the query string andincludedTypeis the constant"pharmacy". Filtering by vaccine type is the consuming agent's job, downstream of this skill. - No date input either. Same reason. Slot windows are a downstream concern.
- Hard-coded Google API key in the bundle.
AIzaSyCVvG6ZXIlxy76PCbqGnf9cP37XtA3HE-Mis shipped in plaintext in/_next/static/chunks/app/[locale]/results/page-*.js(bundle hash may rotate; if your stored key 403s, re-fetch the results-page chunk and grepAIza). Restrictions on the key are referer-based — sending the request from an arbitrary IP without aReferer: https://www.vaccines.gov/header may be rejected; mirror the browser referer when calling directly. - 50-result cap. The vaccines.gov UI iterates page-tokens at most 3× and stops when accumulated results reach 50 (
o.length < 50 && c < 3in the bundle). To exceed 50, tile the bounding box into sub-rectangles and call separately — Google Places will not return more from a single rectangle even with furtherpageTokenchasing. includedType: "pharmacy"+strictTypeFiltering: trueexcludes urgent-care clinics, doctor offices, and grocery stores that also carry pharmacies. To capture supermarket-embedded pharmacies (Kroger, Publix, Wegmans), relax tostrictTypeFiltering: falseand post-filter onaddressComponentsordisplayName.text.- Akamai gate, but soft. The
www.vaccines.govHTML response setsakamai_visit_id/Akamai-Request-IDcookies. Bare-session fetches still succeed for HTML and/_next/static/*chunks (verified 2026-05-18). Stealth is not required to read the page or hit the Places API; reserve--verified --proxiesfor the browser fallback only. - Page is fully client-rendered. Server-rendered HTML for both
/en/and/en/results/?zip=...contains only a title and shell — no pharmacy data is in the initial HTML.browse cloud fetchof the results URL will not return any pharmacy info; you must either (a) call Google Places directly (recommended), or (b) drive the browser to allow client-side hydration. - Costco is a dead-end for booking.
costco.com/pharmacy.htmlhas no online scheduler. Return the phone number; do not waste turns trying to find slots. - Walgreens requires sign-in before slots. A direct fetch of
walgreens.com/findcare/vaccination/our-services(verified 2026-05-18) lands on a sign-in wall. The downstreamwalgreens.com/schedule-vaccineskill must carry credentials or a persisted browser context. - Don't trust the inner-agent hallucination of "VTrckS / VAMS / CDC provider registry". Models trained on pre-2024 data describe vaccines.gov as a CDC-backed inventory federation. That is no longer true. The actual data plane is one Google Places
searchTextcall, period. - No appointment slot data exists on vaccines.gov in any surface — not via the HTML, not via any internal API, not via the Places response. Confirmed by JS-bundle inspection: the only outbound API call from the results page is to
places.googleapis.com, and the field mask requested does not include any vaccine, schedule, or inventory field. Future agents: do not waste iterations trying to discover a hidden slot API. There is none.
Expected Output
{
"zip": "10001",
"radius_miles": 5,
"slot_data_available": false,
"next_step": "Dispatch each pharmacy to its chain's scheduler skill — vaccines.gov does not surface slot times.",
"pharmacies": [
{
"place_id": "ChIJxxxxxxxxxxxxxxxxxxxxxxxx",
"chain": "cvs",
"name": "CVS Pharmacy",
"address": "298 8th Ave, New York, NY 10001, USA",
"phone": "(212) 555-1212",
"website": "https://www.cvs.com/store-locator/cvs-pharmacy-address/...",
"rating": 3.4,
"open_now": true,
"location": { "latitude": 40.7466, "longitude": -73.9986 },
"directions_url": "https://www.google.com/maps/dir/?api=1&destination=298+8th+Ave+New+York+NY+10001&destination_place_id=ChIJxxxxxxxxxxxxxxxxxxxxxxxx",
"scheduler_url": "https://www.cvs.com/vaccine/intake/store/",
"scheduler_method": "browser",
"scheduler_notes": "Hand off to cvs.com/schedule-vaccine skill with {zip, vaccine_type, date_window}.",
"slots": null,
"next_available": null
},
{
"place_id": "ChIJyyyyyyyyyyyyyyyyyyyyyyyy",
"chain": "walgreens",
"name": "Duane Reade",
"address": "401 W 14th St, New York, NY 10014, USA",
"phone": "(212) 555-3434",
"website": "https://www.walgreens.com/locator/walgreens-...",
"rating": 3.1,
"open_now": true,
"location": { "latitude": 40.7398, "longitude": -74.0049 },
"directions_url": "https://www.google.com/maps/dir/?api=1&destination=...",
"scheduler_url": "https://www.walgreens.com/findcare/vaccination/our-services",
"scheduler_method": "browser",
"scheduler_notes": "Walgreens.com sign-in required before slots are shown. Duane Reade uses the Walgreens scheduler.",
"slots": null,
"next_available": null
},
{
"place_id": "ChIJzzzzzzzzzzzzzzzzzzzzzzzz",
"chain": "costco",
"name": "Costco Pharmacy",
"address": "517 E 117th St, New York, NY 10035, USA",
"phone": "(212) 555-9090",
"website": "https://www.costco.com/warehouse-locations/...",
"rating": 4.2,
"open_now": false,
"location": { "latitude": 40.7995, "longitude": -73.9341 },
"directions_url": "https://www.google.com/maps/dir/?api=1&destination=...",
"scheduler_url": "https://www.costco.com/pharmacy.html",
"scheduler_method": "phone_only",
"scheduler_notes": "Costco does not expose an online scheduler. Call the pharmacy's phone number to book.",
"slots": null,
"next_available": null
}
]
}
If no matching pharmacies are returned for the requested chains (e.g. rural ZIP, no CVS / Walgreens / Costco within radius), respond with an empty pharmacies array and reason: "no_target_chain_in_radius". If the Places API returns a non-200 (key rotated, quota exhausted), fall back to the browser path or surface reason: "places_api_error", "status": <code>.
{ "zip": "59718", "radius_miles": 25, "slot_data_available": false, "pharmacies": [], "reason": "no_target_chain_in_radius" }
{ "zip": "10001", "slot_data_available": false, "reason": "places_api_error", "status": 403, "hint": "Refetch /_next/static/chunks/app/[locale]/results/page-*.js and re-scrape the AIza... key." }