{
  "name": "Google Maps leads with emails to Google Sheets (Hubertino)",
  "nodes": [
    {
      "id": "4a9561d5-0759-4304-88f4-3e07f6d59313",
      "parameters": {
        "content": "## Google Maps leads with emails → Google Sheets\n**Who it's for:** agencies, sales teams and freelancers building local-business lead lists.\n\n**What it does:** runs a [Hubertino](https://hubertino.com/?utm_source=n8n&utm_medium=template) Google Maps scrape (for example *dentists in Austin, TX*), waits for it to finish, and writes every business that has an email into a Google Sheet: name, email, phone, website, address, rating, socials and the Maps link.\n\n**How it works**\n1. **Start scrape** sends your search to the Hubertino API.\n2. The loop polls the scrape every 30 s until it's done.\n3. **Fetch all rows** pages through the results, 1,000 at a time.\n4. **Has email?** keeps rows with an email. Delete it to keep every business.\n5. **Upsert into Google Sheet** appends new places and updates known ones, matched on `place_id`, so re-runs never duplicate rows.\n\n**1. Hubertino API key**\nSign up at [hubertino.com](https://hubertino.com/?utm_source=n8n&utm_medium=template) (100 free credits, no card) → **Dashboard → Settings → API key**. The key starts with `hub_live_` and is shown once.\nIn n8n, create one **Bearer Auth** credential, paste the key as the *Bearer Token*, and select it in every HTTP Request node (Start scrape, Get scrape status, Fetch all rows).\n*Older n8n without Bearer Auth?* Use **Header Auth** with name `Authorization` and value `Bearer hub_live_…`.\n\n**2. Google Sheet**\nCreate a spreadsheet with a tab named `Leads` (it can be empty; the header row is written on the first run). Connect Google Sheets in **Upsert into Google Sheet** and pick the file.\n\n**Credits.** 1 credit = 1 business delivered. A scrape reserves categories × locations × maxResults credits (capped at your balance) and refunds whatever it didn't use when it finishes. Emails and social links cost nothing extra.",
        "height": 900,
        "width": 560
      },
      "name": "About this workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -620,
        -380
      ]
    },
    {
      "id": "c4d8b392-d00c-48d3-ab16-04b0b5984eda",
      "parameters": {
        "content": "## Edit your search here\nOpen **Start scrape** and change the JSON body:\n```\n{\n  \"categories\": [\"dentist\"],\n  \"locations\": [\"Austin, TX, United States\"],\n  \"maxResults\": 50\n}\n```\n- `maxResults`: places per category × location pair (1–500)\n- up to 2,000 pairs per scrape\n- optional `\"country\": \"gb\"` picks the Google country edition (otherwise it's inferred from the location)\n- 12 new scrapes per 5 minutes per account",
        "height": 380,
        "width": 380,
        "color": 5
      },
      "name": "Edit your search",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        160,
        160
      ]
    },
    {
      "id": "884d5f6b-c69b-45a2-a845-5a3af5962a89",
      "parameters": {
        "content": "## Wait for the scrape\nA scrape takes a few minutes. This loop checks the status every 30 s: `queued` → `running` → `done`. If the scrape ends in `error`, the workflow stops and the reserved credits are refunded automatically.\nThere are no webhooks, so polling is the supported pattern.",
        "height": 260,
        "width": 420,
        "color": 7
      },
      "name": "Polling loop",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        620,
        160
      ]
    },
    {
      "id": "830d93ee-418d-41db-ad86-1e5aab18b711",
      "parameters": {},
      "name": "When clicking ‘Execute workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "id": "4faa8de9-802b-41f9-a100-73550fbbb0e5",
      "parameters": {
        "method": "POST",
        "url": "https://hubertino.com/api/v1/scrapes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"categories\": [\n    \"dentist\"\n  ],\n  \"locations\": [\n    \"Austin, TX, United States\"\n  ],\n  \"maxResults\": 50,\n  \"enrichWebsite\": true,\n  \"enrichLinks\": true\n}",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Start scrape",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        220,
        0
      ],
      "notes": "POST /scrapes. Edit the JSON body to change the search."
    },
    {
      "id": "0aecc879-765f-4ae4-91fb-ab2b4e8f357d",
      "parameters": {
        "amount": 30,
        "unit": "seconds"
      },
      "name": "Wait 30 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        440,
        0
      ],
      "webhookId": "2ee09db6-d0ef-4669-8c61-b25aa67664e4"
    },
    {
      "id": "6ce05c67-18f5-4243-b0e2-635d98dfa41d",
      "parameters": {
        "url": "=https://hubertino.com/api/v1/scrapes/{{ $('Start scrape').first().json.scrape.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 60000
        }
      },
      "name": "Get scrape status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "f76356bf-6c6c-4205-9404-4fd24517b5cb",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "889e7b82-0ef8-4372-9781-f5cbb797d8a8",
              "leftValue": "={{ $json.scrape.status }}",
              "rightValue": "done",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Scrape finished?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "id": "b7be47ab-ad31-472a-b778-f9f6f508574a",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "eaabd7e4-cdde-4ea6-8b68-717628eec30c",
              "leftValue": "={{ $json.scrape.status }}",
              "rightValue": "error",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Scrape failed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        200
      ]
    },
    {
      "id": "efa3dabc-0b88-4141-940a-746a4daead6e",
      "parameters": {
        "errorMessage": "=Hubertino scrape {{ $json.scrape.id }} failed: {{ $json.scrape.error }}"
      },
      "name": "Stop: scrape failed",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1320,
        180
      ]
    },
    {
      "id": "834ffb71-3cf5-4682-b078-bc8bee8d502e",
      "parameters": {
        "url": "=https://hubertino.com/api/v1/scrapes/{{ $('Start scrape').first().json.scrape.id }}/results",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "1000"
            }
          ]
        },
        "options": {
          "timeout": 120000,
          "pagination": {
            "pagination": {
              "parameters": {
                "parameters": [
                  {
                    "type": "qs",
                    "name": "offset",
                    "value": "={{ $pageCount * 1000 }}"
                  }
                ]
              },
              "paginationCompleteWhen": "other",
              "completeExpression": "={{ !$response.body.hasMore }}",
              "limitPagesFetched": true,
              "maxRequests": 100
            }
          }
        }
      },
      "name": "Fetch all rows",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        -200
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "notes": "GET /scrapes/{id}/results, 1000 rows per page, until hasMore is false."
    },
    {
      "id": "ac5c307c-a050-4aac-bafc-5e141fbb6e9a",
      "parameters": {
        "fieldToSplitOut": "rows",
        "options": {}
      },
      "name": "One item per place",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1320,
        -200
      ]
    },
    {
      "id": "a91a4386-4402-40aa-be8b-919791093fbd",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ad1b5cee-8a89-4190-85e2-8ea356f26305",
              "leftValue": "={{ $json.email }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "name": "Has email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        -200
      ]
    },
    {
      "id": "92cf1aaa-ec40-4e2c-a17c-5d810deb7678",
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8e44d38c-f87a-4dfb-8167-0e8e1d0ffff2",
              "name": "place_id",
              "value": "={{ $json.place_id || $json.google_id || $json.cid }}",
              "type": "string"
            },
            {
              "id": "ee6e893f-dedb-4472-af36-03cee1f8ce7a",
              "name": "name",
              "value": "={{ $json.name }}",
              "type": "string"
            },
            {
              "id": "b73b4707-94e6-4f62-87dc-3c5b2be3a7a4",
              "name": "category",
              "value": "={{ $json.category }}",
              "type": "string"
            },
            {
              "id": "a7899658-e514-4ef9-9ab1-17027c3f8474",
              "name": "email",
              "value": "={{ $json.email }}",
              "type": "string"
            },
            {
              "id": "985e6a07-c276-4f37-8f77-5cb4bf0f638a",
              "name": "phone",
              "value": "={{ $json.phone }}",
              "type": "string"
            },
            {
              "id": "b09e0f3b-6e30-424a-960d-9150bd94ecd9",
              "name": "website",
              "value": "={{ $json.website }}",
              "type": "string"
            },
            {
              "id": "6915754b-7de6-4b5d-b0bb-408a1175f3bd",
              "name": "address",
              "value": "={{ $json.address }}",
              "type": "string"
            },
            {
              "id": "af098c3f-a492-450b-9e0b-e69145671580",
              "name": "city",
              "value": "={{ $json.city }}",
              "type": "string"
            },
            {
              "id": "9286940b-b40c-4ae6-bbd2-1e02ea5552a5",
              "name": "state",
              "value": "={{ $json.state }}",
              "type": "string"
            },
            {
              "id": "7ed77321-ae36-4f34-ac4b-a674fa7d5b6b",
              "name": "postal_code",
              "value": "={{ $json.postal_code }}",
              "type": "string"
            },
            {
              "id": "7eb3f788-d533-40c3-8265-350b576ec44d",
              "name": "country_code",
              "value": "={{ $json.country_code }}",
              "type": "string"
            },
            {
              "id": "31cf8977-1b59-486b-94ae-466920b3505e",
              "name": "rating",
              "value": "={{ $json.rating }}",
              "type": "number"
            },
            {
              "id": "3eb4b8b6-d391-491e-8439-0906fc7eeba3",
              "name": "reviews",
              "value": "={{ $json.reviews }}",
              "type": "number"
            },
            {
              "id": "180dc426-e0b9-46cf-af93-45cfd3f1a085",
              "name": "company_facebook",
              "value": "={{ $json.company_facebook }}",
              "type": "string"
            },
            {
              "id": "b0b1ea74-dd3f-4571-9234-6b8b4386614e",
              "name": "company_instagram",
              "value": "={{ $json.company_instagram }}",
              "type": "string"
            },
            {
              "id": "f2ea2e23-f549-4564-8947-f12bae002598",
              "name": "company_linkedin",
              "value": "={{ $json.company_linkedin }}",
              "type": "string"
            },
            {
              "id": "2cf9a585-ddb2-47ed-8633-564d0db9b8b7",
              "name": "company_x",
              "value": "={{ $json.company_x }}",
              "type": "string"
            },
            {
              "id": "b1cdfa34-df26-4300-80d1-b08f88b1e318",
              "name": "location_link",
              "value": "={{ $json.location_link }}",
              "type": "string"
            },
            {
              "id": "abcba6ca-e602-404c-b28f-079d03dd898e",
              "name": "query",
              "value": "={{ $json.query }}",
              "type": "string"
            },
            {
              "id": "bbdb7398-1591-4cff-b30c-214406046842",
              "name": "hubertino_scrape_id",
              "value": "={{ $('Start scrape').first().json.scrape.id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "name": "Pick lead columns",
      "position": [
        1760,
        -220
      ]
    },
    {
      "id": "64069b3a-7b97-461e-be07-653b23b5b5e8",
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "",
          "mode": "list",
          "cachedResultName": ""
        },
        "sheetName": {
          "__rl": true,
          "value": "Leads",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "place_id"
          ],
          "schema": []
        },
        "options": {
          "cellFormat": "RAW"
        }
      },
      "name": "Upsert into Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1980,
        -220
      ],
      "notes": "Append new places, update known ones (matched on place_id). RAW keeps +1 phone numbers and ZIP codes as text."
    }
  ],
  "connections": {
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Start scrape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start scrape": {
      "main": [
        [
          {
            "node": "Wait 30 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30 seconds": {
      "main": [
        [
          {
            "node": "Get scrape status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get scrape status": {
      "main": [
        [
          {
            "node": "Scrape finished?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape finished?": {
      "main": [
        [
          {
            "node": "Fetch all rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape failed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape failed?": {
      "main": [
        [
          {
            "node": "Stop: scrape failed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 30 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch all rows": {
      "main": [
        [
          {
            "node": "One item per place",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One item per place": {
      "main": [
        [
          {
            "node": "Has email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has email?": {
      "main": [
        [
          {
            "node": "Pick lead columns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick lead columns": {
      "main": [
        [
          {
            "node": "Upsert into Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
