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 | Lower, from ₹4/min |
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://voice-agent.edesy.in/api/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 |
Comparing Outbound Scripts#
Edesy has no built-in A/B split. To compare scripts, split your contact list into two CSVs, run two campaigns whose agents differ in one thing (greeting, prompt or calling window), and compare connection rate, duration and outcomes in campaign analytics.
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