Firecrawl email verification with BillionVerify
Firecrawl is a developer-focused web scraping and crawling API that extracts structured data from websites at scale. When your crawls surface email addresses from public pages, BillionVerify lets you validate them immediately so only reachable, non-disposable contacts flow into your outreach tools or databases.
Why verify before the send
Web-scraped email addresses vary wildly in quality β many are outdated, role-based, or belong to domains with no working mail server. Sending to unvalidated scraped lists destroys sender reputation fast. Running every harvested address through BillionVerify filters out the junk and lets you focus outreach on genuinely deliverable contacts.
Ready-to-use n8n workflow
Import this workflow into n8n β it verifies every address with BillionVerify before Firecrawl sends, so only deliverable contacts are emailed. Install the BillionVerify community node first, then add your API key. Adapted from this n8n template
{
"name": "Monitor competitor prices with Firecrawl, GPT-4.1, Sheets & Gmail alerts + BillionVerify",
"nodes": [
{
"id": "1a7684b3-eb26-414f-ad30-e613306b50b1",
"name": "π Read Historical Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Loads previous scan data for comparison",
"position": [
-1472,
176
],
"parameters": {
"options": {},
"sheetName": {
"mode": "name",
"value": "Historical Data"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "googleSheetsOAuth2Api Credential"
}
},
"typeVersion": 4.7
},
{
"id": "9239f63d-2717-4248-918a-b886016c9a98",
"name": "π Merge Current with Historical",
"type": "n8n-nodes-base.merge",
"notes": "Combines current scrape with historical data for comparison",
"position": [
-1296,
0
],
"parameters": {
"mode": "combine",
"options": {},
"fieldsToMatchString": "rawResponse.message.content"
},
"typeVersion": 3
},
{
"id": "3b453791-3aca-4563-97d7-34d1bc751824",
"name": "π Detect Price & Stock Changes",
"type": "n8n-nodes-base.code",
"notes": "Intelligent change detection with alert level classification",
"position": [
-1120,
0
],
"parameters": {
"jsCode": "// Compare current prices with historical and detect changes\nconst results = [];\n\nfor (const item of $input.all()) {\n const current = item.json;\n \n // Skip error items\n if (current.error) {\n results.push({ json: current });\n continue;\n }\n \n // Find historical data for this competitor\n const historical = $('π Read Historical Data').all()\n .find(h => h.json.competitorName === current.competitorName);\n \n let alertLevel = 'none';\n let changes = [];\n \n if (historical && historical.json.currentPrice) {\n const oldPrice = parseFloat(historical.json.currentPrice);\n const newPrice = parseFloat(current.currentPrice);\n const priceChange = newPrice - oldPrice;\n const priceChangePercent = ((priceChange / oldPrice) * 100).toFixed(2);\n \n current.priceChange = priceChange;\n current.priceChangePercent = parseFloat(priceChangePercent);\n current.previousPrice = oldPrice;\n \n // Determine alert level based on price changes\n if (Math.abs(priceChangePercent) >= 20) {\n alertLevel = 'critical';\n changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n } else if (Math.abs(priceChangePercent) >= 10) {\n alertLevel = 'warning';\n changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n } else if (Math.abs(priceChangePercent) >= 5) {\n alertLevel = 'info';\n changes.push(`Minor price change: ${priceChangePercent}%`);\n }\n \n // Check if it's a new low price\n const historicalLow = parseFloat(historical.json.lowestPrice || oldPrice);\n if (newPrice < historicalLow) {\n current.isNewLow = true;\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push('π― NEW LOWEST PRICE!');\n }\n current.lowestPrice = Math.min(newPrice, historicalLow);\n \n // Stock level changes\n if (historical.json.stockLevel !== current.stockLevel) {\n if (current.stockLevel === 'Out of Stock') {\n alertLevel = 'critical';\n changes.push('π¦ Product went OUT OF STOCK');\n } else if (current.stockLevel === 'Low Stock') {\n alertLevel = alertLevel === 'none' ? 'warning' : alertLevel;\n changes.push('β οΈ Stock level is LOW');\n } else if (historical.json.stockLevel === 'Out of Stock' && current.inStock) {\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push('β
Back in stock!');\n }\n }\n \n // Rating changes\n const oldRating = parseFloat(historical.json.rating || 0);\n const newRating = parseFloat(current.rating || 0);\n const ratingChange = newRating - oldRating;\n \n if (Math.abs(ratingChange) >= 0.5) {\n alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n changes.push(`β Rating ${ratingChange > 0 ? 'improved' : 'dropped'} by ${Math.abs(ratingChange).toFixed(1)} stars`);\n }\n \n // Review count changes\n const oldReviews = parseInt(historical.json.reviewCount || 0);\n const newReviews = parseInt(current.reviewCount || 0);\n const reviewDiff = newReviews - oldReviews;\n \n if (reviewDiff > 0) {\n changes.push(`π¬ ${reviewDiff} new review${reviewDiff > 1 ? 's' : ''}`);\n }\n } else {\n // First time seeing this competitor\n alertLevel = 'info';\n changes.push('π First time tracking this competitor');\n current.lowestPrice = current.currentPrice;\n }\n \n current.alertLevel = alertLevel;\n current.changesSummary = changes.join(' | ');\n current.hasChanges = alertLevel !== 'none';\n \n results.push({ json: current });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "c83f80f9-8954-406e-a43a-20299f94d4ef",
"name": "πΎ Update Historical Data",
"type": "n8n-nodes-base.googleSheets",
"notes": "Saves current data to historical tracking sheet",
"position": [
-944,
80
],
"parameters": {
"columns": {
"value": {
"rating": "={{ $json.rating }}",
"inStock": "={{ $json.inStock }}",
"currency": "={{ $json.currency }}",
"scrapedAt": "={{ $json.scrapedAt }}",
"productUrl": "={{ $json.productUrl }}",
"stockLevel": "={{ $json.stockLevel }}",
"lowestPrice": "={{ $json.lowestPrice }}",
"productName": "={{ $json.productName }}",
"reviewCount": "={{ $json.reviewCount }}",
"currentPrice": "={{ $json.currentPrice }}",
"originalPrice": "={{ $json.originalPrice }}",
"competitorName": "={{ $json.competitorName }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"mode": "name",
"value": "Historical Data"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "= {{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "googleSheetsOAuth2Api Credential"
}
},
"typeVersion": 4.7
},
{
"id": "c1ba6419-781d-4c2d-bfa6-dfe202751e19",
"name": "π Log Alert Details",
"type": "n8n-nodes-base.googleSheets",
"notes": "Logs all alerts to separate tracking sheet",
"position": [
-944,
-80
],
"parameters": {
"columns": {
"value": {
"rating": "={{ $json.rating }}",
"timestamp": "={{ $json.scrapedAt }}",
"alertLevel": "={{ $json.alertLevel }}",
"productUrl": "={{ $json.productUrl }}",
"stockLevel": "={{ $json.stockLevel }}",
"priceChange": "={{ $json.priceChange || 0 }}",
"productName": "={{ $json.productName }}",
"currentPrice": "={{ $json.currentPrice }}",
"previousPrice": "={{ $json.previousPrice || 'N/A' }}",
"changesSummary": "={{ $json.changesSummary }}",
"competitorName": "={{ $json.competitorName }}",
"priceChangePercent": "={{ $json.priceChangePercent || 0 }}"
},
"mappingMode": "defineBelow"
},
"options": {},
"operation": "append",
"sheetName": {
"mode": "name",
"value": "Alert Log"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GOOGLE_SHEET_ID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "credential-id",
"name": "googleSheetsOAuth2Api Credential"
}
},
"typeVersion": 4.7
},
{
"id": "233f07c7-c121-4e14-abf1-0a917b023a11",
"name": "π Aggregate Daily Digest",
"type": "n8n-nodes-base.code",
"notes": "Combines all alerts into a comprehensive summary",
"position": [
-944,
-240
],
"parameters": {
"jsCode": "// Aggregate all items for daily digest email\nconst allItems = $input.all();\n\nconst criticalAlerts = allItems.filter(item => item.json.alertLevel === 'critical');\nconst warningAlerts = allItems.filter(item => item.json.alertLevel === 'warning');\nconst infoAlerts = allItems.filter(item => item.json.alertLevel === 'info');\nconst noChanges = allItems.filter(item => item.json.alertLevel === 'none');\n\nconst summary = {\n totalCompetitors: allItems.length,\n criticalCount: criticalAlerts.length,\n warningCount: warningAlerts.length,\n infoCount: infoAlerts.length,\n noChangeCount: noChanges.length,\n timestamp: new Date().toISOString(),\n criticalAlerts: criticalAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary,\n price: `${i.json.currency} ${i.json.currentPrice}`,\n priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n stock: i.json.stockLevel,\n url: i.json.productUrl\n })),\n warningAlerts: warningAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary,\n price: `${i.json.currency} ${i.json.currentPrice}`,\n priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n stock: i.json.stockLevel\n })),\n infoAlerts: infoAlerts.map(i => ({\n competitor: i.json.competitorName,\n product: i.json.productName,\n changes: i.json.changesSummary\n }))\n};\n\nreturn [{ json: summary }];"
},
"typeVersion": 2
},
{
"id": "bb79bf76-29bd-44d9-8064-48c14622e1f5",
"name": "Scrape URL: nike.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
-176
],
"parameters": {
"url": "https://www.nike.com/sg/w/mens-shoes-nik1zy7ok",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"id": "credential-id",
"name": "firecrawlApi Credential"
}
},
"typeVersion": 1
},
{
"id": "905a1a25-92aa-4459-8d4e-8392d6ca4e61",
"name": "When clicking βExecute workflowβ",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-2096,
32
],
"parameters": {},
"typeVersion": 1
},
{
"id": "fb7b71da-f70d-48d8-afef-ee42bdb48567",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
-800,
-240
],
"webhookId": "db581b6f-5b87-4fcb-be62-37ad7ab7a26f",
"parameters": {
"sendTo": " info@example.com",
"message": "The pricing of the competitors is attached",
"options": {},
"subject": "Shoes pricing"
},
"credentials": {
"gmailOAuth2": {
"id": "credential-id",
"name": "gmailOAuth2 Credential"
}
},
"typeVersion": 2.1
},
{
"id": "5c08b0c2-5bc1-4594-be36-938e67308a1f",
"name": "Scrape URL: adidas.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
-16
],
"parameters": {
"url": "=https://www.adidas.com/us/men-shoes",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"id": "credential-id",
"name": "firecrawlApi Credential"
}
},
"typeVersion": 1
},
{
"id": "57ac5f72-cc18-4919-bdea-4e16939b8080",
"name": "Scrape URL: sneakerpricer.com",
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"position": [
-1904,
144
],
"parameters": {
"url": "=https://www.sneakerpricer.com/us-EN",
"operation": "scrape",
"scrapeOptions": {
"options": {
"formats": {
"format": [
{
"type": "json",
"prompt": "price of the shoe"
}
]
},
"headers": {}
}
},
"requestOptions": {}
},
"credentials": {
"firecrawlApi": {
"id": "credential-id",
"name": "firecrawlApi Credential"
}
},
"typeVersion": 1
},
{
"id": "5eb1ffd8-d98d-4682-b200-92ab2432fd81",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2656,
-400
],
"parameters": {
"width": 2032,
"height": 880,
"content": "## Introduction\nAutomate price monitoring for e-commerce competitorsβideal for retailers, analysts, and pricing teams.\n\n**β οΈ Self-Hosted Only:** Requires self-hosted n8n instance.\n## How It Works\nScrapes competitor URLs, extracts data via AI, detects price/stock changes, logs to Google Sheets with email alerts.\n## Workflow Template\nTrigger β Scrape β AI Extract β Parse β Compare β Detect Changes β Update Sheets + Alert\n## Workflow Steps\n1. **Scraping:** Firecrawl fetches Nike, Adidas, Sneaker data\n2. **AI Extraction:** Processes product details\n3. **Parsing:** Structures response\n4. **Historical Check:** Reads Sheets data\n5. **Change Detection:** Identifies price/stock updates\n6. **Dual Output:** Updates Sheets + sends alerts\n## Setup Instructions\n1. **Firecrawl API**\nGet key from dashboard β Add to n8n\n2. **OpenAI API**\nGet key from platform β Add to n8n\n3. **Google Sheets OAuth2**\nCreate OAuth2 in Google Cloud Console β Authorize in n8n β Enable API\n4. **Gmail OAuth2**\nUse same project β Authorize in n8n β Enable API\n5. **Spreadsheet Setup**\nCreate Sheet with required columns β Copy ID from URL β Paste in workflow\n## Prerequisites\nSelf-hosted n8n, Firecrawl account, OpenAI key, Google account (Sheets + Gmail OAuth2)\n## Customization\nAdd URLs, adjust thresholds, integrate Slack\n## Benefits\nSaves 2+ hours daily, real-time tracking, automated alerts-time competitor tracking, automated alerts, historical data analysis."
},
"typeVersion": 1
},
{
"id": "738fd9c2-91ff-4f15-b7a3-1aa0e4c1f8a1",
"name": "Converts unstructured AI text into organized, usable data fields",
"type": "n8n-nodes-base.code",
"notes": "Parses and validates the AI extracted data",
"position": [
-1504,
-16
],
"parameters": {
"jsCode": "// Parse AI response and clean data\nconst items = [];\n\nfor (const item of $input.all()) {\n try {\n // Parse the AI response\n let parsed;\n const response = item.json.choices?.[0]?.message?.content || item.json.message || '';\n \n // Remove markdown code blocks if present\n const cleaned = response.replace(/```json\\n?|```\\n?/g, '').trim();\n \n try {\n parsed = JSON.parse(cleaned);\n } catch (e) {\n // Try to extract JSON from the response\n const jsonMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n parsed = JSON.parse(jsonMatch[0]);\n } else {\n throw new Error('Could not parse JSON from AI response');\n }\n }\n \n // Enrich with metadata\n items.push({\n json: {\n ...parsed,\n scrapedAt: new Date().toISOString(),\n priceChange: 0, // Will be calculated in comparison\n priceChangePercent: 0,\n isNewLow: false,\n alertLevel: 'none'\n }\n });\n } catch (error) {\n console.error('Failed to parse item:', error.message);\n // Add error item for debugging\n items.push({\n json: {\n error: error.message,\n rawResponse: item.json,\n competitorName: 'Parse Error',\n scrapedAt: new Date().toISOString()\n }\n });\n }\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "2a1d727b-8147-42e7-bfcd-c3ed37af7bc7",
"name": "π€ AI Extract Product Data using GPT-4.1-mini",
"type": "n8n-nodes-base.openAi",
"notes": "Uses OpenAI to intelligently extract structured data from HTML",
"position": [
-1696,
-16
],
"parameters": {
"prompt": {
"messages": [
{
"role": "system",
"content": "You are a precise e-commerce data extraction expert. Extract shoes information from HTML and return ONLY valid JSON with no markdown formatting.\n\nExtract these fields:\n- productName: string\n- currentPrice: number (numeric value only, no currency symbols)\n- originalPrice: number (if discounted, otherwise same as currentPrice)\n- currency: string (USD, EUR, etc.)\n- inStock: boolean\n- stockLevel: string (\"In Stock\", \"Low Stock\", \"Out of Stock\", \"Limited\", etc.)\n- rating: number (0-5 scale)\n- reviewCount: number\n- lastUpdated: string (current ISO timestamp)\n- productUrl: string (from context)\n- competitorName: string (from context)\n\nReturn ONLY the JSON object, no explanations."
},
{
"content": "HTML Content:\n{{ $json.body }}\n\nProduct URL: {{ $json.url || 'unknown' }}\nCompetitor: {{ $json.competitor || 'unknown' }}\n\nExtract the product data as JSON:"
}
]
},
"options": {
"temperature": 0.1
},
"resource": "chat",
"chatModel": "gpt-4.1-mini",
"requestOptions": {}
},
"credentials": {
"openAiApi": {
"id": "credential-id",
"name": "openAiApi Credential"
}
},
"typeVersion": 1.1
},
{
"id": "652a2128-6e01-4d6e-8bcb-b3f844cec2a8",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1584,
-352
],
"parameters": {
"color": 6,
"width": 352,
"height": 240,
"content": "## Google Sheets Structure\n**Required Columns:**\n- **Product Name** (Column A)\n- **Current Price** (Column B)\n- **Previous Price** (Column C)\n- **Stock Status** (Column D)\n- **Last Updated** (Column E)\n- **URL** (Column F)\n- **Change Detected** (Column G)"
},
"typeVersion": 1
},
{
"parameters": {
"operation": "verify",
"email": "={{ $json.email || $json.Email }}",
"additionalOptions": {}
},
"type": "n8n-nodes-billionverify.billionVerify",
"typeVersion": 1,
"position": [
-1160,
-240
],
"name": "Verify Email (BillionVerify)",
"credentials": {
"billionVerifyApi": {
"id": "",
"name": "BillionVerify account"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "is-deliverable",
"leftValue": "={{ $json.is_deliverable }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
]
}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-980,
-240
],
"name": "IF deliverable"
}
],
"connections": {
"Send a message": {
"main": [
[]
]
},
"Scrape URL: nike.com": {
"main": [
[
{
"node": "π€ AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"Scrape URL: adidas.com": {
"main": [
[
{
"node": "π€ AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"π Read Historical Data": {
"main": [
[
{
"node": "π Merge Current with Historical",
"type": "main",
"index": 1
}
]
]
},
"π Aggregate Daily Digest": {
"main": [
[
{
"node": "Verify Email (BillionVerify)",
"type": "main",
"index": 0
}
]
]
},
"Scrape URL: sneakerpricer.com": {
"main": [
[
{
"node": "π€ AI Extract Product Data using GPT-4.1-mini",
"type": "main",
"index": 0
}
]
]
},
"π Detect Price & Stock Changes": {
"main": [
[
{
"node": "π Aggregate Daily Digest",
"type": "main",
"index": 0
},
{
"node": "πΎ Update Historical Data",
"type": "main",
"index": 0
},
{
"node": "π Log Alert Details",
"type": "main",
"index": 0
}
]
]
},
"π Merge Current with Historical": {
"main": [
[
{
"node": "π Detect Price & Stock Changes",
"type": "main",
"index": 0
}
]
]
},
"When clicking βExecute workflowβ": {
"main": [
[
{
"node": "Scrape URL: nike.com",
"type": "main",
"index": 0
},
{
"node": "Scrape URL: adidas.com",
"type": "main",
"index": 0
},
{
"node": "Scrape URL: sneakerpricer.com",
"type": "main",
"index": 0
}
]
]
},
"π€ AI Extract Product Data using GPT-4.1-mini": {
"main": [
[
{
"node": "Converts unstructured AI text into organized, usable data fields",
"type": "main",
"index": 0
}
]
]
},
"Converts unstructured AI text into organized, usable data fields": {
"main": [
[
{
"node": "π Merge Current with Historical",
"type": "main",
"index": 0
}
]
]
},
"Verify Email (BillionVerify)": {
"main": [
[
{
"node": "IF deliverable",
"type": "main",
"index": 0
}
]
]
},
"IF deliverable": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
],
[]
]
}
},
"settings": {
"executionOrder": "v1"
}
}Workflow templates with Firecrawl
Ready-to-use workflows that verify emails before Firecrawl sends.
How it works
- 1
Connect Firecrawl and BillionVerify in an n8n workflow or call both REST APIs sequentially in your pipeline script.
- 2
After Firecrawl returns scraped data, extract email addresses from the structured output.
- 3
Send each address to BillionVerify for syntax, MX, deliverability, role-address, and disposable-domain checks.
- 4
Tag each result with its verification status and filter out invalid or high-risk addresses before downstream use.
- 5
Import only the clean, verified contacts into your CRM, outreach tool, or database.
When to use this
Filter scraped lead emails before CRM import
After Firecrawl extracts contact emails from target websites, pipe them through BillionVerify in an n8n workflow. Only import leads with valid, non-disposable addresses into your CRM, cutting bounce risk from the start.
Score and prioritize outreach lists
Use BillionVerify's verification results to score scraped contacts by deliverability confidence. Send high-priority sequences only to verified addresses and hold risky ones for a secondary review pass.
FAQ
Can I verify emails in bulk after a large Firecrawl job?
Yes. BillionVerify's bulk API accepts large lists and processes them asynchronously, making it practical to validate thousands of scraped addresses from a single crawl without rate-limiting concerns.
What percentage of scraped emails are typically invalid?
Rates vary by source, but publicly scraped lists often have 20β40% invalid or undeliverable addresses. Verifying before sending protects your domain reputation and improves deliverability metrics significantly.
Does BillionVerify flag role-based addresses like contact@ or info@?
Yes. Role-based addresses are commonly scraped from websites but respond poorly to cold outreach and can trigger spam complaints. BillionVerify identifies them so you can exclude or deprioritize them.
Verify emails in Firecrawl
Create a free account, grab your API key, and stop bounces before they happen.
Get started free