{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Scrape Apollo.io leads and sync enriched contacts into Airtable via ScraperCity",
  "nodes": [
    {
      "id": "eca51c8c-dad6-472c-8af3-be7aca6a77bb",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        0
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Scrape Apollo.io leads and sync enriched contacts into Airtable via ScraperCity\n\n### How it works\n\n1. The workflow is triggered manually, and search parameters (job titles, industry, company size, lead count) are configured before initiating a scrape job on Apollo.io via the ScraperCity API.\n2. The scrape job ID is stored alongside Airtable configuration, then the workflow waits 60 seconds before polling ScraperCity for the job's completion status.\n3. If the scrape is not yet complete, the workflow waits another 60 seconds, re-attaches the run ID, and re-checks the status in a polling loop until the job finishes.\n4. Once complete, the raw lead results are downloaded from ScraperCity, then parsed and normalized into a consistent contact structure.\n5. Duplicate contacts are removed and only contacts with a valid email address are retained before the cleaned leads are saved to an Airtable base.\n\n### Setup steps\n\n- [ ] Create a ScraperCity account and obtain your API key for Apollo.io lead scraping\n- [ ] Set your desired search parameters (jobTitles, industry, companySize, leadCount) in the 'Configure Search Parameters' node\n- [ ] Update the 'Store Run ID' node with your Airtable Base ID and Table Name\n- [ ] Connect your Airtable account credentials in n8n and verify the 'Save Leads to Airtable' node points to the correct base and table\n- [ ] Ensure your Airtable table has columns matching the fields output by the 'Parse and Normalize Leads' node\n- [ ] Test the workflow by clicking 'Execute workflow' and monitoring the polling loop until leads appear in Airtable\n\n### Customization\n\nAdjust the two 60-second wait durations to suit expected scrape completion times \u2014 longer waits reduce API calls for large scrapes. Modify the filter in 'Filter Contacts with Email' to add additional quality criteria (e.g., phone number present, specific job title). The deduplication key in 'Remove Duplicate Contacts' can be changed from email to another unique field if needed."
      },
      "typeVersion": 1
    },
    {
      "id": "c4735e11-870f-427d-8238-bb2e3ed74669",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Workflow trigger and setup\n\nManually triggers the workflow and defines the lead search criteria (job titles, industry, company size, lead count) before any API calls are made."
      },
      "typeVersion": 1
    },
    {
      "id": "77ec30c6-3e32-4083-8487-14288d1671a8",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 336,
        "content": "## Initiate Apollo scrape job\n\nSends a POST request to ScraperCity to start the Apollo.io lead scrape, then stores the returned run ID alongside Airtable configuration for downstream use."
      },
      "typeVersion": 1
    },
    {
      "id": "4f033aba-270d-4178-b847-df43c0fbb94c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 320,
        "content": "## Poll scrape completion status\n\nWaits 60 seconds, then polls ScraperCity to check whether the scrape job is finished. Branches on the result \u2014 proceeding to download on success or entering the retry loop on pending."
      },
      "typeVersion": 1
    },
    {
      "id": "cc8aabbe-9fb9-43b8-8ecb-305d42e426fa",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 560,
        "content": "## Retry loop for pending scrapes\n\nIf the scrape is not yet complete, waits another 60 seconds, re-attaches the run ID and Airtable metadata, then loops back to re-check the status."
      },
      "typeVersion": 1
    },
    {
      "id": "0a3704b0-f51e-4815-99d0-34ee01c437e6",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1968,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 336,
        "content": "## Download and normalize leads\n\nDownloads the completed lead results from ScraperCity and runs a code node to parse and normalize the raw contact objects into a consistent, usable structure."
      },
      "typeVersion": 1
    },
    {
      "id": "c7704576-b166-459b-87c5-854aeba7e8c7",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 320,
        "content": "## Clean and save to Airtable\n\nRemoves duplicate contacts, filters out any records missing an email address, and writes the final clean lead list to the configured Airtable base and table."
      },
      "typeVersion": 1
    },
    {
      "id": "a1000000-0000-0000-0000-000000000001",
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        208,
        304
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a1000000-0000-0000-0000-000000000002",
      "name": "Configure Search Parameters",
      "type": "n8n-nodes-base.set",
      "position": [
        464,
        304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b1000001",
              "name": "jobTitles",
              "type": "string",
              "value": "CEO,CTO,VP of Sales"
            },
            {
              "id": "b1000002",
              "name": "industry",
              "type": "string",
              "value": "Technology"
            },
            {
              "id": "b1000003",
              "name": "companySize",
              "type": "string",
              "value": "11-50"
            },
            {
              "id": "b1000004",
              "name": "leadCount",
              "type": "number",
              "value": 100
            },
            {
              "id": "b1000005",
              "name": "airtableBaseId",
              "type": "string",
              "value": "appYOUR_BASE_ID"
            },
            {
              "id": "b1000006",
              "name": "airtableTableName",
              "type": "string",
              "value": "Leads"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1000000-0000-0000-0000-000000000003",
      "name": "Start Apollo Lead Scrape",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        720,
        304
      ],
      "parameters": {
        "url": "https://app.scrapercity.com/api/v1/scrape/apollo-filters",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personTitles\": {{ JSON.stringify($json.jobTitles.split(',').map(t => t.trim())) }},\n  \"companyIndustry\": \"{{ $json.industry }}\",\n  \"companySize\": \"{{ $json.companySize }}\",\n  \"count\": {{ $json.leadCount }},\n  \"fileName\": \"Apollo Export {{ $now.toISO() }}\"\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": [
        976,
        304
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "c1000001",
              "name": "runId",
              "type": "string",
              "value": "={{ $json.runId }}"
            },
            {
              "id": "c1000002",
              "name": "airtableBaseId",
              "type": "string",
              "value": "={{ $('Configure Search Parameters').item.json.airtableBaseId }}"
            },
            {
              "id": "c1000003",
              "name": "airtableTableName",
              "type": "string",
              "value": "={{ $('Configure Search Parameters').item.json.airtableTableName }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1000000-0000-0000-0000-000000000005",
      "name": "Wait 60 Seconds Before Poll",
      "type": "n8n-nodes-base.wait",
      "position": [
        1248,
        304
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 60
      },
      "typeVersion": 1.1
    },
    {
      "id": "a1000000-0000-0000-0000-000000000006",
      "name": "Check Scrape Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1504,
        304
      ],
      "parameters": {
        "url": "=https://app.scrapercity.com/api/v1/scrape/status/{{ $json.runId }}",
        "method": "GET",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a1000000-0000-0000-0000-000000000007",
      "name": "Is Scrape Complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        1760,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "d1000001",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "SUCCEEDED"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a1000000-0000-0000-0000-000000000008",
      "name": "Wait 60 Seconds Before Retry",
      "type": "n8n-nodes-base.wait",
      "position": [
        2016,
        768
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 60
      },
      "typeVersion": 1.1
    },
    {
      "id": "a1000000-0000-0000-0000-000000000009",
      "name": "Preserve Run ID on Retry",
      "type": "n8n-nodes-base.set",
      "position": [
        2288,
        528
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e1000001",
              "name": "runId",
              "type": "string",
              "value": "={{ $json.runId }}"
            },
            {
              "id": "e1000002",
              "name": "airtableBaseId",
              "type": "string",
              "value": "={{ $('Configure Search Parameters').item.json.airtableBaseId }}"
            },
            {
              "id": "e1000003",
              "name": "airtableTableName",
              "type": "string",
              "value": "={{ $('Configure Search Parameters').item.json.airtableTableName }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1000000-0000-0000-0000-000000000010",
      "name": "Download Lead Results",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2016,
        176
      ],
      "parameters": {
        "url": "=https://app.scrapercity.com/api/downloads/{{ $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 and Normalize Leads",
      "type": "n8n-nodes-base.code",
      "position": [
        2288,
        176
      ],
      "parameters": {
        "jsCode": "// ScraperCity returns an array of contact objects.\n// Normalize each lead into a flat structure for Airtable.\nconst raw = Array.isArray($input.first().json) ? $input.first().json : ($input.first().json.contacts || $input.first().json.data || []);\n\nconst results = [];\nfor (const contact of raw) {\n  results.push({\n    json: {\n      firstName: contact.first_name || contact.firstName || '',\n      lastName: contact.last_name || contact.lastName || '',\n      fullName: contact.name || `${contact.first_name || ''} ${contact.last_name || ''}`.trim(),\n      jobTitle: contact.title || contact.job_title || '',\n      company: contact.organization_name || contact.company || '',\n      workEmail: contact.email || contact.work_email || '',\n      personalEmail: contact.personal_email || '',\n      phone: contact.phone || contact.primary_phone || '',\n      linkedinUrl: contact.linkedin_url || '',\n      city: contact.city || '',\n      country: contact.country || '',\n      companyWebsite: contact.account ? (contact.account.website_url || '') : (contact.website_url || '')\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-0000-0000-000000000012",
      "name": "Remove Duplicate Contacts",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        2544,
        304
      ],
      "parameters": {
        "compare": "selectedFields",
        "options": {},
        "fieldsToCompare": {
          "fields": [
            {
              "fieldName": "workEmail"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-0000-0000-000000000013",
      "name": "Filter Contacts with Email",
      "type": "n8n-nodes-base.filter",
      "position": [
        2800,
        304
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1000001",
              "operator": {
                "type": "string",
                "operation": "isNotEmpty"
              },
              "leftValue": "={{ $json.workEmail }}",
              "rightValue": ""
            },
            {
              "id": "f1000002",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.workEmail }}",
              "rightValue": "@"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a1000000-0000-0000-0000-000000000014",
      "name": "Save Leads to Airtable",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3056,
        304
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Store Run ID').item.json.airtableBaseId }}"
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Store Run ID').item.json.airtableTableName }}"
        },
        "columns": {
          "value": {
            "City": "={{ $json.city }}",
            "Phone": "={{ $json.phone }}",
            "Company": "={{ $json.company }}",
            "Country": "={{ $json.country }}",
            "Full Name": "={{ $json.fullName }}",
            "Job Title": "={{ $json.jobTitle }}",
            "Last Name": "={{ $json.lastName }}",
            "First Name": "={{ $json.firstName }}",
            "Work Email": "={{ $json.workEmail }}",
            "LinkedIn URL": "={{ $json.linkedinUrl }}",
            "Personal Email": "={{ $json.personalEmail }}",
            "Company Website": "={{ $json.companyWebsite }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Store Run ID": {
      "main": [
        [
          {
            "node": "Wait 60 Seconds Before Poll",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Scrape Status": {
      "main": [
        [
          {
            "node": "Is Scrape Complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Scrape Complete?": {
      "main": [
        [
          {
            "node": "Download Lead Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 60 Seconds Before Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Lead Results": {
      "main": [
        [
          {
            "node": "Parse and Normalize Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preserve Run ID on Retry": {
      "main": [
        [
          {
            "node": "Check Scrape Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start Apollo Lead Scrape": {
      "main": [
        [
          {
            "node": "Store Run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and Normalize Leads": {
      "main": [
        [
          {
            "node": "Remove Duplicate Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remove Duplicate Contacts": {
      "main": [
        [
          {
            "node": "Filter Contacts with Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Contacts with Email": {
      "main": [
        [
          {
            "node": "Save Leads to Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Search Parameters": {
      "main": [
        [
          {
            "node": "Start Apollo Lead Scrape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 60 Seconds Before Poll": {
      "main": [
        [
          {
            "node": "Check Scrape Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 60 Seconds Before Retry": {
      "main": [
        [
          {
            "node": "Preserve Run ID on Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Configure Search Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}