Skip to content

Instantly share code, notes, and snippets.

@hmseeb
Last active March 13, 2026 19:04
Show Gist options
  • Select an option

  • Save hmseeb/d4612c19dc4f589f3716b44009e25973 to your computer and use it in GitHub Desktop.

Select an option

Save hmseeb/d4612c19dc4f589f3716b44009e25973 to your computer and use it in GitHub Desktop.

GHL Integration — PPL Lead Management

This system receives leads from GHL, scores them against all eligible orders using a 0-100 point algorithm, and delivers them via webhook to the winning broker's CRM.


Incoming Lead

POST https://ppl-leadr-mgmt.vercel.app/api/leads/incoming
Content-Type: application/json

Request Payload

{
  "ghl_contact_id": "ghl_abc123",        // REQUIRED — unique GHL contact ID
  "vertical": "MCA",                      // optional — e.g. "MCA", "Equipment", "SBA"
  "first_name": "John",                   // optional
  "last_name": "Smith",                   // optional
  "email": "john@example.com",            // optional — valid email
  "phone": "+17025551234",                // optional — min 7 chars
  "business_name": "Smith Industries",    // optional
  "credit_score": 720,                    // optional — integer 300–850 (can be string, auto-coerced)
  "funding_amount": 50000,               // RECOMMENDED — positive number (rejected if missing/invalid)
  "funding_purpose": "expansion",         // optional
  "state": "NV",                          // optional
  "ai_call_notes": "Interested in MCA",   // optional
  "ai_call_status": "completed"           // optional
}

Field Reference

Field Type Required Notes
ghl_contact_id string ✅ Yes Must be unique per lead. Used for idempotency — duplicate webhooks with same ID return the existing lead.
vertical string Optional Lead's funding vertical (e.g. "MCA", "Equipment", "SBA"). Used for order matching. If omitted, only orders accepting "All" verticals will match.
first_name string Optional
last_name string Optional
email string Optional Must be valid email format if provided. Also used for email+phone dedup.
phone string Optional Min 7 characters. Also used for email+phone dedup.
business_name string Optional
credit_score number Optional Integer 300–850. Can be sent as string — auto-coerced. Leads with credit < 600 are rejected.
funding_amount number Recommended Positive number. Can be sent as string — auto-coerced. Leads with missing or invalid funding_amount are rejected.
funding_purpose string Optional
state string Optional US state code or full name.
ai_call_notes string Optional Notes from AI voice call.
ai_call_status string Optional Status of the AI call (e.g. "completed", "no_answer").

Pre-flight Validation

Before scoring, every lead passes through pre-flight checks. If any check fails, the lead is stored with status rejected and never enters the scoring engine.

Check Condition Rejection Reason
Credit floor credit_score provided and < 600 credit_too_low
Loan amount funding_amount missing, null, or <= 0 invalid_loan_amount
Active orders Zero active orders in the system no_active_orders

Deduplication

Two layers of dedup run before any validation:

  1. ghl_contact_id — exact match on contact ID (existing behavior)
  2. email + phone — if both are provided and match an existing lead (case-insensitive email), returns the existing lead

Responses

Success — lead created & assigned

{
  "lead_id": "a1b2c3d4-...",
  "assignment": {
    "status": "assigned",
    "broker_id": "f7e8d9c0-...",
    "order_id": "b2c3d4e5-..."
  }
}

Success — lead created, no matching order

{
  "lead_id": "a1b2c3d4-...",
  "assignment": {
    "status": "unassigned",
    "reason": "no matching order for vertical"
  }
}

Rejected — pre-flight validation failed

{
  "lead_id": "a1b2c3d4-...",
  "status": "rejected",
  "reason": "credit_too_low"
}

Duplicate (same ghl_contact_id or email+phone already exists)

{
  "lead_id": "a1b2c3d4-...",
  "status": "duplicate",
  "message": "lead already exists"
}

Validation Error (400)

{
  "error": "invalid_payload",
  "details": { ... }
}

Example cURL

curl -X POST https://ppl-leadr-mgmt.vercel.app/api/leads/incoming \
  -H "Content-Type: application/json" \
  -d '{
    "ghl_contact_id": "ghl_test_001",
    "vertical": "MCA",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "credit_score": 680,
    "funding_amount": 35000,
    "state": "TX"
  }'

Update Lead

PATCH https://ppl-leadr-mgmt.vercel.app/api/leads/update
Content-Type: application/json

Updates an existing lead by ghl_contact_id. Use this to push AI call results or updated lead info back from GHL after the lead has been created.

Request Payload

{
  "ghl_contact_id": "ghl_abc123",        // REQUIRED — identifies which lead to update
  "ai_call_notes": "Qualified, wants $50k MCA",
  "ai_call_status": "completed",
  "credit_score": 720,
  "funding_amount": 50000
}

Updatable Fields

Field Type Notes
ghl_contact_id string Required — used to find the lead.
first_name string
last_name string
email string Valid email or empty string.
phone string
business_name string
vertical string
credit_score number Integer 300–850. Auto-coerced from string.
funding_amount number Positive number. Auto-coerced from string.
funding_purpose string
state string
ai_call_notes string
ai_call_status string

Protected fields (cannot be updated via this endpoint): assigned_broker_id, assigned_order_id, assigned_at, status. These are managed by the assignment engine only.

Responses

Success

{
  "lead_id": "a1b2c3d4-...",
  "updated_fields": ["ai_call_notes", "ai_call_status", "credit_score"]
}

Lead not found (404)

{
  "error": "lead_not_found",
  "ghl_contact_id": "ghl_abc123"
}

Validation Error (400)

{
  "error": "validation_error",
  "details": { ... }
}

Example cURL

curl -X PATCH https://ppl-leadr-mgmt.vercel.app/api/leads/update \
  -H "Content-Type: application/json" \
  -d '{
    "ghl_contact_id": "ghl_test_001",
    "ai_call_status": "completed",
    "ai_call_notes": "Qualified. Wants $50k MCA for expansion. Good credit."
  }'

Assignment Logic (Scoring Engine v2.0)

When a lead passes pre-flight validation, the scoring engine runs automatically:

Step 1: Hard Filters

Each active order is checked against hard filters. If any filter fails, the order is disqualified:

Filter Rule
Order status Must be active
Broker status Broker's assignment_status must be active
Capacity leads_remaining > 0 (unless bonus mode is on)
Vertical match Lead's vertical must be in order's verticals array (or order accepts "All")
Credit tier 680+ Orders with credit_score_min >= 680 never receive leads with credit < 680
Credit tier 600+ Orders with credit_score_min >= 600 reject leads below that minimum
Loan range If lead has funding_amount, it must fall within order's loan_min / loan_max range

Step 2: Scoring (0-100 points)

Each eligible order is scored:

Component Max Points Formula
Credit Fit 40 (lead.credit - order.credit_min) / (850 - order.credit_min) * 40
Capacity 30 (1 - fill_rate) * 30 where fill_rate = delivered / total
Tier Match 20 20pts exact tier match, 10pts fallback (680+ lead to 600+ order)
Loan Fit 10 10pts if funding_amount within order's loan range
Priority Bonus +8 Orders with priority = "high"
Urgency Bonus +5/-5 +5 if fill > 80%, -5 if fill < 10%

Tiebreaker: lowest fill rate wins (most capacity remaining).

Step 3: Assignment

The highest-scoring eligible order wins. The lead is assigned atomically using advisory locks to prevent race conditions.

Routing Audit Trail

Every scoring decision is logged. For each lead, a routing_logs entry is created for every order considered (not just the winner), including:

  • Whether it was eligible or disqualified (and why)
  • Full score breakdown (all 6 components)
  • Whether it was the selected winner

Admins can view this on the lead detail page under "Routing Audit".

If no eligible order exists, the lead goes to the unassigned queue. An admin can manually assign it from the dashboard.

Assignment failure does not block the API response — the lead is always stored, and the assignment result is returned in the response body.


Order Types

Type Behavior
one_time Fixed lead allocation. When leads_remaining hits 0, order completes.
monthly Auto-resets on the 1st of each month. leads_delivered resets to 0, leads_remaining restores to total_leads, completed orders reactivated to active. Each reset is logged in the activity log.

Webhook Delivery

When a lead is assigned to a broker who has a crm_webhook_url configured:

  • A webhook_delivery record is created with status pending
  • The payload contains the full lead object as JSON
  • Delivery status is tracked: pending -> sent or failed
  • Retry count and error messages are logged
  • Deliveries respect broker contact hours — out-of-window deliveries are queued and released when the window opens
  • Delivery history is visible on the lead's detail page in the dashboard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment