# GrabFood Restaurant Scraper: Menus, Ratings & Delivery Data (`abotapi/grabfood-restaurants-scraper`) Actor

Scrape GrabFood restaurants across Southeast Asia, including SG, MY, TH, VN, PH, ID, KH, and MM. Search by keyword or URL and extract 45+ fields: restaurant name, address, GPS, cuisine, ratings, reviews, promos, delivery time, fees, opening hours, and full menus with item prices and images.

- **URL**: https://apify.com/abotapi/grabfood-restaurants-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Developer tools, Automation
- **Stats:** 22 total users, 10 monthly users, 89.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 restaurant results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## GrabFood Restaurants Scraper

Pull restaurants and full menus from GrabFood across Southeast Asia. Returns 45+ fields per restaurant: name, chain, address, GPS, cuisine tags, star rating and review count, promotions, delivery time and fee, opening hours, price tier, and the complete menu (categories, items, descriptions, prices, discounted/takeaway prices, images, modifier counts). Search by keyword in any of 8 countries, or paste GrabFood URLs (single restaurant or a listing page).

### Why this scraper

- 45+ fields per restaurant, including the full menu — vs the ~20 flat fields in the older GrabFood scraper on the Store
- Full menu surface: categories, items, descriptions, original and discounted prices, takeaway prices, item images, and modifier-group counts
- Delivery detail: estimated delivery time and range, delivery fee (with discounted fee), delivery options (delivery / takeaway / dine-in), price tier
- Promotions captured as structured labels (e.g. "S$5.00 off"), not just a boolean flag
- Full address block from the restaurant page: street, suburb, postcode, city, country
- Rating and review count on every card, with the review count populated from GrabFood's own data
- 8 regions: Singapore, Malaysia, Thailand, Vietnam, Philippines, Indonesia, Cambodia, Myanmar
- Two modes: search by keyword or paste URLs, plus filters for cuisine, minimum rating, and minimum review count, and 5 sort options
- Daily/recurring change monitoring: turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED restaurants on every scheduled run, or resume one specific interrupted crawl with `resumeFromRunId`

### Ratings and reviews

Every restaurant record carries GrabFood's aggregate **`rating`** (star score) and **`voteCount`** (the number of ratings the restaurant has received). These are the rating signals GrabFood exposes on its public web listings.

GrabFood's public web surface does **not** expose individual written customer reviews (author, date, review text) — the restaurant page returns only the aggregate score and the total number of ratings. We verified this with a live check of the restaurant detail data on food.grab.com: the merchant payload contains `rating` and `voteCount` but no per-review comment, author, or body objects. Written review text is only available inside the logged-in Grab mobile app, which is not part of the public site. This scraper therefore captures the rating and review-count data that exists, and does not fabricate review text that the site does not provide.

### Data you get

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

| Field | Example |
|---|---|
| id | `SGDD00000` |
| name | `Sample Burger Place` |
| chainName | `Sample Burger` |
| branchName | `Downtown` |
| cuisine | `["Burger", "Fast Food", "Halal"]` |
| address | `Sample Burger Place - Downtown` |
| fullAddress | `1 Sample Street, #01-01` |
| street | `Sample Street` |
| suburb | `Downtown` |
| postcode | `000000` |
| city | `Singapore` |
| countryCode | `SG` |
| latitude / longitude | `1.2880` / `103.8520` |
| rating | `4.3` |
| voteCount | `1200` |
| priceTag | `2` |
| isOpen | `true` |
| estimatedDeliveryTime | `35` |
| estimatedDeliveryTimeRange | `From 35 mins` |
| distanceInKm | `1.34` |
| hasPromo | `true` |
| promoLabels | `["S$5.00 off"]` |
| deliveryOptions | `DELIVERY_TAKEAWAY_DINEIN` |
| deliveryFee | `{ currencyCode, currencySymbol, price, priceDisplay, discountedPrice, hasDiscountedPrice }` |
| currency | `{ code: "SGD", symbol: "S$", exponent: 2 }` |
| openHours | `{ mon, tue, ..., displayedHours }` |
| photoHref / smallPhotoHref / iconHref | image URLs |
| grabUrl | `https://food.grab.com/sg/en/restaurant/sample-burger-place/SGDD00000` |
| menuCategoryCount / menuItemCount | `12` / `86` |
| menu | array of `{ name, available, itemCount, items: [{ id, name, description, price, discountedPrice, takeawayPrice, imageUrl, modifierGroupCount }] }` |
| orderValueLimit | `15000` |
| announcements | `["Public holiday hours apply"]` |
| region | `SG` |
| scrapedAt | ISO timestamp |
| changeType | NEW (incremental mode only — see "Resume & recurring updates" section below) |
| changedFields | `["rating", "hasPromo"]` (incremental mode, UPDATED rows only) |
| firstSeenAt | 2026-01-01T00:00:00.000Z (incremental mode only) |
| lastSeenAt | 2026-01-02T00:00:00.000Z (incremental mode only) |

### Input

- **mode** — `search` (keyword queries) or `url` (paste GrabFood links).
- **region** — the GrabFood country: SG, MY, TH, VN, PH, ID, KH, MM.
- **searchQueries** — keywords to search in the region. Leave empty to browse the region's recommended restaurants.
- **startUrls** — GrabFood restaurant pages or listing pages (URL mode).
- **cuisineFilter** — keep only restaurants whose cuisine tags contain any of these terms.
- **minRating** / **minReviews** — drop restaurants below a rating or review-count threshold.
- **sortBy** — default (GrabFood ranking), rating, review count, distance, or delivery time.
- **fetchMenu** — enrich each restaurant with the full menu and full address (on by default).
- **includeRatings** — populate rating and review count (on by default).
- **maxItems** — the main cap: total restaurants per run (default 20). Leave 0 for unlimited.
- **maxPages** — no page limit by default; the run stops at Max restaurants. Set only to also cap listing pages walked.
- **proxyConfiguration** — a residential connection is required; the exit country is aligned to the region automatically.
- **resumeFromRunId** / **incrementalMode** / **stateKey** / **emitUnchanged** / **emitExpired** — see "Resume & recurring updates" below.

### Resume & recurring updates

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

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large pull: paste a run ID or dataset ID and this run skips restaurants already collected there (matched on `id`, GrabFood's own merchant id), returning only the remaining NEW restaurants.

**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` restaurants by default — duplicates and unchanged restaurants are suppressed (and not charged, including the menu-enrichment surcharge). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per mode/region/search-or-URL/sort/detail-depth/filter setup automatically; set `stateKey` to name or deliberately share a monitoring campaign. Raising `maxItems`/`maxPages` on a later run never starts a new baseline.

A restaurant is only classified `UPDATED` when a real data field changes (rating, review count, name, promo state, menu, hours, etc.) — a re-scrape timestamp alone (`scrapedAt`) never counts as a change. `EXPIRED` rows are only produced once a run has fully scanned the tracked search (not when Max restaurants capped it, `resumeFromRunId` was used, or a source's pagination was cut short) — otherwise the actor can't tell "gone from GrabFood" apart from "not reached yet", and the previous state for those restaurants is kept as-is.

Scheduled-run example — same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "region": "SG", "searchQueries": ["ramen"], "incrementalMode": true }
```

→ every restaurant comes back with `"changeType": "NEW"`.

Day 2 (the schedule fires again, identical input): only restaurants that are new, changed, or reappeared come back — unchanged restaurants are skipped entirely (no rows, no charge) unless `emitUnchanged` is on.

### Output

One record per restaurant, pushed to the dataset. Enable **fetchMenu** for the full menu and full address block; disable it for faster listing-only runs.

### Send results into your apps (MCP connectors)

Optionally pipe results into the apps you already use via Model Context Protocol (MCP) connectors. Authorize a connector once under **Apify → Settings → Integrations**, then select it in the **mcpConnectors** input. Each connector receives a condensed, human-readable summary per item (title + key fields) — the full record always stays in the dataset. Supported: Notion, Linear, Airtable, and Apify. For Notion, also set **notionParentPageUrl**.

### Plan Requirement

Runs on any Apify plan.

### Notes

- Use a residential connection for reliable listing loads. Datacenter connections may be rejected by the site.
- Prices are in the region's currency and are returned in minor units (e.g. cents) alongside a formatted display string where GrabFood provides one.

# Actor input Schema

## `mode` (type: `string`):

Pick how to find restaurants. Search runs keyword queries in one region. URL mode pastes ready-made GrabFood links (a single restaurant page or a restaurants listing page).

## `region` (type: `string`):

GrabFood country to search. Results and prices are for that country's stores.

## `searchQueries` (type: `array`):

Keywords to search within the selected region (e.g. "ramen", "pizza", "bubble tea"). Leave empty to browse the region's recommended restaurants instead. Only applies in Search mode.

## `startUrls` (type: `array`):

Ready-made GrabFood URLs. Both a single restaurant page (https://food.grab.com/<cc>/<lang>/restaurant/<slug>/<id>) and a restaurants listing page (https://food.grab.com/<cc>/<lang>/restaurants?search=<keyword>) are accepted. Only applies in URL mode.

## `cuisineFilter` (type: `array`):

Keep only restaurants whose cuisine tags contain any of these terms (e.g. burger, sushi, halal, western, vegetarian). Case-insensitive substring match. Leave empty to include all.

## `minRating` (type: `integer`):

Minimum star rating (0-5). Restaurants below this are skipped. Leave empty for no minimum.

## `minReviews` (type: `integer`):

Minimum number of ratings a restaurant must have to be included. Leave empty or 0 for no minimum.

## `sortBy` (type: `string`):

Order of restaurants in the output. Default keeps GrabFood's own ranking; the other options re-order the collected results client-side.

## `fetchMenu` (type: `boolean`):

When ON, each restaurant is enriched with its full menu (categories, items, prices, images) and full address. Turn off for faster listing-only runs.

## `includeRatings` (type: `boolean`):

When ON, the rating score and review count are populated on every record. Turn off if you don't need any rating data.

## `maxItems` (type: `integer`):

Hard cap on total restaurants for the whole run. This is the main limit. Leave empty or 0 for unlimited (the run then stops at Max pages).

## `maxPages` (type: `integer`):

No page limit by default — the run stops at Max restaurants. Set a number only to also cap how many listing pages are walked per search.

## `proxyConfiguration` (type: `object`):

A residential connection is required for reliable listing loads. The exit country is aligned to the selected region automatically.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull of restaurants without returning or charging for restaurants already collected there. Use this after an interrupted run, or when continuing a large crawl in another run. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching restaurants as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED restaurants. Turn on "Emit unchanged" or "Emit expired" only when you also want those restaurants returned (and billed). State is kept separately for each mode/region/search/URL and detail/filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode/region/search/URL and detail/filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return restaurants that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return restaurants that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max restaurants capped it or when Resume was used. This returns — and bills — extra synthetic rows, so leave it off unless you need expiry tracking.

## Actor input object example

```json
{
  "mode": "search",
  "region": "SG",
  "searchQueries": [
    "ramen"
  ],
  "startUrls": [
    "https://food.grab.com/sg/en/restaurants?search=sushi"
  ],
  "minReviews": 0,
  "sortBy": "default",
  "fetchMenu": true,
  "includeRatings": true,
  "maxItems": 20,
  "maxPages": 0,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "maxNotifyListings": 50,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "searchQueries": [
        "ramen"
    ],
    "startUrls": [
        "https://food.grab.com/sg/en/restaurants?search=sushi"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/grabfood-restaurants-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "searchQueries": ["ramen"],
    "startUrls": ["https://food.grab.com/sg/en/restaurants?search=sushi"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/grabfood-restaurants-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "searchQueries": [
    "ramen"
  ],
  "startUrls": [
    "https://food.grab.com/sg/en/restaurants?search=sushi"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false
}' |
apify call abotapi/grabfood-restaurants-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=abotapi/grabfood-restaurants-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lBfQ4FQAj9x1wllx6/builds/vfM8UMHdnx17etKab/openapi.json
