Skip to main content

GHL Functions

Four edge functions handle the GoHighLevel integration: two webhook receivers, one push sync, and one pull import.

ghl-call-webhook

Receives incoming call webhooks from GHL for client campaign calls. Logs calls to the calls table.

Endpoint

POST /ghl-call-webhook

GHL Webhook Payload

{
"callId": "abc123",
"callStatus": "Answered",
"direction": "outbound",
"from": "+15551234567",
"to": "+15559876543",
"duration": 120,
"contactId": "ghl_contact_id",
"locationId": "ghl_location_id",
"callStartTime": "2026-02-12T10:00:00Z",
"notes": "Left voicemail about market report",
"transcription": "Full call transcript text..."
}

Processing Steps

  1. Normalize callStatus -- Convert to lowercase. GHL sends mixed case values.
  2. Deduplicate -- Check if external_call_id (from callId) already exists in calls.
  3. Match contact -- Find the contact in contacts by phone number or GHL contact ID.
  4. Infer disposition -- Use regex patterns on notes/transcript to determine disposition.
  5. Insert or update -- First webhook inserts, second webhook updates existing record.

Disposition Inference

The function uses regex-based inference to map GHL call data to the 9 canonical dispositions:

function inferDisposition(callStatus: string, notes: string, transcript: string): string {
const status = callStatus.toLowerCase();
const text = (notes + ' ' + transcript).toLowerCase();

if (status === 'no answer' || status === 'missed') return 'No Answer';
if (status === 'voicemail') return 'Voicemail';
if (status === 'busy') return 'No Answer';
if (status === 'failed') return 'Incorrect Number';

// For answered/completed calls, check transcript/notes
if (text.match(/gatekeeper|assistant|receptionist/)) return 'Gatekeeper';
if (text.match(/wrong number|disconnected|not in service/)) return 'Incorrect Number';
if (text.match(/not interested|no thanks|don't call/)) return 'Not Interested';
if (text.match(/call back|try again|call later/)) return 'Callback Requested';
if (text.match(/meeting|schedule|book/)) return 'Meeting Booked';
if (text.match(/do not contact|remove|opt out/)) return 'Do Not Contact';

return 'Future Potential'; // Default for answered calls
}
Mixed Case Normalization

GHL callStatus values are mixed case: Answered (715 occurrences), No answer (72), Busy (25), Failed (24), completed (23). Always normalize to lowercase. Both "answered" and "completed" mean the call connected.

Dual Webhook Deduplication

GHL fires two webhooks per call (Call Completed + Disposition workflows). The function uses external_call_id to deduplicate:

  • First hit: INSERT into calls
  • Second hit: UPDATE the existing record (may add transcript or refined notes)

ghl-sales-webhook

Receives incoming call webhooks from GHL for internal sales calls. Logs calls to sales_calls and optionally triggers AI summarization.

Endpoint

POST /ghl-sales-webhook

Processing Steps

  1. Parse and normalize -- Same normalization as ghl-call-webhook
  2. Deduplicate -- Check external_call_id in sales_calls
  3. Match prospect -- Find prospect by phone number or GHL contact ID
  4. Infer disposition -- Regex inference
  5. Insert or update -- sales_calls record
  6. Update prospect -- Set last_contacted_at on sales_prospects
  7. Trigger AI -- If transcript.length > 100, auto-invoke summarize-sales-call

Sequence Diagram

Side Effects

  • Inserts/updates sales_calls
  • Updates sales_prospects.last_contacted_at
  • Triggers summarize-sales-call for transcripts over 100 characters
  • AI function then updates sales_calls.ai_summary and sales_prospects.engagement_level

sync-prospects-to-ghl

Pushes unsynced sales_prospects to GHL as contacts. Looks for prospects where ghl_synced_at IS NULL and creates them in GHL.

Endpoint

POST /sync-prospects-to-ghl

Input

interface SyncRequest {
locationId: string; // GHL location to sync to
limit?: number; // Max prospects to sync (default: 50)
}

Flow

Custom Field Mapping

The function reads ghl_integrations.custom_field_mapping to map Client Portal fields to GHL custom field IDs:

const mapping = integration.custom_field_mapping;
const customFields = {};

if (mapping.company_name) {
customFields[mapping.company_name] = prospect.company_name;
}
if (mapping.contact_title) {
customFields[mapping.contact_title] = prospect.contact_title;
}
// ... etc

GHL Contact API

const response = await fetch(
`https://services.leadconnectorhq.com/contacts/`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${integration.api_token}`,
'Version': '2021-07-28',
'Content-Type': 'application/json',
},
body: JSON.stringify({
firstName: prospect.contact_name?.split(' ')[0],
lastName: prospect.contact_name?.split(' ').slice(1).join(' '),
email: prospect.contact_email,
phone: prospect.contact_phone,
locationId: integration.location_id,
customFields: customFields,
}),
}
);

Side Effects

  • Creates contacts in GHL
  • Updates sales_prospects with ghl_contact_id, ghl_synced_at, ghl_location_id
  • Skips prospects where do_not_contact = true

pull-from-ghl

Pulls contacts and call data from GHL for batch import. Stages records in ghl_import_staging for review before processing.

Endpoint

POST /pull-from-ghl

Input

interface PullRequest {
locationId: string; // GHL location to pull from
startDate?: string; // ISO date filter
endDate?: string; // ISO date filter
}

Flow

Side Effects

  • Creates a ghl_import_batches record
  • Inserts records into ghl_import_staging with status = 'pending'
  • Attempts to match staged records to existing sales_prospects or contacts
  • Does NOT automatically import -- records stay in staging for admin review