Lead Capture
The lead capture system in src/lead.ts handles form submissions from marketing websites and enriches them with behavioral intelligence before forwarding to CRM.
Flow
- Parse — Accepts JSON, form-data, multipart, and text/plain request bodies
- Extract — Pulls lead fields: name, email, phone, project, location, team
- Enrich — Adds geolocation from Cloudflare headers (country, city, timezone, region)
- Detect — Checks for bot submissions using honeypot signal (
__pg_hp) whenBOT_LEAD_DETECTIONis enabled - Score — Looks up visitor's tracking session for engagement scoring (see Lead Scoring)
- Assess suspicion — Calculates fraud/suspicion score from honeypot, time-to-submit, and other signals (see Fraud Detection)
- Link projects — Finds other projects this visitor previously inquired about (see Related Projects)
- Resolve primary lead — Finds existing lead by project + identity (
visitor_id> email > phone > IP) - Store submission history — Writes every form submit to
LeadSubmissions(CTA/page history) - Forward — Sends enriched payload to Make.com webhook for new primary leads
- Store — Writes/updates canonical lead in D1
Leadstable - Redirect — Returns 301 redirect to the source domain's thank-you page
Lead Payload
Every lead sent to the webhook and stored in D1 includes:
| Category | Fields |
|---|---|
| Contact | first_name, phone_mobile, email, email1 (legacy compatibility), country_code |
| Project | project_name, user_location, lead_source, source_team |
| Geo | user_country, user_city, user_timezone, user_region |
| Scoring | engagement_score, engagement_label, session_duration_seconds, pages_visited, return_visits |
| Fraud | suspicion_score, suspicion_reasons, time_to_submit, form_submit_count |
| Identity | visitor_id, session_id, vpn_suspected |
| Context | related_projects, refered_by, searched_keyword, user_agent, user_ip |
| Status | is_duplicate, is_bot_lead, test_lead, dev_environment |
Storage Model
Leadsstores one canonical row per user+project (primary lead record).LeadSubmissionsstores every submit attempt (including repeated submits from different CTAs/pages). Each row includesPayloadJson(raw form body at submit time) so the dashboard can show exactly what was submitted in each follow-up.
Dashboard
In the dashboard Leads page, when you click a primary lead the detail panel shows:
- Follow-up history — A list of every form submission (primary + follow-ups) for that lead. Expand any row to see the data as submitted at that time: name, email, phone, project, source, CTA, and optional message. Use this to see how lead data changed across multiple submits (e.g. corrected phone number or name).
- Activity timeline — Combined submissions and sessions with optional session replay links.
Configuration
| Variable | Default | Description |
|---|---|---|
ENVIRONMENT | — | development or production |
BOT_LEAD_DETECTION | true | Enable bot handling using honeypot detection (__pg_hp) |
Webhook Integration
Leads are forwarded to Make.com for CRM integration. Two webhook endpoints are used:
| Webhook | Trigger |
|---|---|
| Make.com | Production non-duplicate leads |
| Bot webhook | Bot-detected leads (when BOT_LEAD_DETECTION is enabled) |
Bot-detected submissions are still persisted to D1 (Leads + LeadSubmissions) for audit/history; only CRM forwarding is suppressed.
Retry Strategy
Webhook delivery uses exponential backoff retry via src/lib/webhook.ts:
- Max retries: 3 attempts
- Backoff: 1s → 2s → 4s between attempts
- Timeout: 10s per request
- 5xx responses: Retried (server error, likely transient)
- 4xx responses: Not retried (client error, won't resolve on retry)
- Network errors: Retried (DNS failure, connection reset, timeout)
Dead Letter Queue
On final failure (all retries exhausted), the payload is stored in KV:
- Key:
dead_letter:{timestamp}:{random_id} - TTL: 7 days
- Contents: webhook URL, full payload, error message, attempt count, failure timestamp
Leads are always persisted to D1 first, so the dead letter queue is a safety net for webhook delivery only — no data is lost even if the webhook permanently fails.