AutomationFlowsGeneral › Monitor New Domain and Certificate Activity with Domainkits and Email Alerts

Monitor New Domain and Certificate Activity with Domainkits and Email Alerts

ByABTdomain @lyalphaniub on n8n.io

This workflow runs every 30 minutes to monitor a keyword across DomainKits newly registered domain data and Certificate Transparency activity, correlates both feeds, and emails only what changed since the last run with a text report and attached CSV. Runs every 30 minutes on a…

Cron / scheduled trigger★★★★☆ complexity15 nodesN8N Nodes DomainkitsEmail Send
General Trigger: Cron / scheduled Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #18058 — 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": "Monitor newly registered domains and new activity with DomainKits",
  "nodes": [
    {
      "id": "e0000000-0000-0000-0000-000000000001",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -700,
        -260
      ],
      "parameters": {
        "width": 540,
        "height": 1180,
        "content": "## Monitor newly registered domains and new activity\n\n### How it works\n\nTwo independent searches run every 30 minutes and feed one comparison step.\n\nThe registration search reports names created inside its window. The certificate search reports names seen in Certificate Transparency, whatever their age. A name registered months ago and switched on today shows up in the second search and not the first, which is the case a registration feed on its own cannot reach.\n\nThe comparison step keeps the previous result in workflow storage and reports only what moved: a name that appeared, a name that gained certificate activity, or a name whose activity advanced. New names then go through a bulk nameserver lookup so names configured together arrive grouped.\n\n### Setup steps\n\n- Set your keyword in the Set Watch node. Add your own domains to Exclude Suffixes so your own certificates stay out of the report.\n- Add your DomainKits API credentials to the three DomainKits nodes. One key covers all of them.\n- Configure the Send Alert node with your SMTP credentials and recipients.\n- Activate the workflow. n8n saves workflow storage only after an execution started by a trigger, so a manual test run reports everything it finds and keeps nothing.\n- The first scheduled run stores a baseline and reports every match. Later runs report changes only.\n\n### Customization\n\nRaise the schedule to every 15 minutes to match how often the certificate index refreshes. Swap the Send Alert node for Slack or a ticket system. Narrow the keyword to catch a specific pattern, for example a brand plus a word such as login or secure, which is where certificates for lookalike names tend to appear.\n\n### Data note\n\nResults contain domain names, registration dates, hostnames from certificate logs, nameservers and listing status only. No registrant personal data is included, so the output is safe to forward or store.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000002",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -260
      ],
      "parameters": {
        "width": 880,
        "height": 700,
        "content": "## Two inputs\n\nThe registration search finds names created recently. The certificate search finds names with recent activity in Certificate Transparency at any age. Both read the same keyword.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000003",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        900,
        -260
      ],
      "parameters": {
        "width": 620,
        "height": 700,
        "content": "## Compare and keep state\n\nWorkflow storage holds the previous result. This step reports only what moved since then, so a run with nothing new sends nothing.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000004",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1580,
        -260
      ],
      "parameters": {
        "width": 740,
        "height": 700,
        "content": "## Enrich and deliver\n\nNew names go through one bulk nameserver lookup, so names configured together are visible as a group. The report and its CSV go out by email.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000005",
      "name": "Every 30 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        40,
        140
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e0000000-0000-0000-0000-000000000006",
      "name": "Set Watch",
      "type": "n8n-nodes-base.set",
      "position": [
        280,
        140
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "keyword",
              "type": "string",
              "value": "domain"
            },
            {
              "id": "a2",
              "name": "exclude_suffixes",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e0000000-0000-0000-0000-000000000007",
      "name": "Find New Registrations",
      "type": "n8n-nodes-domainkits.domainKits",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        560,
        40
      ],
      "parameters": {
        "limit": 100,
        "filters": {
          "sort": "reg_date_desc"
        },
        "keyword": "={{ $json.keyword }}",
        "resource": "nrd",
        "operation": "search",
        "searchMode": "keyword"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "e0000000-0000-0000-0000-000000000008",
      "name": "Find Certificate Activity",
      "type": "n8n-nodes-domainkits.domainKits",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        560,
        240
      ],
      "parameters": {
        "keyword": "={{ $json.keyword }}",
        "resource": "ct",
        "ctOptions": {
          "limit": 100
        },
        "operation": "search"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "e0000000-0000-0000-0000-000000000009",
      "name": "Wait For Both",
      "type": "n8n-nodes-base.merge",
      "position": [
        960,
        140
      ],
      "parameters": {
        "mode": "chooseBranch",
        "options": {},
        "useDataOfInput": 1
      },
      "typeVersion": 3
    },
    {
      "id": "e0000000-0000-0000-0000-000000000010",
      "name": "Correlate And Diff",
      "type": "n8n-nodes-base.code",
      "position": [
        1200,
        140
      ],
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst cfg = $('Set Watch').first().json;\nconst excludes = String(cfg.exclude_suffixes || '')\n  .split(',').map(s => s.trim().toLowerCase()).filter(Boolean);\n\n// Certificate Transparency reports hostnames. Reduce each one to the name that\n// was actually registered so the two inputs can be compared.\nconst registrable = host => {\n  const parts = String(host).toLowerCase().replace(/^\\*\\./, '').split('.').filter(Boolean);\n  return parts.length <= 2 ? parts.join('.') : parts.slice(-2).join('.');\n};\nconst excluded = host => excludes.some(s => host === s || host.endsWith('.' + s));\n\nconst registered = new Map();\nfor (const item of $('Find New Registrations').all()) {\n  const d = item.json.domain;\n  if (d) registered.set(String(d).toLowerCase(), item.json.registered_date || '');\n}\n\nconst active = new Map();\nfor (const item of $('Find Certificate Activity').all()) {\n  const host = String(item.json.d || item.json.domain || '').toLowerCase();\n  if (!host || excluded(host)) continue;\n  const seen = item.json.ls || item.json.last_seen || '';\n  const root = registrable(host);\n  if (!active.has(root)) active.set(root, { hosts: [], last_seen: '' });\n  const rec = active.get(root);\n  rec.hosts.push(host);\n  if (seen > rec.last_seen) rec.last_seen = seen;\n}\n\nconst seenRegistrations = store.registrations || {};\nconst seenActivity = store.activity || {};\nconst firstRun = !store.last_run;\n\nconst findings = [];\n\nfor (const [domain, regDate] of registered) {\n  if (seenRegistrations[domain]) continue;\n  const act = active.get(domain);\n  findings.push({\n    domain,\n    registered_date: regDate,\n    last_seen: act ? act.last_seen : '',\n    hosts: act ? act.hosts : [],\n    quadrant: act ? 'registered and already active' : 'registered, not yet active',\n    why: act\n      ? 'A certificate showed up for this name in the same window it was registered'\n      : 'New registration with no certificate yet',\n  });\n}\n\nfor (const [domain, rec] of active) {\n  const prev = seenActivity[domain];\n  if (!prev) {\n    if (registered.has(domain)) continue;\n    findings.push({\n      domain,\n      registered_date: '',\n      last_seen: rec.last_seen,\n      hosts: rec.hosts,\n      quadrant: 'active, registered outside the search window',\n      why: 'Certificate activity on a name the registration search does not reach, so it was registered earlier',\n    });\n  } else if (rec.last_seen > prev) {\n    findings.push({\n      domain,\n      registered_date: registered.get(domain) || '',\n      last_seen: rec.last_seen,\n      hosts: rec.hosts,\n      quadrant: 'activity moved forward',\n      why: 'Certificate activity advanced from ' + prev + ' to ' + rec.last_seen,\n    });\n  }\n}\n\nstore.registrations = Object.fromEntries([...registered]);\nstore.activity = Object.fromEntries([...active].map(([d, r]) => [d, r.last_seen]));\nstore.last_run = $now.toISO();\n\n// Returning nothing stops the run here, so a quiet period sends no mail.\nif (!findings.length) return [];\n\nconst lookup = findings.map(f => f.domain).slice(0, 20);\n\nreturn [{\n  json: {\n    keyword: cfg.keyword,\n    first_run: firstRun,\n    findings,\n    finding_count: findings.length,\n    registrations_tracked: registered.size,\n    active_tracked: active.size,\n    lookup_domains: lookup,\n  },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e0000000-0000-0000-0000-000000000011",
      "name": "Resolve Nameservers",
      "type": "n8n-nodes-domainkits.domainKits",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1640,
        140
      ],
      "parameters": {
        "domains": "={{ $json.lookup_domains }}",
        "resource": "lookup",
        "operation": "dnsBulk"
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "e0000000-0000-0000-0000-000000000012",
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1880,
        140
      ],
      "parameters": {
        "jsCode": "const base = $('Correlate And Diff').first().json;\n\nconst dnsByDomain = new Map();\nfor (const item of $input.all()) {\n  const j = item.json;\n  if (j && j.domain) dnsByDomain.set(String(j.domain).toLowerCase(), j);\n}\n\nconst records = base.findings.map(f => {\n  const dns = dnsByDomain.get(f.domain) || {};\n  const ns = (dns.nameservers || []).map(n => String(n).toLowerCase()).sort();\n  return {\n    domain: f.domain,\n    quadrant: f.quadrant,\n    registered_date: f.registered_date || '',\n    certificate_last_seen: f.last_seen || '',\n    hostnames: (f.hosts || []).slice(0, 6).join(' | '),\n    nameservers: ns.join(' '),\n    listing_status: dns.domain_status || '',\n  };\n});\n\nconst byNs = new Map();\nfor (const r of records) {\n  if (!r.nameservers) continue;\n  if (!byNs.has(r.nameservers)) byNs.set(r.nameservers, []);\n  byNs.get(r.nameservers).push(r.domain);\n}\nconst shared = [...byNs.entries()].filter(([, ds]) => ds.length > 1);\n\nconst order = {\n  'registered and already active': 0,\n  'active, registered outside the search window': 1,\n  'activity moved forward': 2,\n  'registered, not yet active': 3,\n};\nrecords.sort((a, b) =>\n  (order[a.quadrant] ?? 9) - (order[b.quadrant] ?? 9) || a.domain.localeCompare(b.domain));\n\nconst lines = records.map(r => {\n  const bits = [r.domain, '  ' + r.quadrant];\n  if (r.registered_date) bits.push('  registered ' + r.registered_date);\n  if (r.certificate_last_seen) bits.push('  certificate last seen ' + r.certificate_last_seen);\n  if (r.hostnames) bits.push('  hostnames: ' + r.hostnames);\n  if (r.nameservers) bits.push('  nameservers: ' + r.nameservers);\n  if (r.listing_status) bits.push('  listing: ' + r.listing_status);\n  return bits.join('\\n');\n});\n\nconst head = [\n  'Changes around \"' + base.keyword + '\"',\n  records.length + ' new items. Tracking ' + base.registrations_tracked +\n    ' registrations and ' + base.active_tracked + ' names with certificate activity.',\n  '',\n  'Two inputs feed this report. The registration search covers names created inside its window. The certificate search covers names with recent activity in Certificate Transparency, whenever they were registered.',\n  '',\n];\n\nconst tail = [];\nif (shared.length) {\n  tail.push('', 'Names sharing a nameserver set in this batch:');\n  shared.forEach(([ns, ds]) => tail.push('  ' + ns, '    ' + ds.join(', ')));\n}\ntail.push('', 'Records are attached as CSV.');\n\nconst body = head\n  .concat(records.length ? lines.join('\\n\\n') : 'Nothing new since the last run.')\n  .concat(tail)\n  .join('\\n');\n\nreturn [{\n  json: {\n    subject: base.keyword + ': ' + records.length + ' new items',\n    message: body,\n    records,\n  },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e0000000-0000-0000-0000-000000000013",
      "name": "Send Alert",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        2120,
        140
      ],
      "parameters": {
        "text": "={{ $('Build Report').first().json.message }}",
        "options": {
          "attachments": "data"
        },
        "subject": "={{ $('Build Report').first().json.subject }}",
        "toEmail": "you@example.com",
        "fromEmail": "user@example.com",
        "emailFormat": "text"
      },
      "typeVersion": 2.1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000014",
      "name": "Split Records",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1880,
        320
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "records"
      },
      "typeVersion": 1
    },
    {
      "id": "e0000000-0000-0000-0000-000000000015",
      "name": "Records To CSV",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        2120,
        320
      ],
      "parameters": {
        "options": {},
        "operation": "csv"
      },
      "typeVersion": 1.1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Set Watch": {
      "main": [
        [
          {
            "node": "Find New Registrations",
            "type": "main",
            "index": 0
          },
          {
            "node": "Find Certificate Activity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          {
            "node": "Send Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Split Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Records": {
      "main": [
        [
          {
            "node": "Records To CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Both": {
      "main": [
        [
          {
            "node": "Correlate And Diff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 30 Minutes": {
      "main": [
        [
          {
            "node": "Set Watch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Correlate And Diff": {
      "main": [
        [
          {
            "node": "Resolve Nameservers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Nameservers": {
      "main": [
        [
          {
            "node": "Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find New Registrations": {
      "main": [
        [
          {
            "node": "Wait For Both",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Certificate Activity": {
      "main": [
        [
          {
            "node": "Wait For Both",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow runs every 30 minutes to monitor a keyword across DomainKits newly registered domain data and Certificate Transparency activity, correlates both feeds, and emails only what changed since the last run with a text report and attached CSV. Runs every 30 minutes on a…

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

More General workflows → · Browse all categories →

Related workflows

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

General

This workflow runs daily to monitor newly registered domains matching your brand keyword and to check typosquat variants of your brand domain using DomainKits, then compiles WHOIS/DNS details into a C

N8N Nodes Domainkits, Email Send
General

Perfect for content publishing with organic scheduling patterns, social media automation, API systems that need to avoid rate limiting, or any automation requiring randomised timing control across mul

n8n, Read Write File, Stop And Error +1
General

Complete backup solution that saves both workflows and credentials to local/server disk with optional FTP upload for off-site redundancy.

Read Write File, Email Send, Execute Command +3
General

dev_activity_reporter. Uses dataTable, emailSend. Scheduled trigger; 19 nodes.

Data Table, Email Send
General

This n8n workflow automates the secure transfer of files between FTP servers on a scheduled basis, providing enterprise-grade reliability with comprehensive error handling and dual notification system

Ftp, Email Send