44 documents
Integrations & APIs⏱️6 min read

Lead capture and delivery

Verified against this repository on 2026-07-30. Variable **names** only — never paste a URL, token, or secret value here.

Lead capture and delivery

Verified against this repository on 2026-07-30. Variable names only — never paste a URL, token, or secret value here.

This is the conversion half of the offer: ../sales/pitch-script.md sells it as a "digital receptionist", and Step 2 of the demo shows a lead arriving live (../sales/demo-framework.md). Decision record: ../decisions/0004-single-crm-webhook-vs-hosted-crm.md.

Flow

MCP tool  request_dev_consultation ──┐
REST      POST /api/leads/submit ────┼──▶ lib/leads.ts submitConsultationLead()
                                     │        │
                                     │        ├─ parseConsultationLead() — Zod validation
                                     │        ├─ client_id (body / X-Client-ID / X-API-Key; default "default")
                                     │        ├─ lead_id = "jsg-" + randomUUID()
                                     │        ├─ received_at = ISO-8601
                                     │        │
                                     │        ▼
                                     │   resolveClientWebhookUrl(client_id)
                                     │        ├─ CLIENT_WEBHOOK_MAP[client_id]
                                     │        ├─ else CLIENT_WEBHOOK_<CLIENT_ID>
                                     │        ├─ else CRM_WEBHOOK_URL
                                     │        ├─ URL ──▶ POST JSON ──▶ Apps Script ──▶ Sheet (incl. client_id col)
                                     │        └─ none ──▶ console.info only (local / unconfigured)

Dogfood: use client_id: "jsglobtech" in demos so the Sheet proves tenancy (../sales/tenant-demo-checklist.md).

POST /api/contact is a separate on-site form path with a different payload and does not flow through submitConsultationLead().

REST surface: POST /api/leads/submit

Item Detail
Route file app/api/leads/submit/route.ts
Runtime nodejs
Methods POST, OPTIONS
CORS Access-Control-Allow-Origin: *, methods POST, OPTIONS, headers Content-Type, Authorization, X-Client-ID, X-API-Key
Rate Limiting Enforced via lib/rate-limit.ts (10 req/min per IP). Returns 429 with X-RateLimit-* headers.
Multi-Tenancy Supports client_id parameter or X-Client-ID / X-API-Key headers for dynamic tenant webhook routing.
Recorded source "openapi"
Spec lib/openapi-spec.ts, operationId submitDevConsultationLead; served at /docs/openapi.json and /api/openapi.json with CORS *

Responses

Status Body error When
200 — { ok: true, lead_id, client_id, message, project_type, received_at }
400 invalid_json Body was not parseable JSON
400 validation_error Zod failure; includes issues[] with path and message
429 rate_limit_exceeded Rate limit exceeded (10 requests/min per IP)
502 webhook_error Lead validated but CRM delivery failed; response suggests emailing getintouch@jsglobtech.com
500 internal_error Anything else; logged as [leads/submit] unexpected error

Note the deliberate asymmetry: a webhook failure is a 502, not a 500, because the caller's payload was fine. Agents can distinguish "fix your input" from "retry later".

Validation contract (lib/leads.ts)

consultationLeadSchema, shared by both surfaces:

Field Rule
client_id Optional trimmed string (max 100 chars), defaults to "default". Used for multi-tenant webhook routing.
client_name Trimmed string, min 2 chars
client_email_or_phone Trimmed, min 5, must match an email pattern or a phone pattern (^[+]?[\d\s().-]{7,}$)
project_type Enum PROJECT_TYPES: flutter_mobile_app, web_application, geo_ai_integration
budget_range_usd Positive finite number or non-empty trimmed string
requirements_notes Optional, trimmed, max 5000 chars, defaults to ""

Error types: LeadValidationError (carries issues: z.ZodIssue[]) and LeadWebhookError (carries the upstream status, message truncated to 500 chars).

LEAD_SUCCESS_MESSAGE is the single user-facing string returned by both surfaces:

"Success! JSGlobtech technical team notified. We will reach out on WhatsApp/Email within 1 hour."

This is a commitment made to every lead by an AI agent, unattended. Treat the 1-hour follow-up as an operational obligation — see ../operations/client-engagement-sop.md.

Delivery payload

Sent to CRM_WEBHOOK_URL as JSON with Content-Type: application/json and User-Agent: JSGlobtech-LeadCapture/1.0:

Key Value
lead_id jsg-<uuid> (randomUUID() from node:crypto)
client_id Tenant key (effective ID after header/body resolution)
source "mcp" | "openapi" | "api"
agency "JSGlobtech"
location "Dubai, UAE"
…validated lead fields client_name, client_email_or_phone, project_type, budget_range_usd, requirements_notes
received_at ISO-8601 timestamp

Outbound request also sets header X-Client-ID to the effective tenant id.

Any non-2xx response from the webhook raises LeadWebhookError. There is no retry and no queue — a webhook outage means the lead is lost beyond what is in the server log.

Destination

Item Detail
Env vars CRM_WEBHOOK_URL (default), optional CLIENT_WEBHOOK_<ID>, optional CLIENT_WEBHOOK_MAP — names only; never record values here
Recommended target Google Apps Script Web App appending rows to a Google Sheet
Script source scripts/google-apps-script/Code.gs
Setup guide scripts/google-apps-script/SETUP.md
Deploy settings Web app, Execute as Me, Who has access Anyone
Sheet columns lead_id, date, time, client_id, source, contact/project fields, agency, location (Asia/Dubai date/time)
When unset Lead is validated and logged via console.info — local dev works with no webhook. Note: HTTP still returns 200 ok in that mode, so always verify the Sheet mid-demo.

The deployed Web App URL is effectively a write credential: anyone holding it can append rows. Keep it in .env.local and host environment settings only.

⚠ WhatsApp is not a wired channel

The playbook and pitch both promise that lead details "land directly in your WhatsApp or CRM instantly" (§3 Tier 2, §4 pitch script), and GeoSchema.tsx lists "CRM/WhatsApp webhook lead routing" as a feature.

What is actually implemented is one generic outbound webhook. It can be pointed at any receiver — including something that relays to WhatsApp — but no WhatsApp integration exists in this repository. LEAD_SUCCESS_MESSAGE only promises that a human will follow up on WhatsApp or email.

When selling this: the webhook can target whatever the client already uses, and WhatsApp relay is scoped work. Do not describe it as pre-built.

Known gaps

  • No retry, queue, or dead-letter path. A transient webhook failure loses the lead.
  • No persistence. No database; the Sheet is the only record. ../architecture/README.md confirms no ORM or DB.
  • No rate limiting or spam protection on a public, unauthenticated, CORS-* endpoint. A single script can flood the Sheet.
  • No vertical field. project_type has three engineering values and no notion of the four target verticals in ../strategy/target-markets.md; GEO retainer leads from a clinic and from a car-rental company both arrive as geo_ai_integration.
  • No notification on arrival. Nothing alerts the owner that a lead landed, yet the response promises a reply within an hour.

Security: REST /api/leads/submit and /api/contact now have in-memory rate limits (see ADR ../decisions/0008-multi-tenant-lead-routing-and-rate-limiting.md), but MCP remains unrate-limited and several production blockers remain. Full audit: ../operations/production-security-audit.md.

Runbooks

Lead-delivery setup and the "leads are not arriving" checklist live in ../operations/README.md.