This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.
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 →
{
"name": "Permit Cash Printer v1",
"nodes": [
{
"parameters": {
"cronExpression": "*/30 * * * *"
},
"id": "Cron",
"name": "Cron - every 30 min",
"type": "n8n-nodes-base.cron",
"typeVersion": 2,
"position": [
200,
300
]
},
{
"parameters": {
"values": {
"string": [
{
"name": "CITY_1_PERMITS_URL",
"value": "https://data.city1.gov/permits?status=issued"
},
{
"name": "CITY_2_PERMITS_URL",
"value": "https://data.city2.gov/permits?status=issued"
},
{
"name": "TRADE_KEYWORDS",
"value": "striping|sealcoat|paving|glass|hvac|clean|tenant improvement|parking"
},
{
"name": "MARKET_NAME",
"value": "OKC Metro"
},
{
"name": "SAMPLE_LEADS_TO_SEND",
"value": "3"
},
{
"name": "PRICE_PER_METRO",
"value": "149"
},
{
"name": "STRIPE_PAYMENT_LINK",
"value": "https://buy.stripe.com/test_XXXXXXXXXXXX"
},
{
"name": "OUTBOUND_LIST_CSV",
"value": "gsheet://YourSpreadsheetName/LeadsOut"
},
{
"name": "FULL_DATA_CSV",
"value": "gsheet://YourSpreadsheetName/LeadsFull"
}
]
},
"options": {}
},
"id": "EnvVars",
"name": "Config Vars",
"type": "n8n-nodes-base.set",
"typeVersion": 3,
"position": [
400,
300
]
},
{
"parameters": {
"functionCode": "\n// Combine multiple city endpoints; allows JSON or HTML feed URLs.\nconst urls = [\n $json['CITY_1_PERMITS_URL'],\n $json['CITY_2_PERMITS_URL']\n].filter(Boolean);\nreturn urls.map(u => ({json: {url: u}}));\n"
},
"id": "PrepURLs",
"name": "Prep URLs",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
600,
300
]
},
{
"parameters": {
"url": "={{$json[\"url\"]}}",
"responseFormat": "string",
"options": {
"followRedirect": true
}
},
"id": "Fetch",
"name": "Fetch Permit Page",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
800,
300
]
},
{
"parameters": {
"functionCode": "\n/**\n * Attempts to parse either JSON (CKAN/Socrata style) or HTML tables into a normalized list of permits.\n * For HTML, looks for rows with columns like Address, Description, IssueDate.\n */\nconst input = items[0].json;\nconst body = input.body || input; // http node returns body in .json.body (string)\nlet permits = [];\ntry {\n const data = typeof body === 'string' ? JSON.parse(body) : body;\n // Try common Socrata format\n if (Array.isArray(data)) {\n // Assume array of permit objects\n permits = data;\n } else if (data && data.data && Array.isArray(data.data)) {\n permits = data.data;\n }\n} catch(e){\n // Fallback: naive HTML parse via regex (works for simple <tr><td>)\n const html = String(body);\n const rows = html.split(/<tr[^>]*>/i).slice(1);\n for (const r of rows) {\n const tds = r.split(/<td[^>]*>/i).slice(1).map(x => x.replace(/<[^>]+>/g,'').trim());\n if (tds.length >= 3) {\n permits.push({\n address: tds[0],\n description: tds[1],\n issued: tds[2],\n });\n }\n }\n}\n\nreturn permits.map(p => ({json: {raw: p}}));\n"
},
"id": "Parse",
"name": "Parse to Permits",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1000,
300
]
},
{
"parameters": {
"functionCode": "\n/**\n * Normalize fields to: address, project, issued_at, city, contact_url\n */\nfunction pick(obj, keys){\n const out={};\n for (const k of keys){\n if (obj[k]!==undefined) out[k]=obj[k];\n }\n return out;\n}\nconst tradeRegex = new RegExp($item(0).$node[\"Config Vars\"].json[\"TRADE_KEYWORDS\"], 'i');\nconst city = $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"];\nconst normalized = [];\n\nfor (const item of items){\n const r = item.json.raw;\n const rec = {\n address: r.address || r.Address || r.site_address || r.location || r['Site Address'] || '',\n project: r.description || r.Description || r.work_description || r['Work Description'] || '',\n issued_at: r.issued || r.issue_date || r['Issue Date'] || r.permit_issued_date || '',\n permit_no: r.permit || r.permit_number || r['Permit Number'] || '',\n city\n };\n // Filter to trades of interest\n if ((rec.project||'').match(tradeRegex)){\n normalized.push({json: rec});\n }\n}\nreturn normalized;\n"
},
"id": "NormalizeFilter",
"name": "Normalize + Filter by Trade",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1200,
300
]
},
{
"parameters": {
"functionCode": "\n// De-dupe by (permit_no || address+project+issued_at) using a simple in-memory cache\n// For production, use Redis node or a Database node.\nconst seen = (global.get('seenPermits') || new Set());\nconst out = [];\nfor (const item of items){\n const j = item.json;\n const key = (j.permit_no || (j.address+'|'+j.project+'|'+j.issued_at)).toLowerCase();\n if (!seen.has(key)){\n seen.add(key);\n out.push(item);\n }\n}\nglobal.set('seenPermits', seen);\nreturn out;\n"
},
"id": "DeDupe",
"name": "De-dup",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1400,
300
]
},
{
"parameters": {
"operation": "append",
"documentFormat": "raw",
"url": "={{$item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"]}}",
"options": {}
},
"id": "WriteFull",
"name": "Write \u2192 Google Sheet (Full)",
"type": "n8n-nodes-base.csvFile",
"typeVersion": 1,
"position": [
1600,
300
]
},
{
"parameters": {
"functionCode": "\n// Prepare a teaser list (first N) for outbound email/DM.\nconst N = parseInt($item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]) || 3;\nreturn items.slice(0, N);\n"
},
"id": "TeaserSlice",
"name": "Slice Teaser N",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1800,
260
]
},
{
"parameters": {
"fromEmail": "",
"toEmail": "=",
"subject": "={{$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}} Fresh {{new Date().toLocaleDateString()}} Permit Leads ({{$item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]}} free sample)",
"text": "={{`Hey there,\n\nHere are ${$item(0).$node[\"Config Vars\"].json[\"SAMPLE_LEADS_TO_SEND\"]} fresh ` + $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"] + ` permit-based opportunities matching your trade:\n\n`}}",
"additionalFields": {
"text2": "={{$json.address + ' \u2014 ' + $json.project + ' \u2014 Issued: ' + $json.issued_at}}"
}
},
"id": "EmailTeaser",
"name": "Email Teaser (Gmail)",
"type": "n8n-nodes-base.gmail",
"typeVersion": 3,
"position": [
2000,
240
]
},
{
"parameters": {
"functionCode": "\n// Build CTA with Stripe payment link\nconst price = $item(0).$node[\"Config Vars\"].json[\"PRICE_PER_METRO\"];\nconst link = $item(0).$node[\"Config Vars\"].json[\"STRIPE_PAYMENT_LINK\"];\nreturn [{\n json: {\n message: `Unlock full weekly sheet for ${$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}: $${price}/mo \u2192 ${link}`\n }\n}];\n"
},
"id": "CTA",
"name": "Build CTA",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1800,
360
]
},
{
"parameters": {
"fromEmail": "",
"toEmail": "=",
"subject": "={{$item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"]}} \u2013 Get the Full Leads Sheet (Stripe Link Inside)",
"text": "={{$json.message}}"
},
"id": "EmailCTA",
"name": "Email CTA (Gmail)",
"type": "n8n-nodes-base.gmail",
"typeVersion": 3,
"position": [
2000,
360
]
},
{
"parameters": {
"path": "stripe/payment-success",
"responseMode": "lastNode",
"options": {}
},
"id": "WebhookStripe",
"name": "Webhook: Stripe Success",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
1600,
540
]
},
{
"parameters": {
"operation": "lookup",
"documentFormat": "raw",
"url": "={{$item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"]}}"
},
"id": "ReadFull",
"name": "Read Full Sheet",
"type": "n8n-nodes-base.csvFile",
"typeVersion": 1,
"position": [
1800,
540
]
},
{
"parameters": {
"fromEmail": "",
"toEmail": "={{$json.query.email || 'buyer@example.com'}}",
"subject": "Full Permit Leads Sheet \u2013 Access Granted",
"text": "={{'Thanks for your purchase! Here is your full leads sheet for ' + $item(0).$node[\"Config Vars\"].json[\"MARKET_NAME\"] + '.\\n\\n' + 'Google Sheet: ' + $item(0).$node[\"Config Vars\"].json[\"FULL_DATA_CSV\"] + '\\n\\nStay winning.'}}"
},
"id": "DeliverFull",
"name": "Deliver Full Access (Gmail)",
"type": "n8n-nodes-base.gmail",
"typeVersion": 3,
"position": [
2000,
540
]
}
],
"connections": {
"Cron - every 30 min": {
"main": [
[
{
"node": "Config Vars",
"type": "main",
"index": 0
}
]
]
},
"Config Vars": {
"main": [
[
{
"node": "Prep URLs",
"type": "main",
"index": 0
}
]
]
},
"Prep URLs": {
"main": [
[
{
"node": "Fetch Permit Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Permit Page": {
"main": [
[
{
"node": "Parse to Permits",
"type": "main",
"index": 0
}
]
]
},
"Parse to Permits": {
"main": [
[
{
"node": "Normalize + Filter by Trade",
"type": "main",
"index": 0
}
]
]
},
"Normalize + Filter by Trade": {
"main": [
[
{
"node": "De-dup",
"type": "main",
"index": 0
}
]
]
},
"De-dup": {
"main": [
[
{
"node": "Write \u2192 Google Sheet (Full)",
"type": "main",
"index": 0
},
{
"node": "Slice Teaser N",
"type": "main",
"index": 0
},
{
"node": "Build CTA",
"type": "main",
"index": 0
}
]
]
},
"Slice Teaser N": {
"main": [
[
{
"node": "Email Teaser (Gmail)",
"type": "main",
"index": 0
}
]
]
},
"Build CTA": {
"main": [
[
{
"node": "Email CTA (Gmail)",
"type": "main",
"index": 0
}
]
]
},
"Webhook: Stripe Success": {
"main": [
[
{
"node": "Read Full Sheet",
"type": "main",
"index": 0
}
]
]
},
"Read Full Sheet": {
"main": [
[
{
"node": "Deliver Full Access (Gmail)",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"version": "1.0",
"notes": "Import into n8n. Configure Gmail creds, Stripe payment link, and Google Sheets names. Replace CITY_* URLs with your local permit feeds."
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Permit Cash Printer v1. Uses httpRequest, csvFile, gmail. Scheduled trigger; 15 nodes.
Source: https://github.com/405naturaldesign-tech/puraestate/blob/main/n8n-workflows/permit_cash_printer_v1.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Relatórios aos Laboratórios. Uses httpRequest, gmail, formTrigger. Scheduled trigger; 54 nodes.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
14310 Send Overdue Invoice Payment Reminders With Ifirma Gmail Postgrid And Slack. Uses httpRequest, stopAndError, slack, gmail. Scheduled trigger; 53 nodes.
Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.
Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.