# Google Local Services Ads Scraper (LSA Lead Extractor) (`optirefine/google-local-service-ads-extractor`) Actor

Extract Google Local Services Ads (LSA) leads: business name, phone number, rating, reviews, badges, years in business and service area.

- **URL**: https://apify.com/optirefine/google-local-service-ads-extractor.md
- **Developed by:** [OptiRefine](https://apify.com/optirefine) (community)
- **Categories:** Lead generation, E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.20 / 1,000 searcheds

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?

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

## Google Local Services Ads Scraper (LSA Extractor)

Extract Google Local Services Ads (LSA) provider listings — the "Google Guaranteed" cards with a call button at the top of local search results — as clean dataset rows. Point this Google Local Services Ads scraper at a service (plumbers, roofers, HVAC, electricians, house cleaners...) and a city, and it returns each provider's business name, call-tracking phone number, star rating, review count, trust badges, years in business and service area.

Google itself caps every search at **20 providers per service, per location** — that's Google's own limit, not an actor setting; see [How many results can I get?](#how-many-results-can-i-get). Within that cap, this LSA scraper is built for lead generation agencies filling a pipeline of local businesses to pitch, local SEO consultants auditing who's winning the paid local pack in a market, and anyone doing competitor research on who is actively paying Google for calls in a city. It reads Google's own server-rendered results page over plain HTTP rather than driving a browser, which keeps runs fast and predictable on the Apify platform.

### How do I scrape Google Local Services Ads?

1. **List the services** in `searchQueries` — plain wording like `plumbers` or `roofers` returns more results than Google's formal category names.
2. **List the cities** in `locations`, including state, province or country (`"Austin, TX"`, not `"Austin"`) so Google — and the proxy — picks the right country.
3. **Add a country-matched proxy** in `proxyConfiguration` (residential recommended). Local Services Ads only render for visitors Google believes are in the same country as the search — see [Why am I getting zero results?](#why-am-i-getting-zero-results).
4. **Run the actor.** Each service × location pair returns up to 20 providers as rows in the [dataset](https://docs.apify.com/platform/storage/dataset), ready to export as CSV/JSON or pulled out over the [Apify API](https://docs.apify.com/api/v2).

### What data you get

Each row in the output dataset is one advertiser from one search. Field names below match `LsaProvider` in the source exactly — use them as-is in your integration. For each field's exact null conditions and where it comes from in Google's markup, see the [full output reference](docs/OUTPUT-REFERENCE.md).

| Field | Type | Description |
|---|---|---|
| `customerId` | string | null | Google's internal advertiser/customer ID. This is the only reliable way to tell two listings apart — two different advertisers can legitimately share the same business name. |
| `name` | string | null | Business name as shown on the ad card. |
| `phone` | string | null | Phone number in E.164 format, e.g. `+14165550142`. This is a Google call-tracking number that forwards to the business — see [About the phone numbers](#about-the-phone-numbers). |
| `phoneDisplay` | string | null | The number as printed in the card's visible text. **Almost always `null`** — Google shows a "Get phone number" button instead of the digits. Use `phone`; this is only a fallback. |
| `phoneRegionCode` | string | null | Two-letter region code Google associates with the phone number, e.g. `CA`. |
| `businessId` | string | null | Google's internal business ID for the listing. |
| `providerId` | string | null | Google's internal provider ID for the listing. |
| `rating` | number | null | Average star rating, e.g. `4.9`. `null` when the provider has no reviews yet. |
| `reviewsCount` | number | null | Number of reviews behind the rating. `null` when the provider has no reviews yet. |
| `providerType` | string | null | The service category label Google prints on the card, e.g. `Plumber`. |
| `yearsInBusinessText` | string | null | The raw "years in business" line as Google renders it, e.g. `28+ years in business`. |
| `yearsInBusiness` | number | null | Just the number parsed out of the line above, e.g. `28`. |
| `serviceAreaText` | string | null | The raw service-area line, e.g. `Serves Whitby`. |
| `serviceArea` | string | null | Just the place name, e.g. `Whitby`. |
| `hours` | string | null | Opening-hours summary shown on the card, e.g. `Open 24 hours` or `Open · Closes 8 PM`. |
| `isOpenNow` | boolean | null | `true` when the hours text indicates the business is open right now. |
| `badges` | string\[] | Trust/verification badges Google shows, e.g. `Google Guaranteed`, `Google Screened`, `Background checked`. |
| `highlights` | string\[] | Marketing highlights the business chose to display, e.g. `Family owned`, `Flat rate pricing`. |
| `responseTime` | string | null | Google's response-time promise for the business, e.g. `Typically replies in 30 min`. |
| `profileUrl` | string | null | Absolute link to the provider's Local Services profile page on Google. |
| `isPreviewAd` | boolean | `true` when Google flags the card as a non-live preview ad rather than a currently running one. |
| `searchQuery` | string | Which of your input search terms produced this row. |
| `location` | string | Which of your input locations produced this row. |
| `countryCode` | string | The ISO-3166-1 alpha-2 country actually used for this search (auto-detected from the location, or your override) — this drives both Google's `gl` parameter and the proxy country. |
| `position` | number | Zero-based rank of this provider in Google's result list for this search (`0` is the top ad). |
| `sourceUrl` | string | The exact `google.com/localservices/prolist` URL the actor requested to produce this row. |
| `scrapedAt` | string | ISO timestamp of when the row was extracted. |

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `searchQueries` | array of strings | Yes | Services to search for. Plain, everyday wording ("plumbers", "electricians") tends to return more results than Google's formal taxonomy names. |
| `locations` | array of strings | Yes | Cities or areas to search in. Include the state, province or country ("Austin, TX", not "Austin") so the right place — and country — is picked. |
| `maxLeads` | integer | No (default `1000`) | A cap on the total number of providers saved across the whole run, not a per-search target. Read [How many results can I get?](#how-many-results-can-i-get) before raising this. |
| `minRating` | integer, 0–5 | No | Only save providers rated at or above this. Leave empty to keep everything. Setting this excludes brand-new providers that have no rating yet. |
| `minReviews` | integer | No | Only save providers with at least this many reviews. |
| `proxyConfiguration` | object | Required for real results | Apify Proxy configuration. The actor automatically forces the proxy country to match each location's country. Residential proxies are strongly recommended. |
| `countryCode` | string (ISO-3166-1 alpha-2) | No | Overrides the country the actor would otherwise detect from a location, e.g. `US`. Useful when a location name is ambiguous. |
| `languageCode` | string | No (default `en`) | Two-letter language code for Google's interface and result text. |

Example input:

```json
{
  "searchQueries": ["plumbers", "electricians"],
  "locations": ["Austin, TX", "Toronto, ON"],
  "maxLeads": 500,
  "minRating": 4,
  "minReviews": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  },
  "languageCode": "en"
}
```

This example runs 4 searches (2 services × 2 locations), each capped at Google's own 20-provider limit, and keeps only providers rated 4+ stars with at least 5 reviews.

### Output example

A single row from the dataset, for a "plumbers near Toronto, ON" search:

```json
{
  "customerId": "5423178690",
  "name": "Maple Leaf Plumbing & Drain",
  "phone": "+14165550142",
  "phoneDisplay": null,
  "phoneRegionCode": "CA",
  "businessId": "9082217634",
  "providerId": "4471183920",
  "rating": 4.9,
  "reviewsCount": 214,
  "providerType": "Plumber",
  "yearsInBusinessText": "18+ years in business",
  "yearsInBusiness": 18,
  "serviceAreaText": "Serves Toronto",
  "serviceArea": "Toronto",
  "hours": "Open 24 hours",
  "isOpenNow": true,
  "badges": ["Google Guaranteed", "Background checked"],
  "highlights": ["Family owned", "Emergency service"],
  "responseTime": "Typically replies in 30 min",
  "profileUrl": "https://www.google.com/localservices/prolist/provider?hl=en&gl=ca",
  "isPreviewAd": false,
  "searchQuery": "plumbers",
  "location": "Toronto, ON",
  "countryCode": "CA",
  "position": 0,
  "sourceUrl": "https://www.google.com/localservices/prolist?q=plumbers+near+Toronto%2C+ON&hl=en&gl=ca&src=1",
  "scrapedAt": "2026-08-25T14:32:07.918Z"
}
```

("Maple Leaf Plumbing & Drain" is an illustrative business, not a real advertiser.)

### How many results can I get?

**Google caps every Local Services Ads search at a maximum of 20 providers per service, per location.** This is a hard limit on Google's side — no offset, page or pagination parameter changes it, and scrolling the results pane doesn't load more. It's a fact about how Google serves LSA, not a limitation of this actor.

That means `maxLeads` is a *ceiling*, never a target: setting it to 5,000 will not make a single "plumbers near Austin, TX" search return more than 20 rows. If you need more than 20 leads, the only way to get there is to run **more searches** — add more service terms (`plumbers`, `drain cleaning`, `water heater repair`) and more locations (individual neighborhoods or nearby suburbs instead of one whole metro). Each service × location pair is its own search with its own 20-provider ceiling, so 10 services across 10 cities can return up to 2,000 rows in one run, provided `maxLeads` is raised above its default of 1,000 to allow it.

### Why am I getting zero results?

Zero results almost always comes down to one of these, roughly in order of likelihood:

1. **Proxy/location country mismatch.** Google only shows Local Services Ads to visitors it believes are physically in the same country as the requested service area. If your proxy exits in Canada while you search "Plumbing near Austin, TX", Google silently returns an empty "try a different area" page — not an error. The actor auto-matches the proxy country to each location (see step 3 of [How do I scrape Google Local Services Ads?](#how-do-i-scrape-google-local-services-ads)), so make sure your [proxy configuration](https://docs.apify.com/platform/proxy) actually has access to the countries you're targeting (residential proxies with broad country coverage work best), and avoid overriding `countryCode` unless you're sure of the ISO code.
2. **There genuinely are no active LSA advertisers for that service in that area.** Local Services Ads coverage varies a lot by city and category — a large metro will have 20 roofers advertising; a small town may have none. This is a real, expected zero, not a bug.
3. **The country isn't supported by Local Services Ads at all.** Google runs LSA in a limited set of countries and is still expanding it. Check the [official Google Local Services Ads page](https://ads.google.com/local-services-ads/) for current availability before assuming your setup is broken.
4. **Filters are removing everything.** If you set `minRating` or `minReviews`, brand-new providers with no reviews yet are excluded entirely, which can take a run from 20 raw results down to very few (or zero) saved rows. Try the run with the filters cleared to confirm providers exist before re-adding them.

### About the phone numbers

Google's own Local Services Ads page hides the phone number behind a "Get phone number" button — but the server-rendered HTML underneath ships the number anyway, in a `data-phone-number` attribute, on every provider card. The actor reads it directly, with no clicking and no browser involved, which is why `phone` is populated on effectively every row.

These are **Google call-tracking numbers**, not the business's direct line. Calls to them are forwarded to the actual business, and this is the number the advertiser pays Google to receive calls on — which makes it the correct number to use for outreach or lead delivery. `phone` is always in E.164 format (e.g. `+14165550142`). `phoneDisplay` only carries a value on the rare card where Google also renders visible display text for the number — in the normal case, where the UI hides it behind the "Get phone number" button, `phoneDisplay` is `null` and `phone` is the field to use. `phoneRegionCode` gives the two-letter region Google associates with the number.

### FAQ

#### Do I need a proxy for this to work?

Yes. Local Services Ads is geo-gated by the requesting IP's country, so a proxy in the wrong country returns an empty result even for a real, active market. The `proxyConfiguration` input is required for real results, and residential proxies are recommended. See [Why am I getting zero results?](#why-am-i-getting-zero-results)

#### Are the phone numbers real?

Yes — they ring through to the actual business — but they are Google call-tracking numbers, not the advertiser's private line. That's the number the advertiser pays Google to receive calls on, which makes it the right number for outreach. See [About the phone numbers](#about-the-phone-numbers) for format details.

#### Why do simple search terms like "plumbers" work better than Google's official category names?

Google's Local Services Ads search matches on plain, everyday wording. Colloquial terms such as `roofers`, `electricians` and `plumbers` return full result sets, while some formal taxonomy names return nothing. Type services the way a customer would search for them.

#### Can the same business appear twice, or two different businesses share a name?

Yes to both — business name is not a unique identifier on Google's side. Two distinct advertisers can list under an identical display name. Always dedupe or key your data on `customerId`, not `name`.

#### Is this a browser-based (Playwright/Selenium) scraper?

No. Google's Local Services results page is fully server-rendered, so the actor fetches it over plain HTTP and parses the HTML — no browser is launched. This makes runs fast and avoids browser-fingerprinting issues entirely.

#### Can I get more than 20 results for one service in one city?

No — that's Google's own per-search limit, not a setting in this actor. See [How many results can I get?](#how-many-results-can-i-get) for how to scale up using more services and locations instead.

#### Will `minRating` or `minReviews` reduce my result count even when providers exist?

Yes. Both filters are applied after Google returns results, and `minRating` in particular drops every provider that has no rating yet (which includes brand-new advertisers). If a search that should have 20 providers returns far fewer, try the same run with these filters cleared to see the unfiltered count.

***

This is an independent tool built on publicly accessible Google Local Services Ads result pages. It is not affiliated with, endorsed by, or sponsored by Google LLC. "Google Local Services Ads" and "Google Guaranteed" are trademarks of Google LLC.

# Actor input Schema

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

The services you want providers for. Everyday wording works best — 'plumbers' and 'electricians' return more results than Google's formal category names.

## `locations` (type: `array`):

Cities or areas to search in — always include the state, province or country, e.g. 'Austin, TX' or 'London, UK', not just 'Austin' or 'London'. Without a region, the country can't be determined and the run falls back to the US (it logs a warning when this happens) — which returns zero results if that's the wrong country for that location. The detected country also selects the matching proxy automatically.

## `maxLeads` (type: `integer`):

Stops the run once this many providers have been saved. IMPORTANT: Google returns a maximum of 20 providers per service per location, so a single service in a single city yields at most 20 results no matter how high you set this. To get more leads, add more locations or more services.

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

Only save providers rated at or above this — use a whole number (4, not 4.5). Leave it empty to keep every provider regardless of rating. Setting it to 0 is NOT the same as leaving it empty: providers with no rating yet (brand-new advertisers) are excluded whenever this filter is set, including when set to 0.

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

Only save providers with at least this many reviews — useful for filtering out newly listed businesses. Leave it empty to keep everyone. As with the rating filter, setting this to 0 still excludes providers Google shows no review count for; only an empty field includes them.

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

REQUIRED for real results. Google only shows Local Services Ads to visitors in the same country as the search location, so the run automatically forces the proxy country to match each location. Residential proxies are strongly recommended.

## `countryCode` (type: `string`):

Two-letter ISO country code (US, GB, CA, DE…) that overrides the detected country for EVERY location in this run, not just ambiguous ones, and also silences the ambiguous-location warning. Leave it empty unless all your locations are in the same country — mixing 'Austin, TX' with a GB override sends the Austin search to a GB proxy and returns nothing.

## `languageCode` (type: `string`):

Two-letter language code used for Google's interface and result text.

## Actor input object example

```json
{
  "searchQueries": [
    "roofers",
    "house cleaners"
  ],
  "locations": [
    "London, UK",
    "Toronto, ON"
  ],
  "maxLeads": 100,
  "minRating": 4,
  "minReviews": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "countryCode": "US",
  "languageCode": "en"
}
```

# Actor output Schema

## `leads` (type: `string`):

Every extracted provider. Console renders this as a table using the dataset schema's saved views (Leads overview, Contact list, Ratings and badges); use the Export button there for CSV, JSON or XLSX.

## `runInConsole` (type: `string`):

The run's page in Apify Console, including the log and the dataset views.

# 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 = {
    "searchQueries": [
        "plumbers",
        "electricians"
    ],
    "locations": [
        "Austin, TX"
    ],
    "maxLeads": 100,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("optirefine/google-local-service-ads-extractor").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 = {
    "searchQueries": [
        "plumbers",
        "electricians",
    ],
    "locations": ["Austin, TX"],
    "maxLeads": 100,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("optirefine/google-local-service-ads-extractor").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 '{
  "searchQueries": [
    "plumbers",
    "electricians"
  ],
  "locations": [
    "Austin, TX"
  ],
  "maxLeads": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call optirefine/google-local-service-ads-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,optirefine/google-local-service-ads-extractor"
        }
    }
}

```

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/kByfp66P6XqdiJXnP/builds/R69pkOFqNMS3an3T7/openapi.json
