Call Ingestion Flow
How calls from GoHighLevel are ingested, deduplicated, processed, and enriched with AI summaries.
End-to-End Flow
Webhook Reception
GHL fires two webhook hits per call -- one from the Call Completed workflow and one from the Disposition workflow. Both carry the same external_call_id.
Webhook endpoints:
ghl-call-webhook-- For client-facing calls (writes tocallstable)ghl-sales-webhook-- For internal sales calls (writes tosales_callstable)
Configuration: Each endpoint must have verify_jwt = false in supabase/config.toml since GHL cannot send JWT tokens.
Deduplication
Every incoming webhook is checked against existing records using external_call_id:
SELECT id FROM sales_calls WHERE external_call_id = '<ghl-call-id>' LIMIT 1;
If a match is found, the webhook is acknowledged (200 response) but no new record is created. This prevents the duplicate GHL webhook from creating two call records.
Call Status Normalization
GHL sends callStatus in mixed case. The webhook handler normalizes to lowercase before processing:
| GHL Raw Value | Count (Historical) | Normalized |
|---|---|---|
Answered | 715 | answered |
No answer | 72 | no answer |
Busy | 25 | busy |
Failed | 24 | failed |
completed | 23 | completed |
Voicemail | 4 | voicemail |
Missed | 1 | missed |
Ringing | 1 | ringing |
Critical: answered and completed both mean someone picked up the phone. Treat them identically when inferring disposition.
Disposition Inference
Before AI summarization runs, the webhook performs regex-based disposition inference from the transcript and call notes. This provides an immediate disposition even before the AI processes the call.
9 Canonical Dispositions
All dispositions use Title Case and are stored as TEXT (not enum) in the database:
| # | Disposition | When to Use |
|---|---|---|
| 1 | No Answer | Call was not picked up |
| 2 | Voicemail | Went to voicemail |
| 3 | Gatekeeper | Receptionist or assistant answered, did not connect to target |
| 4 | Incorrect Number | Wrong number or disconnected |
| 5 | Not Interested | Target answered but explicitly declined |
| 6 | Callback Requested | Target asked to be called back at a specific time |
| 7 | Future Potential | Positive engagement but no immediate meeting (default for good calls) |
| 8 | Meeting Booked | A meeting was scheduled |
| 9 | Do Not Contact | Target explicitly requested no further contact |
The calls.disposition column is TEXT, not an enum. It was migrated from the call_disposition enum type in migration 20260210200001_convert_disposition_to_text.sql. Always use the exact Title Case strings above.
AI Summarization
When a call has a transcript longer than 100 characters, the summarize-sales-call edge function is auto-triggered.
What it does:
- Reads the call transcript from the database
- Sends it to Claude AI (Anthropic API) with a structured prompt
- Claude returns a JSON response with:
disposition-- One of the 9 canonical dispositionsengagement_level-- Assessment of the prospect's interestnext_follow_up_at-- Suggested follow-up datesummary-- Brief narrative summary of the call
- The call record is updated with the AI-generated fields
Environment requirement: ANTHROPIC_API_KEY must be set as a Supabase secret:
supabase secrets set ANTHROPIC_API_KEY=sk-ant-...
Manual trigger:
curl -X POST \
https://<supabase-url>/functions/v1/summarize-sales-call \
-H "Authorization: Bearer <service-role-key>" \
-H "Content-Type: application/json" \
-d '{"call_id": "<call-uuid>"}'
Database Tables
calls (Client-Facing)
Used for calls made on behalf of clients through campaigns.
sales_calls (Internal Sales)
Used for 1HR's own outbound sales calls. Key columns:
prospect_id-- FK tosales_prospectscalled_by-- FK toauth.usersexternal_call_id-- GHL call identifier (used for dedup)disposition-- TEXT, one of the 9 canonical dispositionstranscript-- Raw call transcriptai_summary-- Claude-generated summaryengagement_level-- AI-assessed engagementnext_follow_up_at-- AI-suggested follow-up date
Debugging
If calls are not appearing:
- Check
webhook_debug_log-- Raw payloads are logged here for debugging - Verify webhook URL -- Must point to the correct Supabase edge function URL
- Check GHL workflow -- Ensure the Call Completed workflow is active and triggers on the right events
- Check dedup -- If the call already exists (by
external_call_id), it will be silently skipped
See Debugging GHL Issues for more detail.