DoorDash Scraper: Menus, Prices, Reviews & Ratings avatar

DoorDash Scraper: Menus, Prices, Reviews & Ratings

Pricing

from $1.00 / 1,000 results

Go to Apify Store
DoorDash Scraper: Menus, Prices, Reviews & Ratings

DoorDash Scraper: Menus, Prices, Reviews & Ratings

Extract structured doordash.com data at scale. Search by keyword or paste store URLs to get store details, ratings, price tier, location, full menus, contact info, opening hours, review insights, star-rating breakdowns, and Google rating data.

Pricing

from $1.00 / 1,000 results

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

9

Total users

3

Monthly active users

15 days ago

Last modified

Share

DoorDash Scraper: Stores, Menus, Reviews & Search

Pull clean, structured data from DoorDash at scale. Search any keyword or paste store links, and get one record per store containing meta (name, rating, price tier, geo), the full menu, contact details (phone, website, opening hours) and review insights (recent reviews, the full 1 to 5 star histogram, theme highlights and the Google rating aggregate). Fast, reliable, and comprehensive.

Why this scraper?

  • Five things in one actor: Search, Meta, Menu, Contact info and Reviews.
  • 40+ fields per store, including the exact numeric rating count (not just "500+"), the full star histogram and review theme mentions.
  • Lean search runs return about 30 fields per store and are very cheap.
  • Full menu with item names, prices, photos, dietary tags and item ratings.
  • Menu items on a genuine markdown are flagged with isOnSpecial, savingsAmount and discountPercent, derived straight from the store's own listed price.
  • Opening hours by day, phone number, website and precise latitude/longitude.
  • Toggle exactly what you pay for: include menu, contact info and reviews independently.
  • Clean, consistent output with stable field names you can map once and reuse.
  • Resume & recurring updates — turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED stores on every scheduled run, or resume one specific interrupted run with resumeFromRunId.

Data you get

Sample shape, values are illustrative placeholders, not from a live listing.

FieldExample
storeId"00000001"
name"Sample Pizza Place"
url"https://www.doordash.com/store/00000001/"
rating4.5
ratingCount1234
priceTier"$$"
cuisines["Pizza", "Italian"]
city / state"Sampleton" / "CA"
latitude / longitude25.0000 / 55.0000
phone"+10000000000"
website"https://example.com"
etaMinutes25
deliveryFee"$0 delivery fee"
isSponsoredfalse
isDashpasstrue
menuItemCount42
reviewCount18
menu.categories[][{ "name": "Most Ordered", "numItems": 6 }]
menu.items[][{ "name": "Sample Item", "price": "$7.99", "strikethroughPrice": "$9.99", "isOnSpecial": true, "savingsAmount": 2.00, "discountPercent": 20, "dietaryTags": ["Vegetarian"] }]
contact.hours[][{ "day": "MONDAY", "slots": ["10 AM - 10 PM"] }]
reviews.histogram{ "1": 5, "2": 3, "3": 4, "4": 20, "5": 90 }
reviews.items[][{ "reviewer": "Jane D", "stars": 5, "text": "Sample review text." }]
reviews.themes[][{ "label": "Great value", "count": 12, "sentiment": "POSITIVE" }]

How to use

Search by keyword, lean (no detail fetch):

{
"mode": "search",
"search": ["pizza"],
"maxStores": 50,
"maxPages": 2,
"includeMenu": false,
"includeBusiness": false,
"includeReviews": false,
"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }
}

Search a specific city with full detail:

{
"mode": "search",
"search": ["sushi", "ramen"],
"location": "Brooklyn, NY",
"maxStores": 20,
"includeMenu": true,
"includeBusiness": true,
"includeReviews": true,
"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }
}

Specific stores by URL or ID:

{
"mode": "url",
"urls": ["https://www.doordash.com/store/00000001/", "00000002"],
"includeMenu": true,
"includeBusiness": true,
"includeReviews": true,
"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }
}

Input parameters

ParameterTypeDefaultDescription
modestring"search""search" for keyword search, "url" for store links or IDs.
searcharray["pizza"]Keywords to search. One search runs per term. Search mode only.
locationstring(empty)City or address to localize search, e.g. "Brooklyn, NY". Defaults to a major city in your proxy country.
storeTypestring"any"Limit search to a category: any, restaurant, grocery, retail. "restaurant" excludes convenience/grocery stores. Search mode only.
ratedOnlybooleanfalseOnly return stores that have a customer rating. Search mode only.
dealsOnlybooleanfalseOnly return stores currently offering deals. Search mode only.
urlsarray(example)Store links or numeric store IDs. URL mode only.
includeMenubooleantrueAdd the full menu (one store detail fetch per store).
includeBusinessbooleantrueAdd phone, website and opening hours.
includeReviewsbooleantrueAdd reviews, star histogram, themes and Google aggregate.
maxStoresinteger20Max stores across all searches/URLs. 0 means no limit. Counts stores scanned (collected and detail-fetched), before any Incremental mode suppression — see "Resume & recurring updates" below.
maxPagesinteger(empty)Optional bound on result pages walked per search query (50 stores per page). Leave empty to walk every result page. Search mode only.
resumeFromRunIdstring(empty)Previous run ID (or dataset ID) from this actor. Stores already collected there are skipped, so the run only returns new ones. For recurring daily/weekly monitoring of the same search, use Incremental mode instead — see below.
incrementalModebooleanfalseDaily/recurring monitoring of this same search. First run returns everything as NEW; later runs return only NEW/UPDATED/REAPPEARED by default. See "Resume & recurring updates" below.
stateKeystring(empty)Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your search/filter settings when left empty.
emitUnchangedbooleanfalseIncremental mode only. Also return stores unchanged since the last run, marked UNCHANGED. Adds and bills extra rows you already have.
emitExpiredbooleanfalseIncremental mode only. Also return stores from a previous run no longer found, marked EXPIRED, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows.
proxyobjectResidential USProxy configuration. Residential is required.

Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape — the Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each record — not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

Output example

Sample shape, values are illustrative placeholders, not from a live listing.

{
"storeId": "00000001",
"name": "Sample Pizza Place",
"url": "https://www.doordash.com/store/00000001/",
"rating": 4.5,
"ratingCount": 1234,
"priceTier": "$$",
"cuisines": ["Pizza", "Italian"],
"city": "Sampleton",
"state": "CA",
"latitude": 25.0000,
"longitude": 55.0000,
"etaMinutes": 25,
"deliveryFee": "$0 delivery fee",
"isSponsored": false,
"isDashpass": true,
"phone": "+10000000000",
"website": "https://example.com",
"contact": {
"phone": "+10000000000",
"website": "https://example.com",
"hours": [{ "day": "MONDAY", "slots": ["10 AM - 10 PM"] }],
"address": { "street": "1 Sample St", "city": "Sampleton", "state": "CA" }
},
"menu": {
"categories": [{ "name": "Most Ordered", "numItems": 6 }],
"items": [{ "name": "Sample Item", "price": "$7.99", "strikethroughPrice": "$9.99", "isOnSpecial": true, "savingsAmount": 2.00, "discountPercent": 20, "dietaryTags": ["Vegetarian"] }],
"popularItems": [{ "name": "Sample Item", "price": "$9.99", "strikethroughPrice": "", "isOnSpecial": false, "savingsAmount": null, "discountPercent": null }]
},
"menuItemCount": 42,
"reviews": {
"avgRating": 4.5,
"numRatings": 1234,
"numReviews": 18,
"histogram": { "1": 5, "2": 3, "3": 4, "4": 20, "5": 90 },
"items": [{ "reviewer": "Jane D", "stars": 5, "text": "Sample review text.", "isVerified": true }],
"themes": [{ "label": "Great value", "count": 12, "sentiment": "POSITIVE" }],
"google": { "avgRating": 4.4, "numRatings": 500 }
},
"reviewCount": 18
}

Incremental mode only. When incrementalMode is on, every returned record also carries:

FieldDescription
changeTypeNEW | UPDATED | UNCHANGED | REAPPEARED | EXPIRED
changedFieldsTop-level fields that changed since last seen; non-empty only for UPDATED
firstSeenAtWhen this store was first observed by this monitoring campaign
lastSeenAtWhen this store was last observed

Resume & recurring updates

There are two different things here — pick the one that matches what you're doing:

NeedUse
A run stopped and should continueresumeFromRunId / automatic checkpoint recovery
Run the same search every day and receive only changesincrementalMode
Keep separate daily campaigns for similar searchesdistinct stateKey values
Run a normal full snapshotleave both off

Resume (resumeFromRunId) continues one specific interrupted or previous large run: paste a run ID or dataset ID and this run skips stores already collected there, returning only the remaining new ones. An automatic same-run checkpoint also protects against platform migrations/Resurrects without any input needed.

Incremental mode (incrementalMode) is for a schedule (for example, daily): the actor remembers the previous run of the same search by itself, so you never paste a run ID. The first run returns everything as NEW. Later runs return only NEW, UPDATED, and REAPPEARED stores by default — unchanged stores are suppressed (fetched to check, but not returned and not charged for the detail-enrichment surcharge). Turn on emitUnchanged or emitExpired only when you also want those rows returned (and billed for). State is isolated per search/URL and filter/include-toggle combination automatically (maxStores/maxPages are excluded — raising your cap keeps the same baseline instead of re-billing everything); set stateKey to name or deliberately share a monitoring campaign.

Scheduled-run example — same search, run daily:

Day 1 (first run ever for this search):

{ "mode": "search", "search": ["pizza"], "location": "Brooklyn, NY", "incrementalMode": true }

→ every store comes back with "changeType": "NEW".

Day 2 (the schedule fires again, identical input):

{ "mode": "search", "search": ["pizza"], "location": "Brooklyn, NY", "incrementalMode": true }

→ stores whose rating/menu/hours/etc. changed come back as "changeType": "UPDATED" with changedFields listing what changed; brand-new stores come back as "changeType": "NEW"; stores that vanished and came back come back as "changeType": "REAPPEARED"; stores that are still there, unchanged, are not returned at all (suppressed, not charged) unless emitUnchanged is on.

What counts as "changed": DoorDash's own nextOpenTime/nextCloseTime fields tick with the request's wall-clock time (measured: they advance every request even for always-open stores) and are never treated as a real change. Rating, price tier, delivery fee, ETA, menu items/prices and business contact info are real, comparable data and always drive UPDATED. Tiny GPS-distance floating-point noise is rounded away before comparison so it never triggers a false UPDATED either.

maxStores and Incremental mode: the cap counts stores scanned (collected and detail-fetched), not stores actually returned. A quiet monitoring run where nothing changed still scans up to maxStores stores (proxy cost is unchanged), but the dataset-item and detail-enrichment charges drop to near zero since almost everything is suppressed.

Scale: a monitoring campaign's state is capped at roughly 8.5MB (compressed) in Apify's key-value store. Measured against this actor's own record shapes: about 1,500 tracked stores with menu/business/reviews all enabled, or about 65,000 tracked stores with all three off (lean meta-only monitoring). Use a separate stateKey per campaign to stay well under this if you track more than that.

Plan requirement

This actor needs Apify Residential proxy, which is available on the Starter plan and above. Residential is the default and recommended setting. Set the proxy country to the region you want search results localized to (for example US). Recent public reviews are capped at about five per store; the full numeric rating count, the star histogram, theme highlights and the Google rating aggregate are always included.