Retell AI events
- Endpoint
-
POST /api/webhooks/event-logger - Dispatcher
-
WebhookEventDispatcherinlib/events/event-handler.ts
| Event type | Handler | Status |
|---|---|---|
|
|
Registered |
|
|
Not registered — the line is commented out |
|
Two implementations of
The two differ in behaviour — notably the decomposed |
Because call_started is unregistered, the dialing state and the Call row are created by
initiateCall, and the attempt counter is incremented when the call ends.
call_analyzed handler
Correlation comes from Retell metadata: call.metadata.campaignId and
call.metadata.campaignTargetId. Without both, the handler logs and returns — an inbound or
manually-created Retell call has no campaign context.
1. Technical metrics
duration_ms, disconnection_reason, channel_type, call_cost.combined_cost,
call_cost.total_duration_unit_price (stored as latencyMs), start_timestamp, end_timestamp.
2. AI analysis
From call.call_analysis: in_voicemail, call_screening_detected, ai_suspicion_detected,
user_sentiment, detected_language, call_summary, call_successful.
3. Business extraction
From call_analysis.custom_analysis_data: call_disposition (or disposition), next_action,
termination_reason, is_amazon_seller, meets_sales_threshold, is_decision_maker,
callback_requested, proposed_callback_time, monthly_revenue, and the contact name.
The field vocabulary is defined by DEFAULT_POST_CALL_ANALYSIS in
lib/retellai/post-call-analysis.ts — the shared contract between the caller agent, Retell and this
handler.
4. State decision
Priority order; the first match wins.
| Priority | Rule |
|---|---|
1 |
|
2 |
disposition |
3 |
Recoverable — voicemail, screening, technical failure ( |
4 |
Default → |
The full cooling-off window is used for every recoverable outcome so the broadcast loop cannot race the Inngest double tap. If Inngest fails entirely, the cooling-off window is the fallback retry.
5. Database updates
CampaignTarget-
state,attempts(increment),updatedAt,lastAttemptAt,nextEarliestAttemptAt,isAmazonSeller,amazonSeller(raw AI answer, defaulting to"Undetermined"),meetsMonthlyThreshold,stopCalling,disposition,lastCallRecordingUrl,nextAction,lastError,validConversation(durationSec >= 10),leaseExpiresAt = null. Call-
updated by
(providerCallId, targetId)—status = ended, durations, cost, latency,channelType,disconnectionReason,retellDispositionanddisposition(only when a disposition exists), the detection flags,callSuccessful,isDecisionMaker,nextAction,terminationReason,contactName,proposedCallbackTime,recordingUrlandtranscript(only when present),userSentiment(defaulting to"Unknown"),summary,monthlyRevenue,meetsMonthlyThreshold,startTimestamp,endTimestamp.
Conditional spreads are used so a webhook that omits a field does not overwrite a good value with
null.
6. CRM hand-off
Eligible when durationSec >= 10, not voicemail, not screened, the disposition is in
HUBSPOT_WHITE_LISTED_DISPOSITIONS, and the target is not a test target.
When eligible, the handler publishes Crm.DialerCallEnded to the CRM SQS queue with the workspace id
as messageGroupId:
{
"event_type": "Crm.DialerCallEnded",
"source": "dialer",
"subscription_id": "uuid",
"call_id": "uuid",
"provider_call_id": "retell_call_id",
"campaign_id": "uuid",
"campaign_name": "Campaign Name",
"target_id": "uuid",
"disposition": "Qualified_Lead",
"duration_sec": 120
}
The direct HubSpot API sync (hubSync.syncCall) remains in the source, commented out. CRM writes are
now the downstream consumer’s responsibility. Using the workspace id as the message group preserves
per-tenant ordering on a FIFO queue.
7. Emit call-target/call.ended
Fire-and-forget, with the failure logged:
{
callId, providerCallId, campaignId, campaignName, subscriptionId,
targetId, targetPhoneNumber, targetPayload,
disposition, disconnectionReason, durationSec,
inVoicemail, callScreeningDetected, newState,
}
This decouples post-call work from the webhook. callEndedAnalysis consumes it — see
callEndedAnalysis.
call_started handler (dormant)
If re-registered, it would validate campaignId and campaignTargetId, confirm the direction is
outbound, set the target to dialing, increment attempts, and set the Call to dialing with its
start timestamp. Re-registering it without also removing the increment in call-ended.ts would
double-count attempts.