AI Voice Agents for Sales & Lead Conversion
Transform your sales process with AI voice agents that respond to leads in seconds, qualify prospects 24/7, and book meetings while you sleep. This guide covers everything from lead qualification to CRM integration.
Why Voice AI for Sales?
| Challenge | Traditional | With Voice AI |
|---|---|---|
| Lead response time | 5+ minutes | < 30 seconds |
| After-hours leads | Lost or delayed | Instant response |
| Lead qualification | Manual, inconsistent | Automated, standardized |
| Follow-up cadence | Often missed | 100% completion |
| Cost per qualified lead | High | 60-80% reduction |
Use Cases
1. Instant Lead Response
Respond to inbound leads within seconds of form submission.
{
"agent": {
"name": "Instant Lead Responder",
"language": "en-US",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.7,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"greetingMessage": "Hi {{leadName}}! This is Sarah from {{companyName}}. I saw you just requested information about {{interest}}. Perfect timing - do you have a couple minutes to chat?",
"allowInterruptions": true,
"vadSilenceDuration": 700
}
}
Trigger via Webhook (Zapier/Make):
curl -X POST https://api.edesy.in/v1/calls \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "instant_responder",
"to": "+1234567890",
"variables": {
"leadName": "John",
"companyName": "TechCorp",
"interest": "enterprise pricing",
"formSource": "pricing_page",
"leadId": "lead_abc123"
},
"webhook_url": "https://your-server.com/webhooks/call-complete"
}'
2. Lead Qualification (BANT/MEDDIC)
Qualify leads using structured frameworks.
{
"agent": {
"name": "Lead Qualifier - BANT",
"language": "en-US",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.6,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"greetingMessage": "Hi {{leadName}}! Thanks for your interest in {{companyName}}. I'd love to learn more about what you're looking for so I can point you in the right direction. Do you have a few minutes?",
"prompt": "...",
"allowInterruptions": true
}
}
3. Appointment Booking
Convert qualified leads into meetings.
{
"agent": {
"name": "Appointment Setter",
"language": "en-US",
"llmProvider": "gemini-2.5",
"sttProvider": "deepgram",
"ttsProvider": "cartesia",
"greetingMessage": "Hi {{leadName}}! Based on our conversation, I think a demo with our solutions team would be really valuable. I can book that for you right now - what day works best?",
"calendarIntegration": {
"provider": "calendly",
"eventType": "sales-demo",
"duration": 30
}
}
}
4. Follow-Up Sequences
Automated multi-touch follow-ups.
{
"followUpSequence": {
"name": "Demo No-Show Recovery",
"triggers": ["missed_demo", "no_response"],
"steps": [
{
"delay": "1h",
"action": "call",
"message": "Hi {{leadName}}, I noticed you couldn't make the demo today. No worries! Would you like to reschedule for later this week?"
},
{
"delay": "24h",
"action": "call",
"message": "Hi {{leadName}}, just following up on the demo. I know things get busy - want me to find a time that works better?"
},
{
"delay": "72h",
"action": "call",
"maxAttempts": 1,
"message": "Hi {{leadName}}, last call from me about the demo. If you're still interested, I can hold a spot for you this week. Otherwise, feel free to reach out whenever you're ready."
}
]
}
}
5. Lead Nurturing
Keep leads warm until they're ready to buy.
{
"agent": {
"name": "Nurture Agent",
"language": "en-US",
"llmProvider": "gemini-2.5",
"sttProvider": "deepgram",
"ttsProvider": "cartesia",
"greetingMessage": "Hi {{leadName}}! This is Alex from {{companyName}}. We chatted a few weeks ago about {{lastTopic}}. I wanted to share something I think you'll find valuable - do you have a quick minute?",
"prompt": "You are a nurture specialist. Your goal is to provide value and stay top-of-mind without being pushy.\n\nContext:\n- Last interaction: {{lastInteraction}}\n- Interest level: {{interestLevel}}\n- Pain points: {{painPoints}}\n\nApproach:\n1. Share relevant insight, case study, or news\n2. Ask about their current situation\n3. Gauge timing for next steps\n4. Update CRM with new info\n\nNever be pushy. Focus on being helpful."
}
}
System Prompt Example: Sales Qualification
You are a sales qualification specialist for {{companyName}}. Your goal is to understand the prospect's needs, qualify them using BANT criteria, and schedule a meeting if appropriate.
## About {{companyName}}
{{companyDescription}}
## Your Personality
- Friendly, professional, consultative
- Curious - you genuinely want to understand their situation
- Patient - don't rush through questions
- Helpful - focus on solving their problem, not just selling
## BANT Qualification Framework
### Budget
Questions to uncover:
- "Do you have a budget allocated for this initiative?"
- "What are you currently spending on [problem area]?"
- "Who handles budget approvals for tools like this?"
Signals:
- Specific budget mentioned = Strong (25 points)
- Budget being discussed = Medium (15 points)
- No budget/unknown = Weak (5 points)
### Authority
Questions to uncover:
- "What's your role in evaluating solutions like this?"
- "Who else would be involved in this decision?"
- "How does your team typically evaluate new tools?"
Signals:
- Decision maker = Strong (25 points)
- Influencer/evaluator = Medium (15 points)
- Researcher = Weak (5 points)
### Need
Questions to uncover:
- "What challenges are you facing with [problem area]?"
- "How is this impacting your business?"
- "What would solving this mean for your team?"
Signals:
- Urgent, specific need = Strong (25 points)
- General interest = Medium (15 points)
- Just exploring = Weak (5 points)
### Timeline
Questions to uncover:
- "When are you looking to have a solution in place?"
- "Is there a specific deadline driving this?"
- "What happens if you don't solve this soon?"
Signals:
- Within 30 days = Strong (25 points)
- Within 90 days = Medium (15 points)
- No timeline = Weak (5 points)
## Qualification Scoring
**Hot Lead (80-100 points):** Schedule demo immediately
**Warm Lead (50-79 points):** Send resources + schedule follow-up
**Cold Lead (0-49 points):** Add to nurture sequence
## Conversation Flow
1. Build Rapport (30 seconds)
- Thank them for their time
- Reference how they found you (if known)
2. Discovery (2-3 minutes)
- Ask about their current situation
- Understand their challenges
- Listen for pain points
3. Qualification (2-3 minutes)
- Weave BANT questions naturally
- Don't interrogate - converse
- Share relevant insights
4. Value Proposition (1 minute)
- Connect their needs to your solution
- Share a quick relevant case study
- Keep it brief - save details for demo
5. Close (1 minute)
- For qualified leads: "Based on what you've shared, I think a demo would be really valuable. Can I book you in for 15 minutes this week?"
- For unqualified: "Let me send you some resources that might help. Can I check back in a few weeks?"
## Handling Common Objections
### "I'm just researching"
"Totally makes sense! What prompted you to start looking into this now?"
→ Uncover timeline and urgency
### "We already have a solution"
"Got it! What made you curious about alternatives?"
→ Identify gaps in current solution
### "I need to talk to my team first"
"Of course! What information would be most helpful to share with them?"
→ Identify decision-making process
### "Can you just send me pricing?"
"Happy to! To give you accurate pricing, can I ask a couple quick questions about your use case?"
→ Continue qualification
### "I don't have time right now"
"I totally understand. What would be a better time for a quick 5-minute chat?"
→ Schedule callback
## Important Rules
DO:
- Listen more than you talk (aim for 60/40 them/you)
- Ask follow-up questions based on their answers
- Be genuinely helpful
- Capture all relevant information
- End every call with a clear next step
DON'T:
- Rapid-fire questions like an interrogation
- Push too hard if they're not ready
- Make up information about the product
- Promise things you can't deliver
- Talk for more than 30 seconds at a time
Tools for Sales Agents
CRM Integration - Save Lead
{
"type": "function",
"function": {
"name": "save_lead_to_crm",
"description": "Save or update lead information in the CRM system",
"parameters": {
"type": "object",
"properties": {
"lead_id": {
"type": "string",
"description": "Existing lead ID if updating"
},
"first_name": {
"type": "string",
"description": "Lead's first name"
},
"last_name": {
"type": "string",
"description": "Lead's last name"
},
"email": {
"type": "string",
"description": "Email address"
},
"phone": {
"type": "string",
"description": "Phone number"
},
"company": {
"type": "string",
"description": "Company name"
},
"job_title": {
"type": "string",
"description": "Job title/role"
},
"company_size": {
"type": "string",
"enum": ["1-10", "11-50", "51-200", "201-1000", "1000+"],
"description": "Number of employees"
},
"industry": {
"type": "string",
"description": "Industry/vertical"
},
"pain_points": {
"type": "array",
"items": { "type": "string" },
"description": "Key pain points mentioned"
},
"current_solution": {
"type": "string",
"description": "Current solution they're using"
},
"budget_range": {
"type": "string",
"description": "Budget range if discussed"
},
"timeline": {
"type": "string",
"description": "Decision timeline"
},
"lead_score": {
"type": "integer",
"description": "Qualification score 0-100"
},
"lead_status": {
"type": "string",
"enum": ["new", "qualified", "nurture", "disqualified"],
"description": "Lead status"
},
"notes": {
"type": "string",
"description": "Conversation notes and context"
}
},
"required": ["first_name", "phone", "lead_score", "lead_status"]
}
}
}
Calendar Booking - Check Availability
{
"type": "function",
"function": {
"name": "check_calendar_availability",
"description": "Check available meeting slots for the sales team",
"parameters": {
"type": "object",
"properties": {
"meeting_type": {
"type": "string",
"enum": ["discovery", "demo", "technical_review", "closing"],
"description": "Type of meeting to schedule"
},
"duration_minutes": {
"type": "integer",
"description": "Meeting duration in minutes"
},
"date_range_start": {
"type": "string",
"description": "Start of date range (YYYY-MM-DD)"
},
"date_range_end": {
"type": "string",
"description": "End of date range (YYYY-MM-DD)"
},
"timezone": {
"type": "string",
"description": "Prospect's timezone (e.g., 'America/New_York')"
},
"preferred_times": {
"type": "array",
"items": { "type": "string" },
"description": "Preferred time slots (morning, afternoon, evening)"
}
},
"required": ["meeting_type", "duration_minutes"]
}
}
}
Calendar Booking - Book Meeting
{
"type": "function",
"function": {
"name": "book_meeting",
"description": "Book a meeting with the sales team",
"parameters": {
"type": "object",
"properties": {
"lead_id": {
"type": "string",
"description": "CRM lead ID"
},
"meeting_type": {
"type": "string",
"enum": ["discovery", "demo", "technical_review", "closing"]
},
"datetime": {
"type": "string",
"description": "Meeting datetime (ISO 8601)"
},
"duration_minutes": {
"type": "integer"
},
"attendee_email": {
"type": "string",
"description": "Prospect's email for calendar invite"
},
"attendee_name": {
"type": "string"
},
"additional_attendees": {
"type": "array",
"items": { "type": "string" },
"description": "Additional attendee emails"
},
"meeting_notes": {
"type": "string",
"description": "Context for the sales rep"
},
"assigned_rep": {
"type": "string",
"description": "Specific sales rep to assign (optional)"
}
},
"required": ["lead_id", "meeting_type", "datetime", "attendee_email", "attendee_name"]
}
}
}
Lead Scoring - Calculate Score
{
"type": "function",
"function": {
"name": "calculate_lead_score",
"description": "Calculate lead score based on qualification criteria",
"parameters": {
"type": "object",
"properties": {
"budget_signal": {
"type": "string",
"enum": ["strong", "medium", "weak", "unknown"],
"description": "Budget indication strength"
},
"authority_level": {
"type": "string",
"enum": ["decision_maker", "influencer", "evaluator", "researcher"],
"description": "Decision-making authority"
},
"need_urgency": {
"type": "string",
"enum": ["critical", "important", "nice_to_have", "exploring"],
"description": "How urgent is their need"
},
"timeline": {
"type": "string",
"enum": ["immediate", "30_days", "90_days", "6_months", "unknown"],
"description": "Purchase timeline"
},
"engagement_level": {
"type": "string",
"enum": ["highly_engaged", "engaged", "passive", "disengaged"],
"description": "Conversation engagement level"
},
"fit_score": {
"type": "string",
"enum": ["excellent", "good", "moderate", "poor"],
"description": "Product/market fit"
}
},
"required": ["budget_signal", "authority_level", "need_urgency", "timeline"]
}
}
}
Transfer to Sales Rep
{
"type": "function",
"function": {
"name": "transfer_to_sales",
"description": "Transfer hot lead to available sales rep immediately",
"parameters": {
"type": "object",
"properties": {
"lead_id": {
"type": "string"
},
"urgency": {
"type": "string",
"enum": ["hot", "warm"],
"description": "Lead temperature"
},
"context": {
"type": "string",
"description": "Brief context for the rep"
},
"preferred_rep": {
"type": "string",
"description": "Specific rep if requested by lead"
}
},
"required": ["lead_id", "urgency", "context"]
}
}
}
Tool Handlers (Go Implementation)
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// Lead represents a sales lead
type Lead struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Phone string `json:"phone"`
Company string `json:"company"`
JobTitle string `json:"job_title"`
CompanySize string `json:"company_size"`
Industry string `json:"industry"`
PainPoints []string `json:"pain_points"`
CurrentSolution string `json:"current_solution"`
BudgetRange string `json:"budget_range"`
Timeline string `json:"timeline"`
LeadScore int `json:"lead_score"`
LeadStatus string `json:"lead_status"`
Notes string `json:"notes"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SaveLeadToCRM saves lead to CRM (HubSpot/Salesforce)
func HandleSaveLeadToCRM(ctx context.Context, params map[string]any) (any, error) {
lead := Lead{
ID: generateLeadID(),
FirstName: getString(params, "first_name"),
LastName: getString(params, "last_name"),
Email: getString(params, "email"),
Phone: getString(params, "phone"),
Company: getString(params, "company"),
JobTitle: getString(params, "job_title"),
LeadScore: getInt(params, "lead_score"),
LeadStatus: getString(params, "lead_status"),
Notes: getString(params, "notes"),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save to CRM
err := crmClient.SaveLead(ctx, lead)
if err != nil {
return nil, err
}
// If hot lead, notify sales team
if lead.LeadScore >= 80 {
go notifySalesTeam(lead)
}
return map[string]any{
"success": true,
"lead_id": lead.ID,
"message": fmt.Sprintf("Lead saved successfully. Score: %d/100", lead.LeadScore),
}, nil
}
// CheckCalendarAvailability checks available slots
func HandleCheckAvailability(ctx context.Context, params map[string]any) (any, error) {
meetingType := getString(params, "meeting_type")
duration := getInt(params, "duration_minutes")
timezone := getString(params, "timezone")
if timezone == "" {
timezone = "America/New_York"
}
// Get available slots from calendar
slots, err := calendarClient.GetAvailableSlots(ctx, CalendarQuery{
MeetingType: meetingType,
Duration: duration,
Timezone: timezone,
DaysAhead: 7,
MaxSlots: 5,
})
if err != nil {
return nil, err
}
if len(slots) == 0 {
return map[string]any{
"available": false,
"message": "No available slots in the next 7 days. Would you like me to have someone reach out to schedule?",
}, nil
}
// Format slots for speech
formattedSlots := make([]string, len(slots))
for i, slot := range slots {
formattedSlots[i] = formatSlotForSpeech(slot, timezone)
}
return map[string]any{
"available": true,
"slots": formattedSlots,
"message": fmt.Sprintf("I have %d times available: %s. Which works best for you?", len(slots), joinSlotsForSpeech(formattedSlots)),
}, nil
}
// BookMeeting books a meeting
func HandleBookMeeting(ctx context.Context, params map[string]any) (any, error) {
leadID := getString(params, "lead_id")
meetingType := getString(params, "meeting_type")
datetime := getString(params, "datetime")
email := getString(params, "attendee_email")
name := getString(params, "attendee_name")
notes := getString(params, "meeting_notes")
// Parse datetime
meetingTime, err := time.Parse(time.RFC3339, datetime)
if err != nil {
return nil, fmt.Errorf("invalid datetime format")
}
// Book the meeting
meeting, err := calendarClient.BookMeeting(ctx, Meeting{
LeadID: leadID,
Type: meetingType,
DateTime: meetingTime,
Duration: 30 * time.Minute,
Attendees: []string{email},
AttendeeName: name,
Notes: notes,
})
if err != nil {
return nil, err
}
// Update lead status in CRM
go crmClient.UpdateLeadStatus(ctx, leadID, "meeting_booked")
// Send calendar invite
go calendarClient.SendInvite(ctx, meeting, email)
return map[string]any{
"success": true,
"confirmation_id": meeting.ID,
"message": fmt.Sprintf("Perfect! I've booked you for %s. You'll receive a calendar invite at %s shortly.", formatMeetingTimeForSpeech(meetingTime), email),
}, nil
}
// CalculateLeadScore calculates BANT score
func HandleCalculateLeadScore(ctx context.Context, params map[string]any) (any, error) {
var score int
// Budget scoring
switch getString(params, "budget_signal") {
case "strong":
score += 25
case "medium":
score += 15
case "weak":
score += 5
}
// Authority scoring
switch getString(params, "authority_level") {
case "decision_maker":
score += 25
case "influencer":
score += 20
case "evaluator":
score += 10
case "researcher":
score += 5
}
// Need scoring
switch getString(params, "need_urgency") {
case "critical":
score += 25
case "important":
score += 20
case "nice_to_have":
score += 10
case "exploring":
score += 5
}
// Timeline scoring
switch getString(params, "timeline") {
case "immediate":
score += 25
case "30_days":
score += 20
case "90_days":
score += 15
case "6_months":
score += 5
}
// Determine lead status
var status string
var recommendation string
switch {
case score >= 80:
status = "hot"
recommendation = "Schedule a demo immediately. This is a high-priority lead."
case score >= 50:
status = "warm"
recommendation = "Send relevant resources and schedule a follow-up."
default:
status = "cold"
recommendation = "Add to nurture sequence. Not ready to buy yet."
}
return map[string]any{
"score": score,
"status": status,
"recommendation": recommendation,
}, nil
}
// TransferToSales transfers call to available sales rep
func HandleTransferToSales(ctx context.Context, params map[string]any) (any, error) {
leadID := getString(params, "lead_id")
urgency := getString(params, "urgency")
context := getString(params, "context")
// Find available sales rep
rep, err := salesTeamClient.FindAvailableRep(ctx, urgency)
if err != nil || rep == nil {
// No rep available - schedule callback
return map[string]any{
"transferred": false,
"message": "All our sales team members are currently with other customers. Can I schedule a callback for you within the next 30 minutes?",
"fallback": "schedule_callback",
}, nil
}
// Log the handoff
go crmClient.LogHandoff(ctx, leadID, rep.ID, context)
// Initiate transfer
transferResult, err := telephonyClient.TransferCall(ctx, TransferRequest{
ToNumber: rep.Phone,
Announce: fmt.Sprintf("Transferring %s lead. Context: %s", urgency, context),
WarmTransfer: true,
})
if err != nil {
return nil, err
}
return map[string]any{
"transferred": true,
"rep_name": rep.Name,
"message": fmt.Sprintf("Great! I'm transferring you to %s right now. They'll be with you in just a moment.", rep.Name),
}, nil
}
CRM Integrations
HubSpot Integration
package crm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
type HubSpotClient struct {
apiKey string
baseURL string
client *http.Client
}
func NewHubSpotClient(apiKey string) *HubSpotClient {
return &HubSpotClient{
apiKey: apiKey,
baseURL: "https://api.hubapi.com",
client: &http.Client{},
}
}
// CreateOrUpdateContact creates or updates a contact in HubSpot
func (c *HubSpotClient) CreateOrUpdateContact(ctx context.Context, lead Lead) error {
properties := map[string]any{
"firstname": lead.FirstName,
"lastname": lead.LastName,
"email": lead.Email,
"phone": lead.Phone,
"company": lead.Company,
"jobtitle": lead.JobTitle,
// Custom properties
"lead_score": lead.LeadScore,
"lead_status": lead.LeadStatus,
"pain_points": strings.Join(lead.PainPoints, "; "),
"budget_range": lead.BudgetRange,
"decision_timeline": lead.Timeline,
"qualification_notes": lead.Notes,
}
payload := map[string]any{
"properties": properties,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/crm/v3/objects/contacts", c.baseURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("HubSpot API error: %d", resp.StatusCode)
}
return nil
}
// CreateDeal creates a deal/opportunity
func (c *HubSpotClient) CreateDeal(ctx context.Context, leadID string, dealName string, amount float64, stage string) error {
payload := map[string]any{
"properties": map[string]any{
"dealname": dealName,
"amount": amount,
"dealstage": stage,
"pipeline": "default",
},
"associations": []map[string]any{
{
"to": map[string]any{"id": leadID},
"types": []map[string]any{
{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3},
},
},
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/crm/v3/objects/deals", c.baseURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// LogActivity logs a call activity
func (c *HubSpotClient) LogCallActivity(ctx context.Context, contactID string, call CallData) error {
payload := map[string]any{
"properties": map[string]any{
"hs_call_body": call.Transcript,
"hs_call_callee_object_id": contactID,
"hs_call_direction": call.Direction,
"hs_call_disposition": call.Disposition,
"hs_call_duration": call.Duration.Milliseconds(),
"hs_call_status": "COMPLETED",
"hs_call_title": call.Title,
"hs_timestamp": call.Timestamp.UnixMilli(),
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/crm/v3/objects/calls", c.baseURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Salesforce Integration
package crm
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)
type SalesforceClient struct {
accessToken string
instanceURL string
client *http.Client
}
func NewSalesforceClient(accessToken, instanceURL string) *SalesforceClient {
return &SalesforceClient{
accessToken: accessToken,
instanceURL: instanceURL,
client: &http.Client{},
}
}
// CreateLead creates a lead in Salesforce
func (c *SalesforceClient) CreateLead(ctx context.Context, lead Lead) (string, error) {
payload := map[string]any{
"FirstName": lead.FirstName,
"LastName": lead.LastName,
"Email": lead.Email,
"Phone": lead.Phone,
"Company": lead.Company,
"Title": lead.JobTitle,
"Industry": lead.Industry,
"NumberOfEmployees": parseCompanySize(lead.CompanySize),
"LeadSource": "Voice AI",
"Status": mapLeadStatus(lead.LeadStatus),
"Rating": mapLeadRating(lead.LeadScore),
"Description": lead.Notes,
// Custom fields
"Lead_Score__c": lead.LeadScore,
"Pain_Points__c": strings.Join(lead.PainPoints, "; "),
"Budget_Range__c": lead.BudgetRange,
"Timeline__c": lead.Timeline,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/services/data/v58.0/sobjects/Lead/", c.instanceURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
ID string `json:"id"`
Success bool `json:"success"`
}
json.NewDecoder(resp.Body).Decode(&result)
if !result.Success {
return "", fmt.Errorf("failed to create lead")
}
return result.ID, nil
}
// CreateTask creates a follow-up task
func (c *SalesforceClient) CreateTask(ctx context.Context, leadID string, task Task) error {
payload := map[string]any{
"WhoId": leadID,
"Subject": task.Subject,
"Description": task.Description,
"ActivityDate": task.DueDate.Format("2006-01-02"),
"Priority": task.Priority,
"Status": "Not Started",
"TaskSubtype": "Call",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/services/data/v58.0/sobjects/Task/", c.instanceURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
// LogCallActivity logs call activity
func (c *SalesforceClient) LogCallActivity(ctx context.Context, leadID string, call CallData) error {
payload := map[string]any{
"WhoId": leadID,
"Subject": call.Title,
"Description": call.Transcript,
"CallType": call.Direction,
"CallDurationInSeconds": int(call.Duration.Seconds()),
"CallDisposition": call.Disposition,
"Status": "Completed",
"ActivityDate": call.Timestamp.Format("2006-01-02"),
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequestWithContext(ctx, "POST",
fmt.Sprintf("%s/services/data/v58.0/sobjects/Task/", c.instanceURL),
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+c.accessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
Outbound vs Inbound Strategies
Inbound Lead Response
Speed is everything for inbound leads. Respond within seconds of form submission.
// Webhook handler for form submissions
func HandleFormSubmission(w http.ResponseWriter, r *http.Request) {
var form FormSubmission
json.NewDecoder(r.Body).Decode(&form)
// Immediately trigger call
call, err := voiceAgent.InitiateCall(context.Background(), CallRequest{
AgentID: "instant_responder",
To: form.Phone,
Variables: map[string]string{
"leadName": form.FirstName,
"interest": form.Interest,
"formSource": form.Source,
"companyName": "TechCorp",
},
Priority: "high",
})
if err != nil {
// Fallback: Add to high-priority queue
queue.Add(form, "high_priority")
}
w.WriteHeader(http.StatusAccepted)
}
Inbound Best Practices:
| Metric | Target | Why It Matters |
|---|---|---|
| Response time | < 60 seconds | 78% of customers buy from the first responder |
| Attempt rate | 100% | Never miss a lead |
| Connect rate | > 50% | Optimize for reachability |
| Qualification rate | > 30% | Quality of qualification process |
Outbound Sales Campaigns
Proactive outreach requires careful timing and compliance.
// Campaign configuration
type OutboundCampaign struct {
ID string
Name string
AgentID string
ContactList []Contact
CallingHours CallingHours
DelayBetweenCalls time.Duration
MaxAttemptsPerLead int
DaysBeforeRetry int
ComplianceRules ComplianceRules
}
type CallingHours struct {
StartHour int // 9 (9 AM)
EndHour int // 18 (6 PM)
Days []time.Weekday
Timezone string
}
type ComplianceRules struct {
CheckDNC bool
MaxCallsPerDay int
RequireOptIn bool
RecordingConsent bool
}
func RunCampaign(ctx context.Context, campaign OutboundCampaign) {
for _, contact := range campaign.ContactList {
// Check compliance
if !isCompliant(contact, campaign.ComplianceRules) {
continue
}
// Check calling hours
if !isWithinCallingHours(contact.Timezone, campaign.CallingHours) {
scheduleForLater(contact, campaign)
continue
}
// Check attempt limits
if contact.Attempts >= campaign.MaxAttemptsPerLead {
markAsMaxAttempts(contact)
continue
}
// Make the call
initiateOutboundCall(ctx, campaign.AgentID, contact)
// Rate limiting
time.Sleep(campaign.DelayBetweenCalls)
}
}
Outbound Best Practices:
| Strategy | Description | Impact |
|---|---|---|
| Time-zone awareness | Call during local business hours | 2x connect rate |
| Lead prioritization | Hot leads first | 30% higher conversion |
| Multi-channel | Call + email + SMS | 45% more meetings |
| Personalization | Reference specific triggers | 25% better engagement |
| Persistence | 6-8 touch points | 80% of sales on 5th+ contact |
A/B Testing Outbound Scripts
{
"campaign": {
"id": "q1_outreach",
"variants": [
{
"id": "direct_ask",
"weight": 33,
"greetingMessage": "Hi {{contactName}}! Quick question - is improving {{painPoint}} on your radar this quarter?",
"hypothesis": "Direct questions get faster qualification"
},
{
"id": "value_first",
"weight": 33,
"greetingMessage": "Hi {{contactName}}! We just helped {{similarCompany}} cut their {{metric}} by 40%. Thought you might find it relevant.",
"hypothesis": "Social proof increases interest"
},
{
"id": "curiosity",
"weight": 34,
"greetingMessage": "Hi {{contactName}}! I came across something interesting about {{prospectCompany}} and wanted to share a quick thought. Got 30 seconds?",
"hypothesis": "Curiosity drives engagement"
}
],
"successMetric": "meeting_booked_rate",
"minSampleSize": 100
}
}
Key Sales Metrics
Tracking Implementation
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
leadsProcessed = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sales_leads_processed_total",
Help: "Total number of leads processed",
},
[]string{"source", "status"},
)
leadResponseTime = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "sales_lead_response_seconds",
Help: "Time from lead creation to first contact",
Buckets: []float64{30, 60, 120, 300, 600, 1800, 3600},
},
[]string{"source"},
)
qualificationRate = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sales_qualification_rate",
Help: "Percentage of leads qualified",
},
[]string{"campaign"},
)
meetingsBooked = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sales_meetings_booked_total",
Help: "Total meetings booked",
},
[]string{"agent", "meeting_type"},
)
conversionRate = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sales_conversion_rate",
Help: "Meeting to closed deal conversion rate",
},
[]string{"campaign"},
)
averageLeadScore = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sales_average_lead_score",
Help: "Average lead qualification score",
},
[]string{"source"},
)
)
type SalesMetrics struct {
LeadID string
Source string
ResponseTime time.Duration
QualificationScore int
MeetingBooked bool
Converted bool
Revenue float64
}
func TrackLeadMetrics(m SalesMetrics) {
// Track lead processing
status := "unqualified"
if m.QualificationScore >= 50 {
status = "qualified"
}
leadsProcessed.WithLabelValues(m.Source, status).Inc()
// Track response time
leadResponseTime.WithLabelValues(m.Source).Observe(m.ResponseTime.Seconds())
// Track meetings
if m.MeetingBooked {
meetingsBooked.WithLabelValues("voice_agent", "demo").Inc()
}
}
Key Performance Indicators
| Metric | Formula | Target | Why It Matters |
|---|---|---|---|
| Lead Response Time | First contact - Lead created | < 60s | 78% buy from first responder |
| Contact Rate | Connects / Total calls | > 50% | Efficiency of dialing |
| Qualification Rate | Qualified / Contacted | > 30% | Quality of process |
| Meeting Book Rate | Meetings / Qualified | > 40% | Conversion effectiveness |
| Show Rate | Attended / Booked | > 80% | Confirmation process |
| SQL to Opp Rate | Opportunities / SQLs | > 25% | Lead quality |
| Win Rate | Closed-Won / Opportunities | > 20% | Sales effectiveness |
| CAC Efficiency | Revenue / CAC | > 3x | Unit economics |
Real-time Dashboard Query
-- Lead response metrics
WITH lead_metrics AS (
SELECT
DATE_TRUNC('hour', created_at) AS hour,
source,
COUNT(*) AS total_leads,
COUNT(CASE WHEN first_contact_at IS NOT NULL THEN 1 END) AS contacted,
AVG(EXTRACT(EPOCH FROM (first_contact_at - created_at))) AS avg_response_seconds,
COUNT(CASE WHEN lead_score >= 50 THEN 1 END) AS qualified,
COUNT(CASE WHEN meeting_booked_at IS NOT NULL THEN 1 END) AS meetings_booked
FROM leads
WHERE created_at >= NOW() - INTERVAL '24 hours'
GROUP BY 1, 2
)
SELECT
hour,
source,
total_leads,
ROUND(contacted::numeric / total_leads * 100, 1) AS contact_rate,
ROUND(avg_response_seconds, 1) AS avg_response_seconds,
ROUND(qualified::numeric / contacted * 100, 1) AS qualification_rate,
ROUND(meetings_booked::numeric / qualified * 100, 1) AS meeting_rate
FROM lead_metrics
ORDER BY hour DESC;
Best Practices
1. Speed-to-Lead
// Prioritize speed for inbound leads
func HandleInboundLead(lead Lead) {
// Target: Call within 30 seconds
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Pre-fetch agent config (cache)
agent := agentCache.Get("instant_responder")
// Parallel operations
go enrichLead(&lead) // Enrich in background
go logToAnalytics(lead) // Log in background
// Make call immediately
call, err := initiateCall(ctx, agent, lead)
if err != nil {
// Fallback: Queue for immediate retry
priorityQueue.Push(lead)
}
}
2. Personalization at Scale
// Dynamic variable injection
variables := map[string]string{
"leadName": lead.FirstName,
"company": lead.Company,
"industry": lead.Industry,
"triggerEvent": lead.LastActivity, // "downloaded whitepaper"
"painPoint": inferPainPoint(lead),
"similarCompany": findSimilarCustomer(lead.Industry),
"relevantMetric": getRelevantMetric(lead.Industry),
}
3. Graceful Objection Handling
// In system prompt
## Objection Handling Framework
When prospect objects, follow ARC:
- Acknowledge: "I hear you..."
- Relate: "Many of our customers felt the same way..."
- Continue: "What they found was..."
Common objections:
"Not interested"
→ "Totally understand. Just curious - is it the timing or the solution that doesn't feel right?"
→ Listen, then offer to send resources or schedule for later
"Send me an email"
→ "Happy to! What would be most useful to include?"
→ Get email, personalize based on conversation
→ Ask if quick 2-minute call to ensure relevance
"We already use [competitor]"
→ "Great that you're already addressing this! What made you go with them?"
→ Listen for gaps or frustrations
→ Position differentiation if relevant
"Don't have budget"
→ "Makes sense - budgets are tight. When do you typically plan for initiatives like this?"
→ Gauge timeline, offer to reconnect when appropriate
4. Follow-Up Discipline
// Automated follow-up scheduling
func ScheduleFollowUps(call CompletedCall) {
switch call.Outcome {
case "interested_no_meeting":
// Hot follow-up
scheduleCall(call.LeadID, 4*time.Hour, "follow_up_agent")
scheduleEmail(call.LeadID, 1*time.Hour, "resources_email")
case "callback_requested":
// Respect their preferred time
scheduleCall(call.LeadID, call.CallbackTime, "callback_agent")
scheduleReminder(call.LeadID, call.CallbackTime.Add(-30*time.Minute))
case "voicemail":
// Multi-touch sequence
scheduleEmail(call.LeadID, 2*time.Hour, "voicemail_follow_up")
scheduleCall(call.LeadID, 24*time.Hour, "retry_agent")
case "not_interested":
// Long-term nurture
addToNurtureSequence(call.LeadID, "dormant_leads")
}
}
5. Compliance and Consent
// Compliance checks before every call
func PreCallComplianceCheck(contact Contact, campaign Campaign) error {
// Check Do Not Call list
if dncList.Contains(contact.Phone) {
return ErrOnDNCList
}
// Check calling hours (TCPA compliance)
if !isWithinLegalCallingHours(contact.Timezone) {
return ErrOutsideCallingHours
}
// Check frequency limits
callsToday := getCallsToday(contact.Phone)
if callsToday >= campaign.MaxDailyAttempts {
return ErrFrequencyLimitReached
}
// Check opt-in status for cold outreach
if campaign.RequiresOptIn && !contact.HasOptedIn {
return ErrNoOptIn
}
return nil
}
Complete Sales Agent Configuration
{
"agent": {
"name": "Enterprise Sales Qualifier",
"language": "en-US",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.7,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttEndpointing": 500,
"ttsProvider": "cartesia",
"ttsVoice": "95856005-0332-41b0-935f-352e296aa0df",
"greetingMessage": "Hi {{leadName}}! This is Alex from {{companyName}}. {{personalized_opener}} Do you have a quick moment?",
"prompt": "[Full BANT qualification prompt - see above]",
"allowInterruptions": true,
"vadSilenceDuration": 700,
"endCallAfterSilence": 30000,
"tools": [
{
"type": "function",
"function": {
"name": "save_lead_to_crm",
"description": "Save lead information to CRM"
}
},
{
"type": "function",
"function": {
"name": "check_calendar_availability",
"description": "Check available meeting slots"
}
},
{
"type": "function",
"function": {
"name": "book_meeting",
"description": "Book a meeting with sales team"
}
},
{
"type": "function",
"function": {
"name": "calculate_lead_score",
"description": "Calculate BANT lead score"
}
},
{
"type": "function",
"function": {
"name": "transfer_to_sales",
"description": "Transfer hot lead to available rep"
}
}
],
"transferEnabled": true,
"recordingEnabled": true,
"transcriptEnabled": true,
"webhooks": {
"onCallStart": "https://api.yourcrm.com/webhooks/call-start",
"onCallEnd": "https://api.yourcrm.com/webhooks/call-end",
"onMeetingBooked": "https://api.yourcrm.com/webhooks/meeting-booked"
}
}
}
Next Steps
- Lead Qualification Agent - Detailed qualification example
- Outbound Sales Agent - Outbound campaign setup
- Function Calling - CRM integration guide
- Call Transfer - Transfer to human agents
- Webhooks - Post-call automation