{
  "name": "Google Maps leads with emails to HubSpot contacts (Hubertino)",
  "nodes": [
    {
      "id": "cb2e1081-efeb-4ed6-8a33-3fafa29ed5d5",
      "parameters": {
        "content": "## Google Maps leads with emails → HubSpot contacts\n**Who it's for:** teams that prospect local businesses and work their pipeline in HubSpot.\n\n**What it does:** runs a [Hubertino](https://hubertino.com/?utm_source=n8n&utm_medium=template) Google Maps scrape, waits for it to finish, and creates or updates one HubSpot contact per business email, with company name, phone, website and address filled in.\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?** drops businesses without an email, since HubSpot contacts are keyed on email.\n5. **Create or update contact** upserts by email, so re-runs update existing contacts instead of duplicating them.\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. HubSpot**\nCreate a HubSpot *private app* with the `crm.objects.contacts.read` and `crm.objects.contacts.write` scopes, add its token as a **HubSpot App Token** credential in n8n, and select it in **Create or update contact**. OAuth2 works too: switch *Authentication* in that node.\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": "286632fd-0cf8-425b-8612-9df1c7464f63",
      "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": "d65dd5ae-4016-4b6e-86a9-25648032d4c0",
      "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": "4df33f4d-5ac2-4e0b-a148-8294b5aacc67",
      "parameters": {
        "content": "## Field mapping\nHubertino → HubSpot contact:\n- `email` → Email (upsert key)\n- `name` → Company name\n- `phone` → Phone number\n- `website` → Website URL\n- `street`, `city`, `state`, `postal_code`, `country` → address fields\n\nAdd more under *Additional Fields*. Other columns you can map: `category`, `rating`, `reviews`, `company_linkedin`, `company_facebook`, `location_link`, `place_id`.",
        "height": 360,
        "width": 400,
        "color": 6
      },
      "name": "Field mapping",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1700,
        40
      ]
    },
    {
      "id": "6a419e81-68ec-40fa-b78b-bb3a396e3bc8",
      "parameters": {},
      "name": "When clicking ‘Execute workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "id": "1ecfd6d8-515f-43db-b543-468b30e80940",
      "parameters": {
        "method": "POST",
        "url": "https://hubertino.com/api/v1/scrapes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"categories\": [\n    \"roofing contractor\"\n  ],\n  \"locations\": [\n    \"Denver, CO, 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": "de2b5068-1404-442b-bcda-aaf5192bd641",
      "parameters": {
        "amount": 30,
        "unit": "seconds"
      },
      "name": "Wait 30 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        440,
        0
      ],
      "webhookId": "2d4d484f-90d2-4a77-93dd-2b9751973347"
    },
    {
      "id": "51e6fde7-28d9-4607-a9d9-5fd8c886da80",
      "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": "9be3e25b-ba6d-418b-92fa-620cb99c176e",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "782ba89a-7613-4770-8faa-8979f3454d21",
              "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": "6fbe7aa6-8457-4834-8bab-7e5fb97615f1",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "6ad6d394-59a5-4dee-851b-f1f9bfdcc547",
              "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": "69b0b871-7183-4001-8741-007a7c790167",
      "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": "313960af-123c-405f-b6d9-229ec805c140",
      "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": "de676ce9-48a8-42b2-bc8c-0ee2c822a236",
      "parameters": {
        "fieldToSplitOut": "rows",
        "options": {}
      },
      "name": "One item per place",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1320,
        -200
      ]
    },
    {
      "id": "25dda814-c404-4001-b69f-43899b427115",
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "9e43e6eb-1c59-4323-bbe9-9d8a817ce11d",
              "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": "f1516105-b2fc-4566-bec2-d18bbee0981b",
      "parameters": {
        "authentication": "appToken",
        "email": "={{ $json.email }}",
        "additionalFields": {
          "companyName": "={{ $json.name }}",
          "phoneNumber": "={{ $json.phone }}",
          "websiteUrl": "={{ $json.website }}",
          "streetAddress": "={{ $json.street }}",
          "city": "={{ $json.city }}",
          "stateRegion": "={{ $json.state }}",
          "postalCode": "={{ $json.postal_code }}",
          "country": "={{ $json.country }}"
        },
        "options": {}
      },
      "name": "Create or update contact",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 2.1,
      "position": [
        1760,
        -220
      ],
      "notes": "HubSpot upsert by email."
    }
  ],
  "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": "Create or update contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}
