Skip to main content

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 to calls table)
  • ghl-sales-webhook -- For internal sales calls (writes to sales_calls table)

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 ValueCount (Historical)Normalized
Answered715answered
No answer72no answer
Busy25busy
Failed24failed
completed23completed
Voicemail4voicemail
Missed1missed
Ringing1ringing

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:

#DispositionWhen to Use
1No AnswerCall was not picked up
2VoicemailWent to voicemail
3GatekeeperReceptionist or assistant answered, did not connect to target
4Incorrect NumberWrong number or disconnected
5Not InterestedTarget answered but explicitly declined
6Callback RequestedTarget asked to be called back at a specific time
7Future PotentialPositive engagement but no immediate meeting (default for good calls)
8Meeting BookedA meeting was scheduled
9Do Not ContactTarget explicitly requested no further contact
caution

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:

  1. Reads the call transcript from the database
  2. Sends it to Claude AI (Anthropic API) with a structured prompt
  3. Claude returns a JSON response with:
    • disposition -- One of the 9 canonical dispositions
    • engagement_level -- Assessment of the prospect's interest
    • next_follow_up_at -- Suggested follow-up date
    • summary -- Brief narrative summary of the call
  4. 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 to sales_prospects
  • called_by -- FK to auth.users
  • external_call_id -- GHL call identifier (used for dedup)
  • disposition -- TEXT, one of the 9 canonical dispositions
  • transcript -- Raw call transcript
  • ai_summary -- Claude-generated summary
  • engagement_level -- AI-assessed engagement
  • next_follow_up_at -- AI-suggested follow-up date

Debugging

If calls are not appearing:

  1. Check webhook_debug_log -- Raw payloads are logged here for debugging
  2. Verify webhook URL -- Must point to the correct Supabase edge function URL
  3. Check GHL workflow -- Ensure the Call Completed workflow is active and triggers on the right events
  4. Check dedup -- If the call already exists (by external_call_id), it will be silently skipped

See Debugging GHL Issues for more detail.