{
  "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
          }
        ]
      ]
    }
  }
}