{
  "id": "x8qzsYDsYdpwwNgb",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Scrape Google Maps Business Leads to Google Sheets with SearchApi",
  "tags": [],
  "nodes": [
    {
      "id": "manual",
      "name": "Run Now",
      "type": "n8n-nodes-base.manualTrigger",
      "notes": "Run on demand when you need a fresh batch of leads. Click Execute Workflow.",
      "position": [
        0,
        112
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "schedule",
      "name": "Schedule Weekly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Optional: activate the workflow to pull new listings every Monday at 08:00. Change or delete this if you only run manually.",
      "position": [
        0,
        304
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "settings",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "notes": "The only node you normally edit. categories = comma-separated business types. locations = semicolon-separated places (names contain commas, so we split on ;).",
      "position": [
        240,
        304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "categories",
              "type": "string",
              "value": "coffee shop, cafe"
            },
            {
              "id": "a2",
              "name": "locations",
              "type": "string",
              "value": "Austin, Texas, United States; Denver, Colorado, United States"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "build",
      "name": "Build Search Queries",
      "type": "n8n-nodes-base.code",
      "notes": "Expands your categories and locations into one Google Maps search per combination per page. Raise PAGES here for more leads per search.",
      "position": [
        480,
        304
      ],
      "parameters": {
        "jsCode": "// Build one Google Maps search per category, per location, per page.\n// Google Maps has no separate location field: the reliable way to target a\n// place is to put it in the query itself, e.g. \"coffee shop in Austin, Texas\".\n// Google Maps returns up to ~20 businesses per page; we page for more depth.\nconst cfg = $('Settings').first().json;\nconst PAGES = 2; // pages per category + location. Each page is ~20 leads. Raise for more depth.\n\nconst categories = (cfg.categories || '')\n  .split(',').map(c => c.trim()).filter(c => c.length);\n\n// Locations are semicolon-separated because location names contain commas\n// (e.g. \"Austin, Texas; Denver, Colorado\").\nconst locations = (cfg.locations || '')\n  .split(';').map(l => l.trim()).filter(l => l.length);\n\nif (!categories.length) {\n  throw new Error('No categories found. Add a comma-separated list in the Settings node, e.g. \"dentist, orthodontist\".');\n}\nif (!locations.length) {\n  throw new Error('No locations found. Add a semicolon-separated list in the Settings node, e.g. \"Austin, Texas; Denver, Colorado\".');\n}\n\nconst out = [];\nfor (const category of categories) {\n  for (const location of locations) {\n    for (let page = 1; page <= PAGES; page++) {\n      out.push({ json: { category, location, q: category + ' in ' + location, page } });\n    }\n  }\n}\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "searchapi",
      "name": "Scrape Google Maps",
      "type": "@searchapi/n8n-nodes-searchapi.searchApi",
      "notes": "Live Google Maps results via SearchApi.io, one call per category + location + page. Returns business name, address, phone, website, rating and reviews.",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        720,
        304
      ],
      "parameters": {
        "q": "={{ $json.q }}",
        "resource": "google_maps",
        "pagination": {
          "page": "={{ $json.page }}"
        },
        "requestOptions": {},
        "locationSettings": {},
        "searchConfiguration": {}
      },
      "credentials": {
        "searchApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "extract",
      "name": "Extract Leads",
      "type": "n8n-nodes-base.code",
      "notes": "Reads local_results from every page, keeps the useful lead fields, and removes duplicate businesses (by place_id). This is the clean, structured lead list.",
      "position": [
        960,
        304
      ],
      "parameters": {
        "jsCode": "// Flatten Google Maps local_results from every page into one row per business.\n// Category and location are recovered from Build Search Queries by position\n// (the scrape returns one item per search, in the same order), then we dedup\n// by place_id so the same business is not listed twice.\nconst items = $input.all();\nconst queries = $('Build Search Queries').all();\nconst seen = new Set();\nconst leads = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const res = items[i].json || {};\n  const src = (queries[i] && queries[i].json) || {};\n  const category = src.category || '';\n  const location = src.location || '';\n  const results = res.local_results || [];\n\n  for (const r of results) {\n    const placeId = r.place_id || r.data_id || (r.title + '|' + r.address);\n    if (seen.has(placeId)) continue;\n    seen.add(placeId);\n\n    const mapsUrl = r.place_id\n      ? 'https://www.google.com/maps/place/?q=place_id:' + r.place_id\n      : '';\n\n    // Domain drives the Hunter email lookup. Prefer the domain SearchApi already\n    // parsed; otherwise derive it from the website URL.\n    let domain = r.domain || '';\n    if (!domain && r.website) {\n      try { domain = new URL(r.website).hostname.replace(/^www\\./, ''); } catch (e) {}\n    }\n\n    leads.push({ json: {\n      business: r.title || '',\n      category: category,\n      location: location,\n      type: r.type || '',\n      address: r.address || '',\n      phone: r.phone || '',\n      website: r.website || '',\n      email: '',\n      domain: domain,\n      rating: (r.rating !== undefined && r.rating !== null) ? r.rating : '',\n      reviews: (r.reviews !== undefined && r.reviews !== null) ? r.reviews : '',\n      maps_url: mapsUrl,\n      place_id: r.place_id || ''\n    } });\n  }\n}\n\nreturn leads;"
      },
      "typeVersion": 2
    },
    {
      "id": "sheet",
      "name": "Save Leads to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Appends each lead as a new row. Select your Google Sheet and tab, and give it a header row: business, category, location, type, address, phone, website, rating, reviews, maps_url, place_id.",
      "position": [
        1632,
        304
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "business",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "business",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "address",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "website",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "website",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rating",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reviews",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "reviews",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "maps_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "maps_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "place_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "place_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PLHGO9WBiLaWsSb8N00WB7DtB9mq-lmvezwWwnr6y1M/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1PLHGO9WBiLaWsSb8N00WB7DtB9mq-lmvezwWwnr6y1M",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1PLHGO9WBiLaWsSb8N00WB7DtB9mq-lmvezwWwnr6y1M/edit?usp=drivesdk",
          "cachedResultName": "n8n003"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "sticky_overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -736,
        -256
      ],
      "parameters": {
        "width": 640,
        "height": 900,
        "content": "## Scrape Google Maps Business Leads to Google Sheets\n\nTurn any list of business categories and locations into a clean lead list \u2014 business name, phone, website, address, rating, review count, and (via Hunter.io) an email \u2014 pulled live from Google Maps through SearchApi.io.\n\n### How it works\n1. **Run Now** or **Schedule Weekly** starts the run.\n2. **Settings** holds the two things you edit: your categories and your locations.\n3. **Build Search Queries** turns them into one Google Maps search per category \u00d7 location \u00d7 page.\n4. **Scrape Google Maps** fetches live results through SearchApi.io.\n5. **Extract Leads** flattens every business into a row and removes duplicates.\n6. **Find Emails (Hunter)** looks up an email for each business domain.\n7. **Save Leads to Sheet** appends each lead to your Google Sheet.\n\n### Setup\n- [ ] Install the community node `@searchapi/n8n-nodes-searchapi`.\n- [ ] Add your SearchApi credential (free key at searchapi.io) on **Scrape Google Maps**.\n- [ ] Add your Hunter.io credential on **Find Emails (Hunter)** \u2014 or delete that node + **Attach Emails** to skip emails.\n- [ ] Connect Google Sheets on **Save Leads to Sheet**; header row: business, category, location, type, address, phone, website, email, rating, reviews, maps_url, place_id.\n- [ ] Edit **Settings**: `categories` (comma-separated) and `locations` (semicolon-separated).\n- [ ] Run once to fill the sheet.\n\n### Customization tips\nRaise `PAGES` in Build Search Queries for more leads. Hunter uses one search credit per lead \u2014 remove the Hunter + Attach Emails nodes if you don't need emails."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky_configure",
      "name": "Section - Configure",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 640,
        "content": "## 1. Configure\nRun on demand or on a weekly schedule. Edit your categories and locations in **Settings** \u2014 nothing else needs touching."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky_scrape",
      "name": "Section - Scrape & Extract",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 668,
        "height": 644,
        "content": "## 2. Scrape & extract\nEach category + location becomes a live Google Maps search via SearchApi. Results from every page are flattened into one row per business and deduplicated by place."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky_save",
      "name": "Section - Save",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 644,
        "content": "## 4. Save leads\nAppends every lead to your Google Sheet: name, type, address, phone, website, email, rating, reviews and a Maps link."
      },
      "typeVersion": 1
    },
    {
      "id": "hunter",
      "name": "Find Emails (Hunter)",
      "type": "n8n-nodes-base.hunter",
      "notes": "Looks up the emails on file for each business domain via Hunter.io Domain Search. Businesses with no website/domain pass through with no email. Uses Hunter credits: one search per lead.",
      "onError": "continueRegularOutput",
      "position": [
        1184,
        304
      ],
      "parameters": {
        "limit": 10,
        "domain": "={{ $json.domain }}",
        "filters": {},
        "onlyEmails": false
      },
      "credentials": {
        "hunterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "attach",
      "name": "Attach Emails",
      "type": "n8n-nodes-base.code",
      "notes": "Attaches the best email (highest Hunter confidence) to each lead and passes the full row on to the sheet.",
      "position": [
        1408,
        304
      ],
      "parameters": {
        "jsCode": "// Merge Hunter's result back onto each lead (paired by position with Extract\n// Leads). Pick the highest-confidence email Hunter returned for the domain.\nconst items = $input.all();\nconst leads = $('Extract Leads').all();\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const lead = (leads[i] && leads[i].json) ? Object.assign({}, leads[i].json) : {};\n  const data = items[i].json || {};\n  const emails = Array.isArray(data.emails) ? data.emails : [];\n\n  let best = '';\n  if (emails.length) {\n    const sorted = emails.slice().sort((a, b) => (b.confidence || 0) - (a.confidence || 0));\n    best = sorted[0].value || '';\n  }\n  lead.email = best;\n  out.push({ json: lead });\n}\n\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "sticky_enrich",
      "name": "Section - Enrich",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1136,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 644,
        "content": "## 3. Enrich with emails\nHunter.io looks up the emails on file for each business domain; the highest-confidence one is attached to the lead. Delete these two nodes to skip email enrichment."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "versionId": "0dcd7e60-6b02-4054-9439-9c183591950b",
  "nodeGroups": [],
  "connections": {
    "Run Now": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Build Search Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Emails": {
      "main": [
        [
          {
            "node": "Save Leads to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Leads": {
      "main": [
        [
          {
            "node": "Find Emails (Hunter)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Weekly": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Google Maps": {
      "main": [
        [
          {
            "node": "Extract Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Search Queries": {
      "main": [
        [
          {
            "node": "Scrape Google Maps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Emails (Hunter)": {
      "main": [
        [
          {
            "node": "Attach Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}