# GrabFood Scraper — Restaurants & Menus in Southeast Asia (`polyglot-data/grabfood-scraper`) Actor

Scrape GrabFood restaurants in Singapore, Malaysia, the Philippines, Vietnam and Indonesia: cuisines, ratings, delivery fees, addresses and optional menu prices. Pay per result.

- **URL**: https://apify.com/polyglot-data/grabfood-scraper.md
- **Developed by:** [Polyglot Data](https://apify.com/polyglot-data) (community)
- **Categories:** E-commerce
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 restaurant scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## GrabFood Scraper — Restaurants & Menus in Southeast Asia

Extract restaurant and menu data from **GrabFood** in five Southeast Asian markets:
**Singapore** 🇸🇬, **Malaysia** 🇲🇾, **Philippines** 🇵🇭, **Vietnam** 🇻🇳 and **Indonesia** 🇮🇩.

Get restaurant names, cuisines, ratings, delivery fees and times, addresses, opening hours and, optionally, the full
menu with item prices, for any point on the map or for a list of restaurant URLs. The output is flat, clean JSON
that you can export to Excel, CSV or Google Sheets, or pull from the API.

***

### ✅ Why this scraper

- **Works where HTTP-only scrapers fail.** GrabFood's data API only answers to a guest session created by the site's own JavaScript. This actor opens GrabFood in a real browser, gets that guest session (no login), and calls the same API from inside the page.
- **Polite by default.** It waits 2 seconds between API calls, following the Crawl-delay in GrabFood's robots.txt.
- **Pay per result.** You pay only for restaurants actually saved to the dataset. Error items and failed pages are not charged.
- **Clean numbers.** Prices are converted from GrabFood's minor units into normal numbers (for example `"S$6.50"` becomes `6.5`), currencies are ISO codes (`SGD`, `MYR`, `THB`, `PHP`, `VND`, `IDR`), and missing values are `null`, never empty strings.
- **Five countries, one schema.** The same fields come back from every country.

***

### 📦 What data you get

Fast = from search results. Detail = added with **Include restaurant details**. Menu = added with **Include full menu with prices**.

| Field | Type | Fast | Detail | Menu | Description |
|---|---|:-:|:-:|:-:|---|
| `url` | string | ✅ | ✅ | ✅ | GrabFood restaurant page |
| `restaurantId` | string | ✅ | ✅ | ✅ | GrabFood merchant ID |
| `name` | string | ✅ | ✅ | ✅ | Restaurant name |
| `chainName` | string | ✅ | ✅ | ✅ | Chain name, if any |
| `branchName` | string | ✅ | ✅ | ✅ | Branch name |
| `cuisine` | string\[] | ✅ | ✅ | ✅ | Cuisine tags |
| `rating` | number | ✅ | ✅ | ✅ | Average rating |
| `reviewCount` | integer | ✅ | ✅ | ✅ | Number of ratings |
| `isOpen` | boolean | ✅ | — | — | Open for delivery right now |
| `openingHours` | object | — | ✅ | ✅ | Hours by day (`mon` … `sun`) plus `displayedHours` |
| `hasPromo` | boolean | ✅ | — | — | Has an active promotion |
| `promoLabels` | string\[] | ✅ | — | — | Promotion labels shown on the card |
| `deliveryFee` | number | ✅ | — | — | Delivery fee in major units |
| `currency` | string | ✅ | ✅ | ✅ | ISO 4217 code |
| `estimatedDeliveryMinutes` | integer | ✅ | — | — | Estimated delivery time |
| `deliveryTimeRange` | string | ✅ | — | — | Delivery time as shown, e.g. `20-30 min` |
| `distanceKm` | number | ✅ | — | — | Distance from the search point |
| `address` | string | — | ✅ | ✅ | Full address |
| `city` | string | — | ✅ | ✅ | City |
| `postcode` | string | — | ✅ | ✅ | Postal code |
| `imageUrl` | string | ✅ | ✅ | ✅ | Main photo |
| `menuItemCount` | integer | — | ✅ | ✅ | Total menu items |
| `menu` | object\[] | — | — | ✅ | Menu items with prices (see below) |
| `searchLocation` | string | ✅ | ✅ | ✅ | The `lat,lng` point the search used |
| `country` | string | ✅ | ✅ | ✅ | `sg`, `my`, `ph`, `vn` or `id` |
| `scrapedAt` | string | ✅ | ✅ | ✅ | UTC timestamp of the scrape |

Each menu item in `menu` has `category`, `name`, `description`, `price`, `discountedPrice`, `discountPct`,
`available` and `imageUrl`.

***

### ⬆️ Output examples

All names and IDs below are fictional. The numbers are plausible examples, not real GrabFood data.

#### 1. Fast mode item (search results)

```json
{
  "url": "https://food.grab.com/sg/en/restaurant/online-delivery/2-EXAMPLE01",
  "restaurantId": "2-EXAMPLE01",
  "name": "Example Chicken Rice — Orchard",
  "chainName": null,
  "branchName": "Orchard",
  "cuisine": ["Chinese", "Local"],
  "rating": 4.6,
  "reviewCount": 812,
  "isOpen": true,
  "openingHours": null,
  "hasPromo": true,
  "promoLabels": ["20% off up to S$5"],
  "deliveryFee": 1.8,
  "currency": "SGD",
  "estimatedDeliveryMinutes": 25,
  "deliveryTimeRange": "20-30 min",
  "distanceKm": 1.2,
  "address": null,
  "city": null,
  "postcode": null,
  "imageUrl": "https://example.com/photos/example-chicken-rice.jpg",
  "menuItemCount": null,
  "menu": null,
  "searchLocation": "1.3048,103.8318",
  "country": "sg",
  "scrapedAt": "2026-10-09T08:30:00.000Z"
}
```

#### 2. Detail item (with opening hours and address)

This record came from a search, so the fast fields are still filled. The detail fields add the address, hours and menu size.

```json
{
  "url": "https://food.grab.com/sg/en/restaurant/online-delivery/2-EXAMPLE01",
  "restaurantId": "2-EXAMPLE01",
  "name": "Example Chicken Rice — Orchard",
  "chainName": null,
  "branchName": "Orchard",
  "cuisine": ["Chinese", "Local"],
  "rating": 4.6,
  "reviewCount": 812,
  "isOpen": true,
  "openingHours": {
    "sun": "10:00-21:30",
    "mon": "10:00-21:30",
    "tue": "10:00-21:30",
    "wed": "10:00-21:30",
    "thu": "10:00-21:30",
    "fri": "10:00-22:00",
    "sat": "10:00-22:00",
    "displayedHours": "Daily 10:00 AM - 10:00 PM"
  },
  "hasPromo": true,
  "promoLabels": ["20% off up to S$5"],
  "deliveryFee": 1.8,
  "currency": "SGD",
  "estimatedDeliveryMinutes": 25,
  "deliveryTimeRange": "20-30 min",
  "distanceKm": 1.2,
  "address": "1 Example Street, #01-01, Singapore",
  "city": "Singapore",
  "postcode": "238000",
  "imageUrl": "https://example.com/photos/example-chicken-rice.jpg",
  "menuItemCount": 24,
  "menu": null,
  "searchLocation": "1.3048,103.8318",
  "country": "sg",
  "scrapedAt": "2026-10-09T08:31:12.000Z"
}
```

#### 3. Menu snippet (two of the 24 items)

With **Include full menu with prices** on, `menu` holds every item. This snippet shows two of them. Each price is in
major units (`6.5` means S$6.50).

```json
{
  "name": "Example Chicken Rice — Orchard",
  "menuItemCount": 24,
  "menu": [
    {
      "category": "Chicken Rice",
      "name": "Example Roasted Chicken Rice",
      "description": "Half chicken, rice and chili sauce",
      "price": 6.5,
      "discountedPrice": null,
      "discountPct": null,
      "available": true,
      "imageUrl": "https://example.com/photos/item-1.jpg"
    },
    {
      "category": "Sides",
      "name": "Example Braised Egg",
      "description": null,
      "price": 1.2,
      "discountedPrice": 1.0,
      "discountPct": 17,
      "available": true,
      "imageUrl": null
    }
  ]
}
```

***

### 🚀 How to use

1. Pick a **Country**.
2. Choose where to search. Either leave **Search point** empty to use the city center, or enter a `lat,lng`.
3. Add **Search terms** (dish, cuisine or restaurant name), or leave them empty to get every restaurant near the point.
4. Set **Max items**, turn on the detail or menu options you need, then click **Start**.

#### How to scrape restaurants near a location

Leave the search terms empty and set a point. Every restaurant near it comes back in fast mode.

```json
{
  "country": "sg",
  "latlng": "1.3048,103.8318",
  "maxItems": 200
}
```

For Jakarta, use `"country": "id"` and `"latlng": "-6.2088,106.8456"`. Leave `latlng` out to use the city center.

#### How to search for a dish or cuisine

Each search term runs as its own search around the point, one after another. Items are saved in that order.

```json
{
  "country": "my",
  "searchTerms": ["nasi lemak", "satay"],
  "latlng": "3.1478,101.6953",
  "maxItems": 100
}
```

#### How to get full menus and prices

Turn on **Include full menu with prices**. It also turns on restaurant details, so you get the address and hours too.

```json
{
  "country": "sg",
  "startUrls": [
    { "url": "https://food.grab.com/sg/en/restaurant/example-chicken-rice-orchard/2-EXAMPLE01" }
  ],
  "includeMenu": true,
  "maxItems": 1
}
```

Start URLs are always opened in detail mode, so they return `address`, `openingHours` and `menuItemCount`
without the fast-only fields (`isOpen`, `deliveryFee`, `distanceKm`, `hasPromo`).

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `country` | `sg` / `my` / `ph` / `vn` / `id` | `sg` | Market to search. Also sets the default city center. |
| `latlng` | string | city center | Custom search point as `lat,lng`. |
| `searchTerms` | string\[] | `[]` | Dish, cuisine or restaurant name. Empty = all restaurants near the point. |
| `startUrls` | request list | `[]` | GrabFood restaurant URLs. Each is opened in detail mode. |
| `maxItems` | integer | `100` | Maximum number of restaurants to save. Error items do not count. |
| `includeDetails` | boolean | `false` | Open each restaurant for address, opening hours and menu size. |
| `includeMenu` | boolean | `false` | Also return the full menu with item prices. Implies `includeDetails`. |
| `proxyConfiguration` | proxy object | Apify residential | Residential proxy is recommended. The proxy country is set from `country`. |
| `debug` | boolean | `false` | Adds `_debug` diagnostic items to the dataset. For troubleshooting. |

***

### ⚡ Modes

| | Fast (default) | Details | Details + Menu |
|---|---|---|---|
| Turn on | nothing extra | `includeDetails` | `includeMenu` |
| Data | Search-level fields | + address, city, postcode, opening hours, menu size | + every menu item with prices |
| Requests per restaurant | None extra; one search call returns up to 32 restaurants | One extra request per restaurant | One extra request per restaurant (same request as Details) |
| Speed | Fastest | Slower: at least 2 s per restaurant because of the crawl delay | Same as Details; the menu is in the same response |
| Per-result fee | `restaurant` event | `restaurant` event | `restaurant` + `menu` events |
| Platform usage, measured Oct 2026 (Singapore) | ≈ $0.05 per 1,000 restaurants | ≈ $0.90 per 1,000 restaurants | ≈ $0.90 per 1,000 restaurants |

A run always starts one browser, so very small runs cost a little more per restaurant.

***

### ⚠️ Error items

If an input produces no restaurants, the actor does not fail silently. It saves an **error item** to the dataset
that says why. Error items have an `error` code and a `note`.

| Code | Meaning |
|---|---|
| `NO_RESULTS` | The search returned no restaurants near the point. Try a bigger area, another term or another point. |
| `RESTAURANT_NOT_FOUND` | A start URL did not match a GrabFood restaurant. Check the URL. |
| `INVALID_LOCATION` | `latlng` could not be read. Use `lat,lng`, for example `1.3048,103.8318`. The run stops after this item. |
| `BLOCKED` | GrabFood refused the page after all retries (up to 4 retries per page). Try again later, or check the proxy settings. |

Example error item (no results for a search):

```json
{
  "input": "durian pizza",
  "searchLocation": "1.3048,103.8318",
  "error": "NO_RESULTS",
  "note": "GrabFood returned no restaurants for this search near the location."
}
```

Error items are **not charged**.

***

### 🎯 Use cases

- **Market research:** count restaurants, cuisines and average ratings by area and see where the market is crowded.
- **Competitor menu pricing:** compare item prices and discounts across the restaurants that compete with you.
- **Promo tracking:** watch `hasPromo` and `promoLabels` over time to see which restaurants run discounts and when.
- **B2B lead lists for suppliers:** build a list of restaurant names and addresses (restaurant data only) for food suppliers or equipment sellers.
- **Expansion and white-space analysis:** run the same cuisine search across several points to find areas with few options.

***

### 🔌 Integrations, API & MCP

- **Schedules:** run it daily or hourly from the Schedules tab to track changes.
- **Webhooks, Make, Zapier, n8n, Google Sheets:** send results anywhere when a run finishes.
- **API:** start a run and get the items in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/Polyglot-Data~grabfood-scraper/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"country":"sg","searchTerms":["chicken rice"],"latlng":"1.3048,103.8318","maxItems":50}'
```

**Use with AI agents (MCP).** The Apify MCP server lets Claude, ChatGPT and other MCP clients call this actor
directly. Ask your assistant to "find chicken rice restaurants near Orchard Road in Singapore", and it runs the actor
and reads back the dataset.

***

### 💰 Pricing

**Pay per result.** You pay a small fixed price for each restaurant saved to the dataset (`restaurant` event), and a
second small fee only for restaurants returned **with their full menu** (`menu` event, only when `includeMenu` is on).
See the **Pricing** tab for current prices. Platform usage is on top (see the Modes table). There is no monthly rental.
Error items and failed pages are never charged, and the actor stops when your run's maximum charge is reached.
A test run with `maxItems: 20` costs little.

***

### ❓ FAQ

**Which countries are supported?**
Singapore, Malaysia, the Philippines, Vietnam and Indonesia, through the matching `food.grab.com` site. Thailand is not supported because GrabFood does not offer web ordering there.

**Do I need my own proxies?**
No. The default is Apify residential proxy, and the proxy country follows the **Country** you pick. Keep it on residential.

**Why does it use a browser?**
GrabFood's data API only answers to a guest session that the site's own JavaScript creates. The actor opens the
page in a real browser to get that session (no login), then calls the same API from inside the page.

**How fresh is the data?**
Every run reads GrabFood live when it starts. Results reflect the restaurant data at that moment.

**Can I scrape a whole city?**
Yes. Raise **Max items**, or run several points across the city and merge the datasets. Each run covers the
results around one point.

**Why are some fields `null`?**
Three common reasons: the field is only in fast mode or only in detail mode (see the field table); a start URL
skips the search, so fast-only fields such as `isOpen` and `deliveryFee` are `null`; or GrabFood did not publish
that value for the restaurant.

**Something broke, or a field is missing.**
Open an issue in the **Issues** tab with the input you used and, if you can, the error item. Issues are answered quickly.

***

### ⚖️ Legal

This actor collects only publicly visible restaurant and menu information: names, addresses, cuisines, ratings,
opening hours, delivery information and menu prices. It does not collect personal data, customer names or customer
reviews. You are responsible for making sure your use complies with GrabFood's terms of service and with the laws
that apply to you.

# Actor input Schema

## `country` (type: `string`):

GrabFood country site. Sets the search market and the default city center.

## `latlng` (type: `string`):

Custom search point as 'lat,lng'. Leave empty to use the country's main city center.

## `searchTerms` (type: `array`):

Dish, cuisine or restaurant name to search for, e.g. "chicken rice". Leave empty to get all restaurants near the point.

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

GrabFood restaurant URLs, e.g. https://food.grab.com/sg/en/restaurant/<slug>/<ID>. Each one is opened directly in detail mode.

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

Maximum number of restaurants to save. Error items do not count.

## `includeDetails` (type: `boolean`):

Open each restaurant for address, opening hours and menu size. Slower, one extra request per restaurant.

## `includeMenu` (type: `boolean`):

Also return the full menu with item prices. Implies Include restaurant details.

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

Residential proxy required. The proxy country is set automatically from the selected country.

## `debug` (type: `boolean`):

Add diagnostic items (status, counts, pagination) to the dataset, marked with \_debug. For troubleshooting.

## Actor input object example

```json
{
  "country": "sg",
  "searchTerms": [],
  "startUrls": [],
  "maxItems": 20,
  "includeDetails": false,
  "includeMenu": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "debug": false
}
```

# Actor output Schema

## `restaurants` (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 = {
    "country": "sg",
    "startUrls": [],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("polyglot-data/grabfood-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 = {
    "country": "sg",
    "startUrls": [],
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

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

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

```

## CLI example

```bash
echo '{
  "country": "sg",
  "startUrls": [],
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call polyglot-data/grabfood-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,polyglot-data/grabfood-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

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