Extract complete DoorDash restaurant and retail menus

Site doordash.comTask extract-doordash-menuVersion v8Updated Sep 16, 2026Category food-delivery

Resolves a location-aware DoorDash search through restaurant or grocery/convenience taxonomy, then extracts visible catalogue items, seller metadata, prices, ratings, descriptions, and availability. This skill was captured from a live agent session on doordash.com and is published here as a reusable recipe for agents.

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.

Extracts complete DoorDash restaurant, grocery, and convenience catalogues after resolving a location-aware result. Returns seller metadata, categories, every loaded product, prices, descriptions, ratings, IDs, and explicit nulls for unavailable fields.

Use Cases

  • Retrieve every item from a DoorDash restaurant menu.
  • Browse grocery or convenience taxonomy results instead of restaurant search.
  • Extract lazy-loaded retail products including sold-out items.
  • Resolve a seller from a location-aware search and preserve seller metadata.
  • Apply optional rating, price-range, ETA, and result-rank controls.

Automation Flow

  1. Save {location} in the DoorDash BQL session: try input#HomeAddressAutocomplete; if needed use button[data-testid="addressTextButton"], enter the address in input[placeholder="Address"], select a suggestion within #addressAutocompleteDropdown, press Continue if shown, then press Save. For the manual form use input[autocomplete="street-address"], input[autocomplete="address-level2"], input[autocomplete="postal-code"], the required state select, and button[data-testid="manual-address-submit"] or button[data-testid="manual-address-confirmation-btn"]. Verify the exact address.
  2. Goto https://www.doordash.com/search/store/{encodeURIComponent(query + " " + city + " " + state)}?event_type=search&filterQuery-price_range=%24&filterQuery-price_range=%24%24&filterQuery-price_range=%24%24%24&filterQuery-price_range=%24%24%24%24&filterQuery-star_rating=4.5. For grocery/convenience browsing, add &filterQuery-vertical_ids=3 when supported, then use the visible Grocery taxonomy and its Convenience link/category.
  3. Evaluate visible unique store links. For restaurant results, use a[href^="/store/"]; for retail results, retain links under the Grocery/Convenience taxonomy and read the complete href. Select the {result-rank}th matching seller by visible name and address; never infer an opaque ID. Goto the selected href with pickup=false and verify the seller and location.
  4. On a convenience or grocery page, scroll through the page until all lazy sections are loaded, then run this evaluator. On a restaurant page, run it without scrolling if JSON-LD is present:
(()=>{const clean=x=>{const s=String(x??'').replace(/\s+/g,' ').trim();return s||null},num=x=>{const m=String(x??'').replace(/,/g,'').match(/-?\d+(?:\.\d+)?/);return m?Number(m[0]):null},body=clean(document.body.innerText)||'',docs=[...document.querySelectorAll('script[type="application/ld+json"]')].map(s=>{try{return JSON.parse(s.textContent)}catch{return null}}).filter(Boolean),menu=docs.find(x=>x&&x['@type']==='Menu'),rest=docs.find(x=>x&&x['@type']==='Restaurant')||{};if(menu){const sections=(menu.hasMenuSection||[]).flat(Infinity).filter(Boolean),address=rest.address||{},categories=sections.map(sec=>({name:clean(sec.name)||'Uncategorized',items:(sec.hasMenuItem||[]).map(it=>{const offer=it.offers||{},price=clean(offer.price??it.price);return{id:clean(it.identifier??it.sku??it.productID),name:clean(it.name),price,price_float:num(price),currency:clean(offer.priceCurrency),description:clean(it.description),rating:null,rating_count:null,available:null,image_url:clean(it.image)}})}));return{success:true,seller:{name:clean(rest.name)||clean(document.querySelector('h1')?.innerText),address:[address.streetAddress,address.addressLocality,address.addressRegion,address.postalCode].filter(Boolean).join(', ')||null,rating:rest.aggregateRating?.ratingValue==null?null:Number(rest.aggregateRating.ratingValue),ratingCount:rest.aggregateRating?.ratingCount||rest.aggregateRating?.reviewCount||null,canonicalUrl:document.querySelector('link[rel="canonical"]')?.href||location.href},categoryCount:categories.length,itemCount:categories.reduce((n,c)=>n+c.items.length,0),menu:categories,source:'DoorDash JSON-LD Menu'}}const heads=[...document.querySelectorAll('h2,h3')],cards=[...document.querySelectorAll('[data-testid="LegoFlexibleItemSquareContainer"]')],categories={};for(const card of cards){const text=clean(card.innerText),pm=text.match(/^\$\s*(\d[\d,]*)(?:\s+|\.)(\d{2})(?=\s|$)/),rm=text.match(/\b([0-5](?:\.\d)?)\s*\((\d+\+)\)/),prev=heads.filter(h=>(h.compareDocumentPosition(card)&Node.DOCUMENT_POSITION_FOLLOWING)!==0).pop(),category=clean(prev?.innerText)||'Uncategorized',name=text.replace(/^\$\s*\d[\d,]*(?:\s+|\.)\d{2}(?:\s+|$)/,'').replace(/\s+(?:Many in stock|Likely out of stock|Out of stock|HSA\/FSA Eligible|[0-5](?:\.\d)? \(\d+\+\))/g,'').trim();(categories[category]||(categories[category]=[])).push({id:card.getAttribute('data-item-id'),name,price:pm?`$${pm[1]}.${pm[2]}`:null,price_float:pm?Number(`${pm[1].replace(/,/g,'')}.${pm[2]}`):null,currency:pm?'USD':null,description:null,rating:rm?Number(rm[1]):null,rating_count:rm?rm[2]:null,available:!/(out of stock|likely out of stock)/i.test(text),image_url:card.querySelector('img')?.src||null,raw:text})}const sellerText=[...document.querySelectorAll('h1')].map(e=>clean(e.innerText)).find(Boolean)||null;return{success:true,seller:{name:sellerText,address:null,rating:null,ratingCount:null,canonicalUrl:document.querySelector('link[rel="canonical"]')?.href||location.href,visibleMetadata:body.slice(0,1200)},categoryCount:Object.keys(categories).length,itemCount:cards.length,menu:Object.entries(categories).map(([name,items])=>({name,items})),source:'DoorDash visible retail cards'}})()

Params

Paramwhat it doesexample value
{query}Cuisine, dietary, product, category, or restaurant search termramen
{location}Exact delivery address saved in DoorDash location state3428 San Bernardino Dr, Delray Beach, FL 33445
{city}City appended to the search URLDelray Beach
{state}State appended to the search URLFL
{result-rank}One-based matching seller ordinal1
filterQuery-vertical_ids=3Optional grocery/retail vertical modifier3
filterQuery-star_rating=4.5Optional Over 4.5 rating filter4.5
filterQuery-price_range repeated four timesOptional all-tier price range$, $$, $$$, $$$$
filterQuery-eta=trueOptional Fastest-delivery filtertrue
{store-id}Opaque ID read from the selected seller href1113201
pickup=falseKeeps the seller in delivery modefalse
residential proxySession type used for DoorDashresidential

Possible Friction Points

TriggerAction
Address autocomplete rejects or does not persist the locationSelect a suggestion, complete the manual form if shown, confirm the address, then reload the filtered search URL.
Search results mix restaurants with grocery or convenience sellersUse the Grocery taxonomy, select Convenience when present, and only then choose the seller link.
Retail catalogue loads additional sections during scrollingScroll through the page before evaluating; deduplicate by each card's data-item-id if repeated.
A product is marked Out of stock or Likely out of stockKeep it in the output with available:false; do not add it to a cart.
Convenience cards have no descriptions or seller address in the DOMReturn those fields as null and retain raw card text and visible metadata.
Search results contain multiple sellersMatch visible name and address, select the requested one-based ordinal, and read its complete href; never guess the ID.
JSON-LD menu is absent or has no sectionsUse the visible retail-card branch after scrolling through all lazy-loaded sections.
Store page resolves to another cityReturn to the location-aware search and select a result matching {location}.
Highest Rated is absent and only Over 4.5 appearsKeep filterQuery-star_rating=4.5 and report the applied control as Over 4.5.
Numeric minimum or maximum price controls are unavailableSelect all four DoorDash price tiers and record them as the requested full range.
Search or store page shows a persistent access wallSolve it in BQL; if it persists, restart with a residential proxy and retry the same URL.
DoorDash exposes Fastest instead of an under-30-minute labelRecord filterQuery-eta=true as Fastest and report the visible control name.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=doordash.com&task=extract-doordash-menu