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
- Save
{location}in the DoorDash BQL session: tryinput#HomeAddressAutocomplete; if needed usebutton[data-testid="addressTextButton"], enter the address ininput[placeholder="Address"], select a suggestion within#addressAutocompleteDropdown, press Continue if shown, then press Save. For the manual form useinput[autocomplete="street-address"],input[autocomplete="address-level2"],input[autocomplete="postal-code"], the required state select, andbutton[data-testid="manual-address-submit"]orbutton[data-testid="manual-address-confirmation-btn"]. Verify the exact address. - 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=3when supported, then use the visible Grocery taxonomy and its Convenience link/category. - 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 withpickup=falseand verify the seller and location. - 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
| Param | what it does | example value |
|---|---|---|
{query} | Cuisine, dietary, product, category, or restaurant search term | ramen |
{location} | Exact delivery address saved in DoorDash location state | 3428 San Bernardino Dr, Delray Beach, FL 33445 |
{city} | City appended to the search URL | Delray Beach |
{state} | State appended to the search URL | FL |
{result-rank} | One-based matching seller ordinal | 1 |
filterQuery-vertical_ids=3 | Optional grocery/retail vertical modifier | 3 |
filterQuery-star_rating=4.5 | Optional Over 4.5 rating filter | 4.5 |
filterQuery-price_range repeated four times | Optional all-tier price range | $, $$, $$$, $$$$ |
filterQuery-eta=true | Optional Fastest-delivery filter | true |
{store-id} | Opaque ID read from the selected seller href | 1113201 |
pickup=false | Keeps the seller in delivery mode | false |
residential proxy | Session type used for DoorDash | residential |
Possible Friction Points
| Trigger | Action |
|---|---|
| Address autocomplete rejects or does not persist the location | Select 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 sellers | Use the Grocery taxonomy, select Convenience when present, and only then choose the seller link. |
| Retail catalogue loads additional sections during scrolling | Scroll 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 stock | Keep it in the output with available:false; do not add it to a cart. |
| Convenience cards have no descriptions or seller address in the DOM | Return those fields as null and retain raw card text and visible metadata. |
| Search results contain multiple sellers | Match 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 sections | Use the visible retail-card branch after scrolling through all lazy-loaded sections. |
| Store page resolves to another city | Return to the location-aware search and select a result matching {location}. |
| Highest Rated is absent and only Over 4.5 appears | Keep filterQuery-star_rating=4.5 and report the applied control as Over 4.5. |
| Numeric minimum or maximum price controls are unavailable | Select all four DoorDash price tiers and record them as the requested full range. |
| Search or store page shows a persistent access wall | Solve 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 label | Record filterQuery-eta=true as Fastest and report the visible control name. |