This workflow corresponds to n8n.io template #13975 — we link there as the canonical source.
This workflow follows the HTTP Request → HubSpot recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": false
},
"nodes": [
{
"id": "a1000000-0000-0000-0000-000000000001",
"name": "When clicking 'Execute workflow'",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-2000,
400
],
"parameters": {},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000002",
"name": "Configure Lookup Parameters",
"type": "n8n-nodes-base.set",
"position": [
-1750,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-01",
"name": "emails",
"type": "string",
"value": "user@example.com,user@example.com"
},
{
"id": "cfg-02",
"name": "maxResults",
"type": "number",
"value": 5
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a1000000-0000-0000-0000-000000000003",
"name": "Start People Finder Scrape",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1500,
400
],
"parameters": {
"url": "https://app.scrapercity.com/api/v1/scrape/people-finder",
"method": "POST",
"options": {},
"jsonBody": "={\n \"name\": [],\n \"email\": {{ JSON.stringify($json.emails.split(',').map(e => e.trim())) }},\n \"phone_number\": [],\n \"street_citystatezip\": [],\n \"max_results\": {{ $json.maxResults }}\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "a1000000-0000-0000-0000-000000000004",
"name": "Store Run ID",
"type": "n8n-nodes-base.set",
"position": [
-1250,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "rid-01",
"name": "runId",
"type": "string",
"value": "={{ $json.runId }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a1000000-0000-0000-0000-000000000005",
"name": "Wait Before First Poll",
"type": "n8n-nodes-base.wait",
"position": [
-1000,
400
],
"parameters": {
"amount": 30
},
"typeVersion": 1.1
},
{
"id": "a1000000-0000-0000-0000-000000000006",
"name": "Poll Loop",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-750,
400
],
"parameters": {
"options": {
"reset": false
},
"batchSize": 1
},
"typeVersion": 3
},
{
"id": "a1000000-0000-0000-0000-000000000007",
"name": "Check Scrape Status",
"type": "n8n-nodes-base.httpRequest",
"position": [
-500,
400
],
"parameters": {
"url": "=https://app.scrapercity.com/api/v1/scrape/status/{{ $('Store Run ID').item.json.runId }}",
"method": "GET",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "a1000000-0000-0000-0000-000000000008",
"name": "Is Scrape Complete?",
"type": "n8n-nodes-base.if",
"position": [
-250,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-01",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "SUCCEEDED"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a1000000-0000-0000-0000-000000000009",
"name": "Wait Before Retry",
"type": "n8n-nodes-base.wait",
"position": [
-250,
592
],
"parameters": {
"amount": 60
},
"typeVersion": 1.1
},
{
"id": "a1000000-0000-0000-0000-000000000010",
"name": "Download Scrape Results",
"type": "n8n-nodes-base.httpRequest",
"position": [
0,
400
],
"parameters": {
"url": "=https://app.scrapercity.com/api/downloads/{{ $('Store Run ID').item.json.runId }}",
"method": "GET",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "a1000000-0000-0000-0000-000000000011",
"name": "Parse CSV Results",
"type": "n8n-nodes-base.code",
"position": [
250,
400
],
"parameters": {
"jsCode": "// Parse CSV text returned by the download endpoint\n// Handles both a raw CSV string and a JSON body with a 'data' or 'csv' field\n\nconst raw = $input.first().json;\nlet csvText = '';\n\nif (typeof raw === 'string') {\n csvText = raw;\n} else if (raw.data && typeof raw.data === 'string') {\n csvText = raw.data;\n} else if (raw.csv && typeof raw.csv === 'string') {\n csvText = raw.csv;\n} else {\n // Fallback: treat entire body as an array of contact objects\n const contacts = Array.isArray(raw) ? raw : (raw.results || []);\n return contacts.map(c => ({ json: c }));\n}\n\nconst lines = csvText.trim().split('\\n').filter(l => l.trim() !== '');\nif (lines.length < 2) return [];\n\nconst headers = lines[0].split(',').map(h => h.trim().replace(/^\"|\"$/g, ''));\n\nconst results = [];\nfor (let i = 1; i < lines.length; i++) {\n const values = lines[i].split(',').map(v => v.trim().replace(/^\"|\"$/g, ''));\n const obj = {};\n headers.forEach((h, idx) => {\n obj[h] = values[idx] || '';\n });\n results.push({ json: obj });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "a1000000-0000-0000-0000-000000000012",
"name": "Remove Duplicate Contacts",
"type": "n8n-nodes-base.removeDuplicates",
"position": [
500,
400
],
"parameters": {
"options": {},
"fieldsToCompare": {
"fields": [
{
"fieldName": "email"
}
]
}
},
"typeVersion": 2
},
{
"id": "a1000000-0000-0000-0000-000000000013",
"name": "Filter Valid Contacts",
"type": "n8n-nodes-base.filter",
"position": [
750,
400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "flt-01",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.email }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a1000000-0000-0000-0000-000000000014",
"name": "Map Contact Fields",
"type": "n8n-nodes-base.set",
"position": [
1000,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "map-01",
"name": "email",
"type": "string",
"value": "={{ ($json.email || '').trim().toLowerCase() }}"
},
{
"id": "map-02",
"name": "firstName",
"type": "string",
"value": "={{ $json.first_name || $json.firstName || '' }}"
},
{
"id": "map-03",
"name": "lastName",
"type": "string",
"value": "={{ $json.last_name || $json.lastName || '' }}"
},
{
"id": "map-04",
"name": "phone",
"type": "string",
"value": "={{ $json.phone || $json.phone_number || '' }}"
},
{
"id": "map-05",
"name": "address",
"type": "string",
"value": "={{ $json.address || $json.street || '' }}"
},
{
"id": "map-06",
"name": "city",
"type": "string",
"value": "={{ $json.city || '' }}"
},
{
"id": "map-07",
"name": "state",
"type": "string",
"value": "={{ $json.state || '' }}"
},
{
"id": "map-08",
"name": "zip",
"type": "string",
"value": "={{ $json.zip || $json.zipcode || '' }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a1000000-0000-0000-0000-000000000015",
"name": "Upsert Contact in HubSpot",
"type": "n8n-nodes-base.hubspot",
"position": [
1250,
400
],
"parameters": {
"email": "={{ $json.email }}",
"options": {},
"authentication": "appToken",
"additionalFields": {
"zip": "={{ $json.zip }}",
"city": "={{ $json.city }}",
"phone": "={{ $json.phone }}",
"state": "={{ $json.state }}",
"message": "=Address: {{ $json.address }}, {{ $json.city }}, {{ $json.state }} {{ $json.zip }}\nSource: ScraperCity People Finder reverse lookup",
"lastName": "={{ $json.lastName }}",
"firstName": "={{ $json.firstName }}"
}
},
"credentials": {
"hubspotAppToken": {
"name": "<your credential>"
}
},
"typeVersion": 2
},
{
"id": "a1000000-0000-0000-0000-000000000099",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2600,
100
],
"parameters": {
"width": 450,
"height": 580,
"content": "## How it works\n1. **Configure Lookup Parameters** -- set the email addresses you want to reverse-lookup and the max results per search.\n2. **Start People Finder Scrape** -- submits the emails to ScraperCity and receives an async job ID.\n3. **Polling loop** -- checks job status every 60 seconds until the scrape status is SUCCEEDED.\n4. **Download and parse** -- fetches the CSV result, parses rows into contact records, deduplicates by email, and filters out rows missing an email.\n5. **Upsert to HubSpot** -- maps name, phone, and address fields and creates or updates each contact in HubSpot CRM.\n\n## Setup steps\n1. Add a **ScraperCity API Key** credential: HTTP Header Auth, header `Authorization`, value `Bearer YOUR_KEY`.\n2. Add a **HubSpot App Token** credential using your HubSpot private app token.\n3. In **Configure Lookup Parameters**, replace the example emails with your target list (comma-separated).\n4. Click **Execute workflow** to run."
},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000100",
"name": "Section -- Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2040,
-160
],
"parameters": {
"color": 7,
"width": 730,
"height": 330,
"content": "## Configuration\n**Configure Lookup Parameters** is the only node you must edit. Enter target emails as a comma-separated string. **max_results** controls how many address records ScraperCity returns per email. All credentials are managed through n8n credential store -- no API keys in node parameters."
},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000101",
"name": "Section -- Scrape Submit",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1290,
-160
],
"parameters": {
"color": 7,
"width": 730,
"height": 330,
"content": "## Scrape Submission\n**Start People Finder Scrape** posts the email list to ScraperCity's People Finder endpoint. **Store Run ID** captures the returned `runId` so the polling loop can reference it. **Wait Before First Poll** pauses 30 seconds before the first status check."
},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000102",
"name": "Section -- Async Polling Loop",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-160
],
"parameters": {
"color": 7,
"width": 480,
"height": 530,
"content": "## Async Polling Loop\n**Poll Loop** iterates the status check. **Check Scrape Status** queries `/api/v1/scrape/status/{runId}`. **Is Scrape Complete?** routes to download on `SUCCEEDED`. Otherwise **Wait Before Retry** pauses 60 seconds and routes back into **Poll Loop** to retry. ScraperCity scrapes typically finish in 10--60 minutes."
},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000103",
"name": "Section -- Results Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
-40,
-160
],
"parameters": {
"color": 7,
"width": 730,
"height": 330,
"content": "## Results Processing\n**Download Scrape Results** fetches the completed CSV. **Parse CSV Results** converts rows to JSON objects. **Remove Duplicate Contacts** deduplicates by email field. **Filter Valid Contacts** drops any rows without an email address. **Map Contact Fields** normalises field names ready for HubSpot."
},
"typeVersion": 1
},
{
"id": "a1000000-0000-0000-0000-000000000104",
"name": "Section -- HubSpot Output",
"type": "n8n-nodes-base.stickyNote",
"position": [
710,
-160
],
"parameters": {
"color": 7,
"width": 730,
"height": 330,
"content": "## HubSpot Output\n**Upsert Contact in HubSpot** creates or updates a contact for each result using the email as the unique key. Address, phone, city, state, and zip are all written to standard HubSpot contact properties. Add a HubSpot App Token credential before running."
},
"typeVersion": 1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Poll Loop": {
"main": [
[
{
"node": "Check Scrape Status",
"type": "main",
"index": 0
}
],
[]
]
},
"Store Run ID": {
"main": [
[
{
"node": "Wait Before First Poll",
"type": "main",
"index": 0
}
]
]
},
"Parse CSV Results": {
"main": [
[
{
"node": "Remove Duplicate Contacts",
"type": "main",
"index": 0
}
]
]
},
"Wait Before Retry": {
"main": [
[
{
"node": "Poll Loop",
"type": "main",
"index": 0
}
]
]
},
"Map Contact Fields": {
"main": [
[
{
"node": "Upsert Contact in HubSpot",
"type": "main",
"index": 0
}
]
]
},
"Check Scrape Status": {
"main": [
[
{
"node": "Is Scrape Complete?",
"type": "main",
"index": 0
}
]
]
},
"Is Scrape Complete?": {
"main": [
[
{
"node": "Download Scrape Results",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait Before Retry",
"type": "main",
"index": 0
}
]
]
},
"Filter Valid Contacts": {
"main": [
[
{
"node": "Map Contact Fields",
"type": "main",
"index": 0
}
]
]
},
"Wait Before First Poll": {
"main": [
[
{
"node": "Poll Loop",
"type": "main",
"index": 0
}
]
]
},
"Download Scrape Results": {
"main": [
[
{
"node": "Parse CSV Results",
"type": "main",
"index": 0
}
]
]
},
"Remove Duplicate Contacts": {
"main": [
[
{
"node": "Filter Valid Contacts",
"type": "main",
"index": 0
}
]
]
},
"Start People Finder Scrape": {
"main": [
[
{
"node": "Store Run ID",
"type": "main",
"index": 0
}
]
]
},
"Configure Lookup Parameters": {
"main": [
[
{
"node": "Start People Finder Scrape",
"type": "main",
"index": 0
}
]
]
},
"When clicking 'Execute workflow'": {
"main": [
[
{
"node": "Configure Lookup Parameters",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthhubspotAppToken
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template is for sales teams, recruiters, and growth professionals who need to enrich a list of email addresses with full contact details -- names, phone numbers, and physical addresses -- and push verified new leads directly into HubSpot CRM without manual data entry.
Source: https://n8n.io/workflows/13975/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow uses the Zyte API to automatically detect and extract structured data from E-commerce sites, Articles, Job Boards, and Search Engine Results (SERP) - no custom CSS selectors required.
Automate LinkedIn lead generation by scraping comments from targeted posts and enriching profiles with detailed data
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This workflow runs a spider job in the background via Scrapyd, using a YAML config that defines selectors and parsing rules. When triggered, it schedules the spider with parameters (query, project ID,
This n8n workflow collects leads from Google Maps, scrapes their websites via direct HTTP requests, and extracts valid email addresses — all while mimicking real user behavior to improve scraping reli