Lead Generation Suite NEW in v1.2
Collect, verify, score, and activate outbound prospects — all inside KodMail. Prospects live in a customer-scoped leads table with dedup on (customer_id, email), so every collector is idempotent and every enrichment run is quota-metered.
Where it lives
Customer sidebar → Growth group. Three entries:
- Leads — the prospect list. Filters by ICP tier (A/B/C/D), verification status, source, lifecycle status.
- Lead sources — origin metadata for every batch. One row per CSV import, URL scrape, or hosted form.
- ICP profiles — plain-English descriptions of your ideal customer; used by the AI scorer.
Plan gating
Every collector + enrichment path is gated by a per-plan quota key. Ships enabled on Starter+; Free tier gets a taste (100 stored leads, 20 verifications/month) so buyers can trial before upgrading.
| Quota key | Free | Starter | Pro | Enterprise |
|---|---|---|---|---|
max_leads_stored | 100 | 2,500 | 25,000 | ∞ |
max_lead_enrichments_per_month | 20 | 500 | 5,000 | ∞ |
max_lead_scoring_calls_per_month | 20 | 500 | 5,000 | ∞ |
max_lead_sources_per_month | 5 | 25 | 100 | ∞ |
max_hosted_forms | 0 | 3 | 25 | ∞ |
allow_url_scrape | ✓ | ✓ | ✓ | ✓ |
allow_hosted_form | — | ✓ | ✓ | ✓ |
Collectors — how prospects get in
1. CSV bulk import
Leads index → Import CSV. Auto-detects header aliases — you don't have to name columns a specific way:
email·e_mail·email_address·work_emailfirst_name·firstname·first·given_namelast_name·lastname·surnamecompany_name·company·organization·employercompany_domain·domain·websitetitle·phone·country·region·city·industry·linkedin_url
Upserts on (customer_id, email). Missing company_domain is auto-filled from the email suffix. Sync cap 5,000 rows per upload — split larger files into batches.
2. Hosted lead form
Lead sources → Add source → type = Hosted form. On creation, a 24-character token is generated and a public URL becomes available: https://your-site.com/p/lead-form/<token>. Copy it from the source row and embed anywhere — landing page, link in bio, email signature.
Anti-spam: hidden honeypot field + per-IP rate limit (10 submissions/hour per source). Bots that populate the honeypot get a fake success screen and are silently dropped.
Default fields: email, first name, last name, company, title, phone. Edit the config later to add or remove fields (buyer-facing editor lands in a follow-up patch; for now the form JSON is admin-editable on the source row).
3. URL scrape
Lead sources → Add source → type = URL scrape → paste the page URL. Auto-queues; scraper fetches the page (10-sec timeout, custom UA), extracts emails from mailto: anchors + body text, dedupes, and filters obvious noise (example.com, yourdomain.com, image filenames masquerading as emails).
Company domain per lead is derived from the email suffix; falls back to the page host if the email is on a generic provider.
Verification pipeline
Every lead can be verified before you spend send credits on it. Layered checks, cheapest first:
- Syntax — regex +
filter_var. Bad syntax →invalid. - Disposable-domain blocklist — mailinator, tempmail, guerrillamail, etc. →
invalid. - Role account detection —
info@,sales@,noreply@→ flagis_role_account. - MX lookup — no MX + no A record →
invalid (no_mx). - SMTP handshake — connect to lowest-priority MX,
HELO/MAIL FROM/RCPT TOprobe (noDATA). Also probes a random address on the same domain to detect catch-all.
Verdict: valid (accepted, not catch-all) · risky (catch-all or role account on unreachable host) · invalid (rejected or bounced pre-DATA) · unknown (network blocked port 25 — most shared hosts).
Trigger: Leads index → Verify unknowns (batch, queues one VerifyLeadJob per lead, respects remaining monthly quota). Or open a lead → Re-verify button on the verification card.
ICP scoring
Describe your ideal customer in plain English on an ICP profile. Copilot's AI provider scores every lead 0–100 against it, with a written reasoning paragraph.
Example profile:
B2B SaaS founder or head of growth at a US company with 10–50 employees,
currently spending on paid acquisition. Bonus points for HubSpot,
Segment, or Mixpanel in their stack.
Score buckets (dropdown filter on Leads index):
- A — 76-100 (best fit, prioritise for outreach)
- B — 51-75 (good fit, second-wave)
- C — 26-50 (weak fit, nurture only)
- D — 0-25 (drop or archive)
Trigger: Leads index → Score unscored (batch). Uses the default ICP profile unless you pass rule_id via the Copilot tool.
Cost is logged per call in lead_enrichments.cost_estimate so admins can bill accurately.
Activation — push to a list
Bulk-select prospects on the Leads index (checkboxes), then click Push to list. Modal:
- Pick a target EmailList
- Skip invalid emails (default on)
- Also skip risky — catch-all + role accounts (default off)
Selected leads become ListSubscriber rows on the chosen list. Idempotent — already-subscribed emails are skipped. Consent metadata (consent_method = lead_push) and source (import) are recorded on every subscriber. Lead status flips to opted_in.
Double-opt-in lists insert subscribers as unconfirmed; single-opt-in lists insert as confirmed with confirmed_at = now().
Copilot tools
Four new tools auto-register in Copilot. Ask in plain English:
- find_leads — "find A-tier valid prospects at acme.com"
- verify_lead — "verify lead 42"
- score_prospect — "how does lead 42 fit our ICP?" (distinct from the older
score_leadtool which scores existing subscribers by engagement) - push_leads_to_list — "push A-tier verified leads to list 5" (marked destructive → Copilot asks confirmation)
Data model — for developers
lead_sources → origin metadata (customer_id, name, type, config, status, leads_added)
leads → prospect rows (customer_id, email, firmographics, verification flags, icp_score, custom_fields)
lead_enrichments → per-call audit trail for quota + billing (provider, status, result, duration_ms, cost_estimate)
lead_score_rules → ICP profiles (customer_id, name, icp_description, weights, is_default)
Tenant boundary is enforced by Lead::scopeVisibleTo($customer) on the model + controller-level authorization guards. Every controller uses the scope, never a raw WHERE customer_id.
Common questions
My verification returns "unknown" for everything
Your host blocks outbound port 25 (very common on shared hosting, DigitalOcean droplets, and most PaaS providers). This is treated as risky not invalid so you don't false-negative real leads. For deep verification, wire an external provider (Snov, Hunter) in v1.2.1.
Can I export leads?
Yes — REST API endpoint GET /api/v1/leads ships in v1.2.1. In v1.2, click any lead → detail page, or use the Copilot find_leads tool which returns up to 20 rows as JSON.
Do scraped leads count against my max_leads_stored quota?
Yes — every collector honors it. Over-quota new inserts are silently skipped; already-known emails are still updated. The response summary shows skipped_quota so you know why.