Lead Scoring, Contact Forms, and Sales PipelineNext.js Integration (2026)

lead scoring6 min readJuly 15, 2026

Author: DevStudio.it

TL;DR

Lead scoring is a number or label (cold / warm / hot) that tells sales who to call first — based on form data, on-site behavior, and campaign source. A contact form in Next.js is not just mailto: — it is a Server Action saving the lead to PostgreSQL on Branchly Cloud, a CRM webhook, and a generate_lead event in GA4. The sales pipeline maps statuses (new → qualified → proposal → won) to automation: owner assignment, CRM task, Slack when score > 70. Below: data model, scoring rules, form integration, and anti-spam.

Who is this for

  • B2B companies getting dozens of form inquiries monthly and losing hot leads in a shared "info@" inbox
  • Sales teams without clear "who is priority" criteria — every lead looks the same
  • Developers implementing forms in Next.js 15 (Server Actions) with database and CRM persistence
  • Marketing wanting to connect UTM / GA4 with lead quality, not just submit count
  • Site owners after a DevStudio launch — extending the form with scoring without rewriting the whole site

Keyword

lead scoring contact form, sales pipeline crm, nextjs contact form, server actions lead, crm webhook integration, b2b lead points

From form to deal — process map

Typical flow in a corporate site project:

Site (UTM in session) → Form submit → Server-side validation
    → Save Lead in Branchly (PostgreSQL)
    → Compute score + segment
    → CRM webhook (HubSpot / Pipedrive / custom)
    → Notification (email / Slack) if hot
    → Pipeline: statuses and sales tasks

The form is the system entry point, not the end. Without database and CRM persistence, every submit is an email in a shared inbox — unmeasurable in reports and easy to lose.

Data model — Lead and Score

Prisma on Branchly (illustrative fragment):

model Lead {
  id          String   @id @default(cuid())
  email       String
  company     String?
  budget      String?
  message     String?
  utmSource   String?
  utmCampaign String?
  score       Int      @default(0)
  segment     LeadSegment @default(COLD)
  status      LeadStatus  @default(NEW)
  createdAt   DateTime @default(now())
}

enum LeadSegment {
  COLD
  WARM
  HOT
}

enum LeadStatus {
  NEW
  CONTACTED
  QUALIFIED
  PROPOSAL
  WON
  LOST
}

Score can be a weighted sum of fields and events. Set segment thresholds: 0–39 cold, 40–69 warm, 70+ hot — calibrate quarterly from conversion to won.

Lead scoring rules — what to score

Signal Example weight Rationale
Budget "> 50k" +30 Investment readiness
Company filled (not gmail) +20 B2B vs consumer
utm_medium=cpc +15 Paid traffic intent
Session pages /pricing, /case-studies +10 each Research phase
Message > 200 chars +10 Specific need
Free email (gmail, etc.) −15 Lower B2B value
Missing phone when required −10 Harder to reach

Keep rules in code (TypeScript) or a ScoringRule table — the latter lets marketing change weights without deploy but needs an admin UI.

Example scoring function:

function computeLeadScore(input: LeadInput, sessionPages: string[]): number {
  let score = 0;
  if (input.company && !isFreeEmail(input.email)) score += 20;
  if (input.budget === '50000+') score += 30;
  if (input.utmMedium === 'cpc') score += 15;
  if (sessionPages.includes('/pricing')) score += 10;
  if ((input.message?.length ?? 0) > 200) score += 10;
  if (isFreeEmail(input.email)) score -= 15;
  return Math.max(0, Math.min(100, score));
}

Next.js form — Server Action end-to-end

Server Action validates (Zod), scores, saves, sends webhook:

'use server';

import { z } from 'zod';
import { prisma } from '@/lib/prisma';
import { computeLeadScore } from '@/lib/scoring';

const schema = z.object({
  email: z.string().email(),
  company: z.string().optional(),
  budget: z.enum(['unknown', '10000', '50000+']).optional(),
  message: z.string().max(5000),
  utmSource: z.string().optional(),
  utmMedium: z.string().optional(),
});

export async function submitContactForm(formData: FormData) {
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { ok: false, errors: parsed.error.flatten() };

  const score = computeLeadScore(parsed.data, []);
  const segment = score >= 70 ? 'HOT' : score >= 40 ? 'WARM' : 'COLD';

  const lead = await prisma.lead.create({
    data: { ...parsed.data, score, segment: segment as any, status: 'NEW' },
  });

  await fetch(process.env.CRM_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ leadId: lead.id, score, segment }),
  });

  return { ok: true, leadId: lead.id };
}

The client form passes UTMs from sessionStorage or hidden fields set after page_view. Hosting on DevStudioIT Cloud provides stable Server Actions runtime and env with CRM_WEBHOOK_URL / DATABASE_URL to Branchly.

Sales pipeline in CRM

The pipeline is Kanban columns matching LeadStatus. Automations:

Status Automatic action
NEW + HOT Assign senior AE, Slack within 15 min
NEW + COLD Nurturing email, no day-zero call
CONTACTED Task "follow up in 3 days"
QUALIFIED Create deal, estimate value
PROPOSAL Reminder after 7 days silence
WON / LOST Close, optional GA4 offline conversion sync

CRM receives webhook with score and segment — map to custom fields and "Hot leads this week" lists. One segment definition company-wide — otherwise marketing and sales speak different languages.

Anti-spam, GDPR, and data quality

  • Honeypot + rate limit (IP / fingerprint) — less noise in scoring
  • Double opt-in for newsletter vs single submit for quote request — different consents in privacy policy
  • Consent for processing and sales contact — checkbox before submit
  • Lead storage logs in EU (Branchly) — audit who accessed data

Bot-driven scores lower quality — filter leads with same IP > 5/h.

GA4 and marketing feedback loop

Event generate_lead with lead_score, lead_segment (custom dimensions) compares campaigns by average score, not just submit count. A cheaper CPC campaign with lower avg score may be worse than an expensive one delivering hot leads.

FAQ

How many scoring rules at start?

Five to seven signals is enough. Over-engineered day-one models — nobody validates weights. After 90 days compare hot lead scores with won conversion and adjust thresholds.

Does lead scoring replace qualification calls?

No. Score prioritizes the queue; BANT/MEDDIC qualification stays with sales. Auto-rejecting cold leads without company policy wastes long-tail opportunity.

HubSpot vs own database on Branchly?

Small companies: form → Branchly + webhook to HubSpot free. Larger: lead in PostgreSQL as source of truth, bidirectional CRM sync. Next.js Server Action fits both — consistent lead ID matters.

How to test pipeline without spamming CRM?

Staging on Branchly + CRM sandbox (see staging with Branchly). Test submit with [TEST] prefix in company.

Want a form that delivers leads to sales?

Related posts

n8n — Form Automation: Webhook from Next.js → CRM → Slack (2026)
6 min read
Error Monitoring in Next.js — Sentry in Production, Alerts, and PII Scrubbing (2026)
6 min read
Next.js 15 Server Actions vs Route Handlers — contact forms in 2026
10 min read

About the author

We build fast websites, web/mobile apps, AI chatbots and hosting setups — with a focus on SEO and conversion.

Recommended links

From theory to production — Branchly, our hosting stack and shipped work.

Like how we think? Let's build something together.

Start project configuration