AutomationFlowsMarketing & Ads › Verify Us Contractor Licenses in Lead Lists with Apify

Verify Us Contractor Licenses in Lead Lists with Apify

ByScrapeBench.dev @scrapebench on n8n.io

This workflow verifies contractor licenses for leads across 17 supported US states by routing each lead to the correct Apify license-lookup actor, matching returned license records back to the lead, and labeling each lead as verified, expired, review, not found, or unverifiable.…

Event trigger★★★★☆ complexity15 nodes@Apify/N8N Nodes Apify
Marketing & Ads Trigger: Event Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17355 — we link there as the canonical source.

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 →

Download .json
{
  "name": "Verify contractor licenses in your lead list (17 US states)",
  "nodes": [
    {
      "id": "b0000000-0000-4000-8000-000000000099",
      "name": "Sticky: overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1600,
        -620
      ],
      "parameters": {
        "color": 1,
        "width": 680,
        "height": 2000,
        "content": "## Verify contractor licenses in your lead list (17 US states)\n\n![Workflow overview](https://scrapebench.dev/img/n8n/license-verify-lead-list/canvas.png)\n\n> **Self-hosted only.** This template uses the `@apify/n8n-nodes-apify` community node, which cannot be installed on n8n Cloud.\n\n### Who's it for\nAnyone sitting on a list of contractors \u2014 agencies selling to home services, sales teams prospecting, marketplaces admitting trade accounts. Lead lists scraped from Google Maps tell you a company exists. They do not tell you whether its licence is real, current, or issued in the state it claims to work in.\n\n### What it does\nEach lead is routed to its state's licensing portal, the returned licence is matched back to the lead, and the lead is sorted into verified or needs-review. It returns the licence number, status and expiry date, and one of four verdicts: `verified-active`, `verified-expired`, `review` (an active licence matched, but only partially on name) or `not-found`.\n\nLeads from the other 33 states are labelled `unverifiable - state not covered` and passed through. They are never dropped and never marked unlicensed.\n\n### How to set up\n1. Install the `@apify/n8n-nodes-apify` community node.\n2. Add your Apify API key as an Apify credential on the **Look up the licence** node.\n3. Replace the **Your leads** node with your own source (Google Sheets, CRM, CSV). Each lead needs `company` and a 2-letter `state`; `license_number` is optional and makes the lookup exact.\n\n### Requirements\nAn Apify account (free tier works) and a self-hosted n8n.\n\n### How to customize\nAdd a state by adding one row to the `ACTORS` map in **Route by state**. Change the matching strictness in **Match licence to lead** \u2014 it deliberately flags partial name matches for review rather than passing them.\n\nCovered: AL AR CA CT FL MA MI MN NC NM NV OR SC TN TX VA WA."
      },
      "typeVersion": 1
    },
    {
      "id": "b0000000-0000-4000-8000-00000000000a",
      "name": "Sticky: leads",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -760,
        -620
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 400,
        "content": "## 1. Your leads\nSwap this Set node for a **Google Sheets \u2192 Get rows** node (or any source).\n\nEach lead needs:\n- `company` \u2014 the business or licensee name\n- `state` \u2014 2-letter code\n\n`license_number` is optional. If you have it, the lookup is exact."
      },
      "typeVersion": 1
    },
    {
      "id": "b0000000-0000-4000-8000-00000000000b",
      "name": "Sticky: routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -420,
        -620
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 400,
        "content": "## 2. Route each lead to its state's actor\nOne Code node replaces a 17-way Switch. It maps the state to the right Apify actor **and** to that actor's own input field \u2014 they are not uniform:\n\n- most take `query`\n- New Mexico takes `businessName`\n- South Carolina takes `lastName`\n- Virginia takes `nameFilter`\n- Connecticut goes through the multi-state actor\n\nAdd a state by adding one row to `ACTORS`."
      },
      "typeVersion": 1
    },
    {
      "id": "b0000000-0000-4000-8000-00000000000c",
      "name": "Sticky: uncovered",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        100,
        300
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 260,
        "content": "## 3. Uncovered states pass through\nA lead outside the 17 covered states is labelled `unverifiable - state not covered`.\n\nIt is **never dropped** and **never marked unlicensed**. \"We didn't look\" is not the same as \"they aren't licensed\"."
      },
      "typeVersion": 1
    },
    {
      "id": "b0000000-0000-4000-8000-00000000000d",
      "name": "Sticky: verdict",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        620,
        -620
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 320,
        "content": "## 4. The verdict\n`not-found` means the state's portal returned no match for that name. It does **not** mean the contractor is unlicensed \u2014 they may be licensed under a different legal name, or in another state.\n\nOnly `verified-active` is a positive result."
      },
      "typeVersion": 1
    },
    {
      "id": "a1000000-0000-4000-8000-000000000001",
      "name": "When clicking Execute",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -700,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a1000000-0000-4000-8000-000000000002",
      "name": "Your leads",
      "type": "n8n-nodes-base.code",
      "position": [
        -480,
        0
      ],
      "parameters": {
        "jsCode": "// Replace with your real leads (Google Sheets, CRM, CSV...).\nreturn [\n  { json: { company: 'BENITEZ, MICHAEL', state: 'FL', license_number: 'CGC1522906' } },\n  { json: { company: 'SMITH', state: 'CA', license_number: '' } },\n  { json: { company: 'ACME BUILDERS', state: 'ND', license_number: '' } },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-4000-8000-000000000003",
      "name": "Route by state",
      "type": "n8n-nodes-base.code",
      "position": [
        -260,
        0
      ],
      "parameters": {
        "jsCode": "// state -> { actor, field }\n// `field` matters: these actors do NOT share one input name.\nconst ACTORS = {\n  AL: { actor: 'scrapebench~alabama-contractor-license-lookup',        field: 'query' },\n  AR: { actor: 'scrapebench~arkansas-contractor-license-lookup',       field: 'query' },\n  CA: { actor: 'scrapebench~california-contractor-license-lookup',     field: 'query' },\n  CT: { actor: 'scrapebench~multistate-contractor-license-lookup',     field: 'query', extra: { states: ['CT'] } },\n  FL: { actor: 'scrapebench~florida-contractor-license-lookup',        field: 'query' },\n  MA: { actor: 'scrapebench~massachusetts-construction-license-lookup', field: 'query' },\n  MI: { actor: 'scrapebench~michigan-contractor-license-lookup',       field: 'query' },\n  MN: { actor: 'scrapebench~minnesota-contractor-license-lookup',      field: 'query' },\n  NC: { actor: 'scrapebench~north-carolina-contractor-license-lookup', field: 'query' },\n  NM: { actor: 'scrapebench~new-mexico-contractor-license-lookup',     field: 'businessName' },\n  NV: { actor: 'scrapebench~nevada-contractor-license-lookup',         field: 'query' },\n  OR: { actor: 'scrapebench~oregon-contractor-license-lookup',         field: 'query' },\n  SC: { actor: 'scrapebench~south-carolina-contractor-license-lookup', field: 'lastName' },\n  TN: { actor: 'scrapebench~tennessee-contractor-license-lookup',      field: 'query' },\n  TX: { actor: 'scrapebench~texas-contractor-license-lookup',          field: 'query' },\n  VA: { actor: 'scrapebench~virginia-contractor-license-lookup',       field: 'nameFilter' },\n  WA: { actor: 'scrapebench~washington-contractor-license-lookup',     field: 'query' },\n};\n\nreturn $input.all().map((item) => {\n  const lead = item.json;\n  const state = String(lead.state || '').trim().toUpperCase();\n  const cfg = ACTORS[state];\n\n  if (!cfg) {\n    // Not covered. Pass it through labelled - never drop it, never call it unlicensed.\n    return { json: { ...lead, covered: false, verification_status: 'unverifiable - state not covered' } };\n  }\n\n  // Prefer an exact licence number when the lead has one; otherwise search the name.\n  const term = String(lead.license_number || '').trim() || String(lead.company || '').trim();\n  const input = { [cfg.field]: term, maxResults: 5, ...(cfg.extra || {}) };\n\n  return { json: { ...lead, covered: true, actorId: cfg.actor, actorInput: JSON.stringify(input) } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-4000-8000-000000000004",
      "name": "Covered state?",
      "type": "n8n-nodes-base.if",
      "position": [
        -40,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.covered }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-4000-8000-000000000005",
      "name": "Look up the licence",
      "type": "@apify/n8n-nodes-apify.apify",
      "onError": "continueRegularOutput",
      "position": [
        220,
        -120
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.actorId }}"
        },
        "resource": "Actors",
        "operation": "Run actor and get dataset",
        "customBody": "={{ $json.actorInput }}"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "a1000000-0000-4000-8000-000000000006",
      "name": "Match licence to lead",
      "type": "n8n-nodes-base.code",
      "position": [
        460,
        -120
      ],
      "parameters": {
        "jsCode": "// Pair each returned licence back to the lead that asked for it, then judge it.\n//\n// Matching is deliberately conservative: exact licence number if we had one, otherwise a\n// normalised name comparison. A near-miss is reported as `review`, never as a pass.\nconst norm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');\n\nconst leads = $('Route by state').all().map((i) => i.json).filter((l) => l.covered);\nconst rows = $input.all().map((i) => i.json);\n\nconst out = [];\nfor (const lead of leads) {\n  const wanted = String(lead.license_number || '').trim();\n  const hits = rows.filter((r) => {\n    if (wanted) return String(r.license_number || '').trim().toUpperCase() === wanted.toUpperCase();\n    const a = norm(lead.company);\n    const b = norm(r.business_name || r.company_name || r.person_name);\n    return a && b && (a === b || b.includes(a) || a.includes(b));\n  });\n\n  if (!hits.length) {\n    // NOT the same as \"unlicensed\" - say exactly what happened.\n    out.push({ json: { ...lead, verification_status: 'not-found', note: 'No match at the state portal for this name.' } });\n    continue;\n  }\n\n  const hit = hits[0];\n  const status = String(hit.status || '').toLowerCase();\n  const active = status.includes('active') || status.includes('current');\n  const exactName = !wanted && norm(lead.company) === norm(hit.business_name || hit.company_name || hit.person_name);\n\n  let verdict;\n  if (!active) verdict = 'verified-expired';\n  else if (wanted || exactName) verdict = 'verified-active';\n  else verdict = 'review'; // active licence, but the name only partially matched\n\n  out.push({ json: {\n    ...lead,\n    verification_status: verdict,\n    license_number: hit.license_number ?? lead.license_number ?? null,\n    license_status: hit.status ?? null,\n    license_expiry: hit.expiration_date ?? null,\n    matched_name: hit.business_name ?? hit.person_name ?? null,\n    source_url: hit.source_url ?? null,\n  } });\n}\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-4000-8000-000000000007",
      "name": "Not covered - keep the lead",
      "type": "n8n-nodes-base.set",
      "position": [
        220,
        140
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "u1",
              "name": "verification_status",
              "type": "string",
              "value": "unverifiable - state not covered"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "a1000000-0000-4000-8000-000000000008",
      "name": "Licensed and active?",
      "type": "n8n-nodes-base.if",
      "position": [
        700,
        -120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "v1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.verification_status }}",
              "rightValue": "verified-active"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a1000000-0000-4000-8000-000000000009",
      "name": "Verified leads",
      "type": "n8n-nodes-base.noOp",
      "position": [
        940,
        -220
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a1000000-0000-4000-8000-000000000010",
      "name": "Needs review",
      "type": "n8n-nodes-base.noOp",
      "position": [
        940,
        -20
      ],
      "parameters": {},
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Your leads": {
      "main": [
        [
          {
            "node": "Route by state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Covered state?": {
      "main": [
        [
          {
            "node": "Look up the licence",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Not covered - keep the lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by state": {
      "main": [
        [
          {
            "node": "Covered state?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look up the licence": {
      "main": [
        [
          {
            "node": "Match licence to lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Licensed and active?": {
      "main": [
        [
          {
            "node": "Verified leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match licence to lead": {
      "main": [
        [
          {
            "node": "Licensed and active?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking Execute": {
      "main": [
        [
          {
            "node": "Your leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow verifies contractor licenses for leads across 17 supported US states by routing each lead to the correct Apify license-lookup actor, matching returned license records back to the lead, and labeling each lead as verified, expired, review, not found, or unverifiable.…

Source: https://n8n.io/workflows/17355/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

This workflow runs manually or on a schedule to pull search queries and excluded domains from Google Sheets, launches Apify’s Link Prospecting Tool, then uses an Apify “run succeeded” webhook to fetch

Google Sheets, @Apify/N8N Nodes Apify
Marketing & Ads

This workflow uses Apify and Google Sheets to find accommodation leads from Booking.com, enrich them with Google Maps contact details, discover official websites via Google Search, scrape additional c

@Apify/N8N Nodes Apify, Google Sheets
Marketing & Ads

⚡AI-Powered YouTube Playlist & Video Summarization and Analysis v2. Uses httpRequest, n8n-nodes-youtube-transcription-dmr, redis. Event-driven trigger; 73 nodes.

HTTP Request, N8N Nodes Youtube Transcription Dmr, Redis
Marketing & Ads

Lead Cleanup - SMS Errors & DNC. Uses httpRequest, slack. Event-driven trigger; 49 nodes.

HTTP Request, Slack
Marketing & Ads

Maximize your conversion rates with this end-to-end automated outreach and lead nurturing system. This workflow manages the entire sales lifecycle—from instant contact enrollment via WhatsApp to AI-pe

N8N Nodes Wati, Airtable, HTTP Request