Softechinfra
Productivity

n8n + Apollo + HubSpot: Lead Enrichment That Saves 6 Hours a Week

A real form-to-CRM enrichment workflow on n8n v1.121. Full node JSON, Apollo credit math, and the four traps that quietly burn your enrichment budget.

Hrishikesh BaidyaHrishikesh Baidya
March 27, 202613 min read
n8n + Apollo + HubSpot: Lead Enrichment That Saves 6 Hours a Week

Our sales team got 312 form submissions last month. Before this workflow, an SDR spent ~6 hours/week on Apollo lookups, copy-pasting fields into HubSpot, sizing the company, and pinging Slack. We rebuilt it in n8n in two afternoons: 18 nodes, 6 hours/week back. Below is the exact workflow we run for our own sales pipeline — not a generic template, the live one.

18
Nodes (incl. error branches)
6 hrs
SDR time saved per week
93%
Apollo match rate (B2B Indian leads)

TL;DR — What does this enrichment workflow do?

A web form submits to n8n. The workflow takes the work email, runs an Apollo people/match API call to pull the lead's title, LinkedIn, seniority, and employer; then a second Apollo organizations/enrich call gets headcount, revenue band, and tech stack. Claude Haiku scores the lead 0–100 against your ICP. n8n creates a HubSpot contact + company, attaches the score as a custom property, and posts a structured Slack ping to #leads-new. End-to-end runtime: ~7 seconds.

Why this matters now — March 2026

Apollo's per-credit pricing held flat in 2026 but the new "Person+Org enrichment in one call" endpoint (rolled out Feb 2026) lets you do both lookups in a single API hit — that is two credits down to one for the same data. HubSpot's free-tier custom property limit also moved from 10 to 25 in March 2026, which is when this workflow finally became viable on the free CRM tier.

The trigger for us: our SDR Anjali calculated she was spending 1h 12m a day on enrichment grunt work. At her fully loaded cost, that is a real slice of her year going into copy-paste. The workflow paid for itself in 9 working days.

The 5-step pipeline

📥
Step 1: Form webhook
Tally / Typeform / Webflow form POSTs to /webhook/lead-in. Spam/free-email gate at the door.
🔍
Step 2: Apollo enrich
Single API call returns person + company data. ~1 credit per match, ~93% hit rate on Indian B2B emails.
⚖️
Step 3: ICP score
Claude Haiku 4.5 scores 0–100 against your written ICP rubric. Returns score + reasoning.
📊
Step 4: HubSpot upsert + Slack
Create or update contact + company in HubSpot. Slack ping with score, summary, and an "assign to me" button.

Prerequisites

  • n8n v1.121+ (self-hosted or Cloud — workflow uses no proprietary nodes)
  • Apollo.io account with API access (Basic plan includes 600 enrichment credits)
  • HubSpot account (free tier works; Pro needed for the workflow association API)
  • Anthropic API key with Claude Haiku 4.5 access
  • Slack workspace with bot token, scope chat:write
  • A web form that can POST JSON — we use Tally, but Typeform/Webflow/native HTML all work

Step 1 — The form webhook + spam gate

The form sends a JSON payload to https://n8n.softechinfra.com/webhook/lead-in. The first n8n node is a standard Webhook in production mode. Right after, a Set + IF combination kills three classes of garbage before they cost an Apollo credit:

  • 1. Free email domains (gmail.com, yahoo.com, hotmail.com — about 22% of our inbound) — these almost never convert B2B, route to nurture instead of enrichment.
  • 2. Disposable domains (mailinator.com, tempmail.org) — straight to a quarantine list.
  • 3. Honeypot trigger — our form has a hidden "website2" field; any submission with it populated is a bot.
  • The gating Code node:

    javascript
    const FREE_DOMAINS = new Set([
        'gmail.com','yahoo.com','hotmail.com','outlook.com','live.com',
        'icloud.com','rediffmail.com','aol.com','protonmail.com'
      ]);
      const DISPOSABLE = new Set([
        'mailinator.com','tempmail.org','10minutemail.com','guerrillamail.com',
        'throwaway.email','yopmail.com'
      ]);
      const email = ($json.body.email || '').toLowerCase().trim();
      const domain = email.split('@')[1] || '';
      
      if ($json.body.website2) return { json: { route: 'honeypot' } };
      if (DISPOSABLE.has(domain)) return { json: { route: 'spam' } };
      if (FREE_DOMAINS.has(domain)) return { json: { route: 'nurture', email, domain } };
      if (!email.includes('@') || !domain.includes('.')) return { json: { route: 'invalid' } };
      
      return { json: { route: 'enrich', email, domain, ...$json.body } };

    The Switch node downstream branches on route. Only route === 'enrich' triggers Apollo. This gate alone saves us ~70 Apollo credits/month.

    Step 2 — Apollo people enrichment

    The combined enrichment endpoint (rolled out by Apollo in Feb 2026) is POST https://api.apollo.io/v1/people/match with the reveal_personal_emails=false and reveal_organization=true flags. One credit, two enrichments.

    Node JSON:

    json
    {
        "parameters": {
          "method": "POST",
          "url": "https://api.apollo.io/v1/people/match",
          "sendHeaders": true,
          "headerParameters": {
            "parameters": [
              { "name": "x-api-key", "value": "={{ $credentials.apolloApi.apiKey }}" },
              { "name": "Content-Type", "value": "application/json" },
              { "name": "Cache-Control", "value": "no-cache" }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={\n  \"email\": \"{{ $json.email }}\",\n  \"reveal_personal_emails\": false,\n  \"reveal_phone_number\": false,\n  \"reveal_organization\": true\n}",
          "options": {
            "timeout": 20000,
            "retry": { "enabled": true, "maxRetries": 2 }
          }
        },
        "id": "apollo-enrich-001",
        "name": "Apollo Match Person+Org",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [680, 300]
      }

    Apollo's response on a hit gives you about 60 fields. The ones we keep:

  • person.name, person.title, person.seniority, person.linkedin_url
  • organization.name, organization.website_url, organization.estimated_num_employees, organization.annual_revenue_printed, organization.industry, organization.technologies (top 10)
  • A Set node trims the firehose down to the 14 fields HubSpot actually wants.

    Apollo credit math, real numbers from our March 2026 usage:

  • 312 submissions/month
  • Gate kills ~88 (free email + spam + honeypot) → 224 hit Apollo
  • ~7% are unmatched (Apollo returns null for the person) → 208 successful matches
  • 1 credit per match × 224 attempts = 224 credits
  • Apollo Basic includes 600 credits → headroom for 2.7× growth
  • If you outgrow Basic, the Professional plan bumps you to 1,500 credits. Per-credit cost stays flat.

    Step 3 — Claude Haiku ICP scoring

    We score every enriched lead 0–100 against our written ICP. The ICP rubric lives in a Notion doc that we paste into Claude's system prompt; we re-tune it monthly.

    Claude system prompt (the one we actually run):

    code
    You are a B2B lead scorer for Softechinfra, an Indian IT services firm.
      Score this lead 0-100 against our ICP and return strict JSON only.
      
      ICP heavy positives:
      - Indian-domiciled company, headcount 20-500
      - Industries: D2C, edtech, fintech (RBI-regulated wallet/UPI), logistics, healthcare
      - Title contains: CTO, VP Engineering, Head of Product, COO, Founder
      - Tech stack includes: Node.js, Python, AWS, MongoDB
      - Revenue band: ₹5 Cr - ₹500 Cr
      
      ICP soft positives:
      - SaaS or AI-services adjacent
      - Title contains: Engineering Manager, Tech Lead
      
      ICP negatives (cap score at 30):
      - Headcount <10 or >2000
      - Pure agency / consultancy (we compete)
      - Personal Gmail-only lead
      - Crypto-only company
      
      Schema: { score: int 0-100, tier: 'A'|'B'|'C'|'D', reasoning: string max 200 chars, suggested_action: string max 100 chars }
      
      Tier mapping: A=80-100, B=60-79, C=40-59, D=0-39.

    User message body, built from Apollo's response:

    code
    Lead: {{ $json.person.name }}, {{ $json.person.title }} at {{ $json.organization.name }}.
      Company: {{ $json.organization.estimated_num_employees }} employees, {{ $json.organization.annual_revenue_printed }}, industry: {{ $json.organization.industry }}.
      Tech stack: {{ $json.organization.technologies.slice(0,8).join(', ') }}.
      Submitted message: "{{ $('Webhook').first().json.body.message }}".

    Input is ~500 tokens, output ~80. Across 208 scored leads the scoring cost is basically free.

    Our Claude HTTP Request node uses the same structure as Post 1's, with model claude-haiku-4-5-20251115. We pin model versions explicitly — claude-haiku-latest aliases have flipped on us mid-week before.

    Step 4 — HubSpot upsert + Slack ping

    Two parallel branches. HubSpot first.

    The HubSpot Contact node creates-or-updates by email:

    json
    {
        "parameters": {
          "operation": "upsert",
          "email": "={{ $json.email }}",
          "additionalFields": {
            "firstname": "={{ $json.person.first_name }}",
            "lastname": "={{ $json.person.last_name }}",
            "jobtitle": "={{ $json.person.title }}",
            "linkedinurl": "={{ $json.person.linkedin_url }}",
            "lifecyclestage": "lead",
            "lead_source": "Web Form - Enriched",
            "icp_score": "={{ $json.score }}",
            "icp_tier": "={{ $json.tier }}",
            "icp_reasoning": "={{ $json.reasoning }}",
            "apollo_match_at": "={{ $now.toISO() }}"
          }
        },
        "id": "hubspot-contact-001",
        "name": "Upsert HubSpot Contact",
        "type": "n8n-nodes-base.hubspot",
        "typeVersion": 2.1,
        "position": [1380, 240]
      }

    We then call a HubSpot Company node with the same upsert pattern using organization.website_url as the dedup key, and finally associate the two via the HubSpot Associations API (the native node handles it in v2.1+).

    Three custom HubSpot properties you have to create once: icp_score (number), icp_tier (single-line text, enum-style), apollo_match_at (datetime). The free tier allows 25 custom contact properties as of March 2026.

    The Slack branch — a single Slack node posting a structured message to #leads-new:

    code
    :sparkles: *New lead, tier {{ $json.tier }} ({{ $json.score }}/100)*
      {{ $json.person.name }} — {{ $json.person.title }} at *{{ $json.organization.name }}*
      {{ $json.organization.estimated_num_employees }} ppl · {{ $json.organization.industry }} · {{ $json.organization.annual_revenue_printed }}
      > _{{ $json.reasoning }}_
      > _Action:_ {{ $json.suggested_action }}
      :hubspot: <https://app.hubspot.com/contacts/{{ $env.HUBSPOT_PORTAL_ID }}/contact/{{ $('Upsert HubSpot Contact').first().json.id }}|Open in HubSpot>

    Tier A pings @sales-lead with a notification; B/C/D drop quietly into the channel.

    Cost comparison vs the manual SDR workflow

    For 312 leads/month (our actual volume):

    n8n + Apollo Basic Clearbit + Zapier SDR @ 1h/day manually ZoomInfo + outsourced SDR

    Our n8n + Apollo bar breaks down as: Apollo Basic + Anthropic + Hetzner share + HubSpot free + Slack free + minor n8n storage, plus a buffer.

    When NOT to do this

    Three contexts where this workflow is not the right call.

    You have under 40 leads/month. Apollo's per-credit waste plus the workflow ops overhead exceeds the SDR's actual time. Use a spreadsheet and 15 min/day until you cross ~100/month.

    Your ICP is consumer (B2C). Apollo is a B2B database. Pulling a college student or a homemaker returns no data and burns the credit anyway. For B2C, use Clearbit Reveal on the website-visit side instead.

    You sell into Tier-2/Tier-3 India only. Apollo's coverage on companies headquartered in places like Indore, Coimbatore, or Vizag is patchier — we see closer to 70% match rates vs 93% for Bangalore/Mumbai/Delhi. The workflow still works; you just need a manual-research fallback branch and a slightly bigger time saving claim ("4 hours/week" not "6").

    Apollo TOS gotcha: Apollo prohibits using their data to build a competing database. If you are storing enriched leads in HubSpot for your own sales use — fine. If you are enriching leads for a customer who will then resell, you need an Apollo OEM agreement. We learned this the slow way during a client kickoff.

    Mini case — a Mumbai SaaS startup, ₹4 Cr ARR

    Client: a 22-person product analytics SaaS firm out of Powai. They were getting ~180 leads/month from inbound + paid ads, scoring them by gut, and burning their two SDRs on triage. We ported this exact workflow with their ICP rubric (theirs caps Indonesia/Vietnam leads at score 50 — different market positioning).

    After 6 weeks: SDRs reported saving 8.4 hours/week combined. SQL conversion rate on Tier A leads (their score 75+) went from 17% to 31% — not because the leads got better, but because the SDRs could now focus on the top 30% of inbound instead of every form fill. Their CEO's note to me: "First automation that actually saved my team time instead of creating a new dashboard nobody opens."

    This is the same enrichment pattern we use inside our AI automation services practice — built originally to power the lead flow into TalkDrill, our in-house English fluency app for Indian adults. The graph is identical; only the ICP rubric changes per client.

    Common mistakes

    Mistake 1 — Enriching every form submission. The gate is not optional. Without the free-email + honeypot filter you will burn credits on garbage and exhaust Apollo Basic by the 18th of the month.

    Mistake 2 — Storing Apollo data as the source of truth. Treat Apollo as a snapshot. The lead's title may have changed; the company may have raised a round. Re-enrich on a 90-day TTL via a scheduled Cron node, not on every workflow trigger.

    Mistake 3 — Letting Claude pick the HubSpot lifecycle stage. It will hallucinate "MQL" vs "SQL" and your reporting goes sideways. Hard-code the lifecycle stage in n8n based on tier, not Claude's free-text guess.

    Mistake 4 — Webhook auth wide open. Your /lead-in endpoint should require a secret header. If the URL leaks, every script kiddie on r/sweatshop_lead_gen will pump fake submissions and you will burn through Apollo credits in a weekend.

    FAQ

    How fast is the enrichment per lead?

    Median 6.4 seconds, 95th percentile 11 seconds. Apollo's API responds in ~1.5s, Claude Haiku in ~2.1s, HubSpot in ~1.3s, the rest is n8n overhead. We have run 312 leads in a single day without throttling.

    Can I use Hunter or Lusha instead of Apollo?

    Yes. Swap the Apollo HTTP Request for Hunter's /v2/email-finder or Lusha's /v2/person endpoint. Hunter is cheaper but coverage on Indian B2B is weaker — our test showed 64% match rate vs Apollo's 93%. Lusha is closer to Apollo at 88% match.

    Will this work with Pipedrive or Zoho instead of HubSpot?

    Yes. n8n has native nodes for both. Pipedrive's API is cleaner for custom fields; Zoho's needs you to fetch the field IDs first via the metadata endpoint and pin them in a Set node. Workflow shape is the same.

    How do I prevent duplicate HubSpot contacts?

    Use upsert by email, not create. The native HubSpot node's "upsert" operation hits HubSpot's /contacts/v3/objects/contacts/upsert endpoint which deduplicates server-side. If you create-then-update via two separate operations you will race-condition under load.

    What about leads from LinkedIn ads or Meta?

    Both push lead data via webhook on submission. Point them at the same /lead-in endpoint with a query param like ?source=linkedin and your Set node tags the source for downstream reporting. We have wired all three (Tally + LinkedIn + Meta) to one workflow.

    Does the workflow run when n8n is offline?

    No — and that is a real failure mode. We use n8n Cloud's automatic restart, or for self-hosted we wrap the Docker container in a systemd service with Restart=always. We also send a daily "heartbeat" ping to Better Uptime from a Cron node, so a dead workflow is paged within 5 minutes.

    Is the Apollo data accurate?

    Mostly. Apollo's data freshness is ~6 months on titles and company headcount in our experience. For high-stakes leads the SDR still verifies on LinkedIn before reaching out. The point is the workflow gets you from "no data" to "good-enough data" in 7 seconds; the human still owns the final call.

    Want this enrichment workflow built into your CRM?

    We ship the full 18-node workflow above, wired to your form, Apollo account, and HubSpot/Pipedrive/Zoho. Includes the ICP rubric tuning workshop with your sales lead. Suitable if you are getting 80+ leads/month and your SDRs are losing days to copy-paste. No slides — just your problem and our honest take.

    Book a 20-min Call
    Tags:
    n8nApolloHubSpotLead EnrichmentSales OpsWorkflow AutomationClaude
    Share this post:
    Hrishikesh Baidya

    Hrishikesh Baidya

    CTO at Softechinfra specializing in Python, system architecture, and building secure, scalable software solutions.