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
- Normalize
callStatus-- Convert to lowercase. GHL sends mixed case values. - Deduplicate -- Check if
external_call_id(fromcallId) already exists incalls. - Match contact -- Find the contact in
contactsby phone number or GHL contact ID. - Infer disposition -- Use regex patterns on notes/transcript to determine disposition.
- 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
}
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.
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
- Parse and normalize -- Same normalization as
ghl-call-webhook - Deduplicate -- Check
external_call_idinsales_calls - Match prospect -- Find prospect by phone number or GHL contact ID
- Infer disposition -- Regex inference
- Insert or update --
sales_callsrecord - Update prospect -- Set
last_contacted_atonsales_prospects - Trigger AI -- If
transcript.length > 100, auto-invokesummarize-sales-call
Sequence Diagram
Side Effects
- Inserts/updates
sales_calls - Updates
sales_prospects.last_contacted_at - Triggers
summarize-sales-callfor transcripts over 100 characters - AI function then updates
sales_calls.ai_summaryandsales_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_prospectswithghl_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_batchesrecord - Inserts records into
ghl_import_stagingwithstatus = 'pending' - Attempts to match staged records to existing
sales_prospectsorcontacts - Does NOT automatically import -- records stay in staging for admin review