Skip to content

Lead Scoring Integration

Bridges the visitor tracking system and lead capture system so every lead is enriched with an engagement score, helping sales teams prioritize follow-ups.

How It Works

Visitor browses site → tracker.js records behavior → engagement data in D1
Visitor submits form → form.js reads visitor_id from localStorage
Lead API receives form → looks up session by visitor_id (or IP fallback)
Lead stored in D1 + sent to webhook → enriched with scoring fields

Data Flow

  1. tracker.js stores visitor_id in localStorage as _pg_vid
  2. form.js reads _pg_vid and injects it as hidden field __visitor_id
  3. lead.ts receives the form POST with __visitor_id
  4. lookupVisitorScore() queries:
    • Primary: sessions WHERE visitor_id = ? (most recent)
    • Fallback: sessions WHERE ip = ? only when visitor_id was provided but no session was found (e.g. session expired). If the request has no __visitor_id (API, Postman, server-to-server), no IP fallback is used — the lead is stored with score 0 and label "Cold".
  5. Fetches fingerprint for return visit count and VPN flag
  6. Calculates score inline and enriches the lead payload
  7. Webhook to Make.com carries full scoring data
  8. D1 Leads table stores scoring columns

Scoring Model (Real Estate Optimized)

FactorPointsLogic
Duration0-351 pt per 10 seconds, cap at ~6 min
Scroll Depth0-301 pt per 3.33% depth, full scroll = 30
Clicks0-201 pt per click, cap at 20
Return Visits0-155 pts per return session, cap at 3 returns
Total0-100

Why These Factors for Real Estate

  • Duration: A buyer spending 5-10 min reading floor plans and amenities shows strong intent
  • Scroll Depth: Reaching the pricing/contact section at the bottom = high intent
  • Clicks: Gallery photos, floor plan tabs, brochure downloads = active interest
  • Return Visits: Coming back to the same property page = serious buyer

Engagement Labels

ScoreLabelRecommended Action
0-20ColdBounced or bot — low priority
21-45WarmBrowsed briefly — follow up within 24h
46-70HotRead the page, looked at details — call within 2h
71-100Very HotDeep engagement + return visits — call immediately

Webhook Payload (New Fields)

json
{
  "visitor_id": "abc123...",
  "session_id": "sess-xyz...",
  "engagement_score": 72,
  "engagement_label": "Very Hot",
  "pages_visited": 3,
  "session_duration_seconds": 340,
  "return_visits": 2,
  "vpn_suspected": 0
}

D1 Schema Changes (Migration 005)

sql
ALTER TABLE Leads ADD COLUMN VisitorId TEXT;
ALTER TABLE Leads ADD COLUMN SessionId TEXT;
ALTER TABLE Leads ADD COLUMN EngagementScore REAL DEFAULT 0;
ALTER TABLE Leads ADD COLUMN EngagementLabel TEXT DEFAULT 'Cold';
ALTER TABLE Leads ADD COLUMN PagesVisited INTEGER DEFAULT 0;
ALTER TABLE Leads ADD COLUMN SessionDuration INTEGER DEFAULT 0;
ALTER TABLE Leads ADD COLUMN ReturnVisits INTEGER DEFAULT 0;
ALTER TABLE Leads ADD COLUMN VpnSuspected INTEGER DEFAULT 0;

Graceful Degradation

  • If tracker.js hasn't loaded → no _pg_vid in localStorage → __visitor_id not sent → no IP fallback → score 0, "Cold"
  • If __visitor_id was sent but has no matching session → falls back to IP lookup (same visitor, different device or expired session)
  • If the lead is submitted via API/Postman (no __visitor_id) → no IP fallback, so we never attach another session's engagement → score 0, "Cold"
  • If no session found at all → lead saved with score 0, label "Cold"
  • Scoring never blocks lead submission — errors caught and logged

Website vs API Leads (scoring by channel)

Leads can come from the website (tracked form with __visitor_id) or from internal APIs (Facebook, WhatsApp, JustLead, etc.) where there is no browser session.

ChannelEngagement scoreSuspicion scoreHow to distinguish
Website0–100 from session (duration, scroll, clicks, returns)From honeypot, time-to-submit, VPN, duplicate rate, etc.lead_source = "WebSite" (or no SOURCE in body).
APINot available — stored as 0, label "Cold"Still computed from payload: no_tracking_session (+15), duplicate count, email domain, honeypot/timeToSubmit if API sends them.lead_source = value sent in SOURCE (e.g. "Facebook", "WhatsApp", "JustLead").

Recommendation

  1. API submissions
    Send SOURCE in the request body (e.g. SOURCE: "Facebook", "WhatsApp", "JustLead"). The worker stores it as lead_source so you can filter and report by channel.

  2. Engagement in UI/reports
    For leads where lead_source is not "WebSite" (or where there was no session), treat engagement as not applicable: show "—" or "N/A" instead of "Cold", so it’s clear we had no behavioral data rather than a low score.

  3. Suspicion for API
    Suspicion remains meaningful: we still add no_tracking_session, use duplicate count, email domain, and (if the API sends them) honeypot and time-to-submit. Use it to flag risky API leads.

  4. No session = no engagement
    We do not infer engagement for API leads (e.g. no "medium" default), so analytics and moderation stay consistent: engagement is only from website sessions.