Skip to content

Classifying new signups with n8n

· 7 min read · Tutorials

To classify new signups with n8n, send each signup to a Webhook node, call a domain lookup API with the HTTP Request node, and route on the result with a Switch node: disposable or machine-generated addresses to one branch, company emails to sales, webmail to nurture, role addresses to review. For bulk runs, dedupe by email domain first and use the HTTP Request node's batching option to stay under the API's rate limit.

This guide builds both: a real-time workflow that fires once per signup, and a nightly batch that reclassifies a day's worth of signups from your database. It uses pingutil as the lookup API (free key, 60 requests/minute) and only core n8n nodes.

Where n8n fits, and where it doesn't

n8n runs after your app has accepted the signup. That makes it the right place for routing (post the lead to Slack, create a CRM record, tag a user for review) and the wrong place for blocking. If you want to stop disposable addresses at the form, do it synchronously in your signup handler; the Next.js server action guide shows that pattern. Then let n8n handle everything that can happen a few seconds later.

A common split: block on verdict.is_spam in the app, and use n8n to classify everything that got through.

Workflow 1: classify each signup in real time

Webhook → HTTP Request (pingutil) → Switch
                                      ├─ spam      → flag user / alert
                                      ├─ review    → review queue
                                      ├─ company   → Slack #sales, CRM
                                      └─ fallback  → nurture list

Webhook node

  • HTTP Method: POST
  • Path: something like new-signup
  • Authentication: Header Auth. Create a credential with a header name such as X-Signup-Secret and a long random value; your app sends the same header.
  • Respond: Immediately. Your app shouldn't wait for the lookup.

Your app posts { "email": "jane@acme.com", "user_id": "u_123" } after creating the account. In n8n the payload arrives under body, so the email is {{ $json.body.email }}. Use the test URL while building and switch your app to the production URL once you publish the workflow.

HTTP Request node

  • Method: POST
  • URL: https://pingutil.com/v1/lookup
  • Authentication: Generic Credential Type, then Header Auth. Header name Authorization, value Bearer pk_live_your_key. Storing it as a credential keeps the key out of the workflow JSON when you export or share it.
  • Send Body: on. Body Content Type: JSON. Specify Body: Using Fields Below, with one field: input = {{ $json.body.email }}.

That's a full lookup, which includes company enrichment for company domains. First-seen company domains can take a few seconds while the LLM profile is generated; cached domains come back fast. If you only need the category and spam verdict, switch to Using JSON and send:

{
  "input": "{{ $json.body.email }}",
  "skip_enrichment": true
}

The node's output is the API response, so downstream nodes read {{ $json.category }}, {{ $json.verdict.is_spam }}, {{ $json.company.name }} and so on. The original webhook data is still reachable with {{ $('Webhook').item.json.body.user_id }}.

The fields you'll route on:

Field Values Use
category company, free, disposable, isp, educational, government, unknown Main routing key
verdict.is_spam boolean True for disposable domains, reputation-flagged domains and randomized local parts
verdict.low_trust boolean True for role, system and sales-outreach addresses
verdict.reason disposable_domain, randomized_local_part, role_based, system_generated, sales_outreach, null Put this in alerts so humans see why
company object or null Present only for company domains

Switch node

Mode Rules, with Send data to all matching outputs off so each signup goes to the first rule it matches. Order matters.

  1. Boolean {{ $json.verdict.is_spam }} is true. Rename output to spam.
  2. Boolean {{ $json.verdict.low_trust }} is true. Rename output to review.
  3. String {{ $json.category }} is equal to company. Rename output to company.
  4. Under Options, set Fallback Output to Extra Output and call it other. That catches free, isp, educational, government and unknown.

If you only need two paths ("suspicious" vs "fine"), an If node with a single Boolean condition on verdict.is_spam is simpler. Reach for Switch once you have more than two destinations.

What to do on each branch

  • spam: don't delete the account from n8n. Set a flag in your database through its API, or post to a private Slack channel with the email and verdict.reason. A randomized_local_part verdict can misfire on handles like coolcat123@, so a human glance is cheap insurance. The machine-generated address guide explains what that heuristic catches and misses.
  • review: info@, sales@, noreply@. Often real companies using a shared inbox. Route to a lower-priority queue rather than rejecting; see what to do with role-based addresses.
  • company: post to a sales channel with company.name, company.industry and company.employee_size_estimate, or create a CRM lead. Treat company.confidence below about 0.7 as a hint, not a fact; it's the model's own estimate.
  • other: webmail and ISP users are real people, just not obviously business leads. Send them to your normal onboarding.

Handling errors and rate limits

Three failure modes matter.

429 rate_limited. One signup at a time won't hit 60 per minute unless you're having a very good day or a bot attack. If it happens, the error body includes retry_after_seconds. In the HTTP Request node's settings, set On Error to Continue (using error output). Wire the error output to a Wait node (Resume: After Time Interval, Wait Amount: 60, Wait Unit: Seconds) and then to a second copy of the HTTP Request node. Sixty seconds covers the full rate-limit window. Per n8n's docs, waits under 65 seconds stay in memory rather than being written to the database, so this is cheap.

Upstream errors (5xx, timeouts). Turn on Retry On Fail in the HTTP Request node's settings for transient failures. If the second attempt also fails, route that error output to the other branch. Fail open: an unclassified signup is a normal signup.

400 invalid_input. The API rejects inputs it can't parse as an email or domain (IP literals, reserved TLDs like .test). If your app already validated the address, this is rare. Send it to the review branch.

Workflow 2: a nightly batch with domain dedupe

Real-time is ideal, but sometimes you want to reclassify a backlog: signups from before you built Workflow 1, or a day's worth from a system that can't call webhooks. pingutil has no bulk endpoint, so a batch is a loop of single lookups, and the cheapest way to make it fast is to send fewer of them.

Category and the disposable verdict are properties of the domain, and pingutil keys results on the registrable domain (eTLD+1). Ten signups from acme.com need one lookup, not ten.

Schedule Trigger → Postgres (yesterday's signups) → Edit Fields (add domain)
     ├──────────────────────────────────────────────────────────────┐
     └→ Remove Duplicates → HTTP Request → Edit Fields (lookup_domain) ─→ Merge → Switch
  1. Schedule Trigger, daily.
  2. Your database node (Postgres, MySQL, or whatever stores users) returns id and email for the period.
  3. Edit Fields (Set) adds domain = {{ $json.email.split('@').pop().toLowerCase() }}. Keep the other fields.
  4. Remove Duplicates, operation Remove Items Repeated Within Current Input, Compare: Selected Fields, field domain.
  5. HTTP Request as before, but send { "input": "{{ $json.domain }}", "skip_enrichment": true }. Under Options, add Batching: Items per Batch 1, Batch Interval 1100 (milliseconds). That paces requests at about 55 per minute, under the 60/min key limit.
  6. Edit Fields (Set) after the HTTP Request adds lookup_domain = {{ $('Remove Duplicates').item.json.domain }}, with "include other input fields" on. You need this because the API response's own domain is the normalized registrable domain, which won't always equal what you sent (eu.acme.com comes back as acme.com).
  7. Merge, mode Combine, Combine By: Matching Fields. Input 1 is the full list from step 3, Input 2 is the output of step 6. Match domain (Input 1) to lookup_domain (Input 2), Output Type: Enrich Input 1. Every original signup now carries its domain's category and verdict.
  8. Switch on category and verdict.is_spam as in Workflow 1, then write results back to your database.

Two caveats. First, a bare-domain lookup doesn't compute email_signals, so the batch won't flag info@ or randomized local parts. If those matter, skip the dedupe and send full addresses, and accept one request per row. Second, local dedupe uses the host as typed, while pingutil collapses subdomains (eu.acme.com becomes acme.com, see eTLD+1 explained). Matching on lookup_domain keeps the Merge correct; you just spend an extra request per subdomain variant.

At about 55 lookups a minute, 3,000 unique domains take close to an hour. That's fine for a nightly job. If it isn't, it's a sign the job belongs in a script; see cleaning an email list from a CSV.

Before you publish the workflow

  1. Run the test URL with three addresses: a Gmail address, a known disposable one like test@mailinator.com, and a work address. Check each lands on the expected Switch output.
  2. Confirm the Header Auth credential is set on the Webhook node; an open webhook is an open door into your CRM.
  3. Make sure the error outputs are wired. An unwired error output with Continue selected silently drops items.
  4. Note that blocklists lag behind brand-new disposable domains. The workflow catches known ones, and the disposable domains directory shows what's covered.

The n8n reference pages for the HTTP Request node and Switch node list every option used above if your version labels them differently.

Questions people ask

Should n8n block disposable signups?
No. An n8n workflow runs after your app has already accepted the signup, so it is the wrong place to reject one. Block in your signup handler with a synchronous check and use n8n for routing, alerts and cleanup.
How do I stay under the API rate limit when n8n processes many items?
Use the HTTP Request node's Batching option with one item per batch and a batch interval of about 1100 milliseconds, which keeps you near 55 requests per minute. For lists, dedupe by domain first so repeated domains cost one request.
Does this work on n8n Cloud and self-hosted n8n?
Yes. The workflow only uses core nodes (Webhook, HTTP Request, Switch, Edit Fields, Remove Duplicates, Merge and Wait), which are available in both.