AI Voice Agents for Debt Collection
Deploy AI voice agents that improve collection rates while maintaining compliance with regulatory requirements. This guide covers payment reminders, arrangement negotiation, payment confirmation, and dispute handling.
Use Cases
| Use Case | Description | Automation Rate |
|---|---|---|
| Payment Reminders | Proactive outreach for overdue payments | 85-95% |
| Arrangement Negotiation | Negotiate payment plans within authorized limits | 60-75% |
| Payment Confirmation | Confirm received payments and update accounts | 90-98% |
| Dispute Handling | Document disputes and route to specialists | 70-85% |
| Promise-to-Pay Follow-up | Follow up on missed payment commitments | 80-90% |
Agent Configuration
{
"agent": {
"name": "Collections Agent",
"language": "en-IN",
"llmProvider": "gemini-2.5",
"llmModel": "gemini-2.5-flash-lite",
"llmTemperature": 0.4,
"sttProvider": "deepgram",
"sttModel": "nova-3",
"sttConfig": {
"endpointing": 400,
"interimResults": true,
"keywords": ["payment:2", "dispute:2", "EMI:2", "amount:2"]
},
"ttsProvider": "azure",
"ttsVoice": "en-IN-NeerjaNeural",
"ttsConfig": {
"rate": "0.95",
"pitch": "0%"
},
"greetingMessage": "Hello, this is a call from {{companyName}} regarding your account. Am I speaking with {{customerName}}?",
"allowInterruptions": true,
"interruptionThreshold": 0.6,
"prompt": "...",
"tools": [],
"compliance": {
"recordingEnabled": true,
"recordingDisclosure": true,
"callHoursStart": "08:00",
"callHoursEnd": "21:00",
"timezone": "customer_local",
"maxAttemptsPerDay": 3,
"maxAttemptsPerWeek": 7
},
"webhooks": {
"url": "https://your-server.com/webhooks/collections",
"events": ["call.ended", "payment.arranged", "dispute.raised"]
}
}
}
System Prompt
You are a professional collections agent for {{companyName}}. Your role is to collect outstanding payments while treating customers with respect and empathy.
## Account Information
- Customer Name: {{customerName}}
- Account Number: {{accountNumber}}
- Outstanding Amount: {{outstandingAmount}}
- Days Past Due: {{daysPastDue}}
- Last Payment: {{lastPaymentDate}} ({{lastPaymentAmount}})
- Minimum Payment: {{minimumPayment}}
- Payment Due Date: {{dueDate}}
- Previous Arrangements: {{previousArrangements}}
## Your Objectives (in order)
1. Verify you're speaking with the right person
2. Inform them of the outstanding balance
3. Understand their situation
4. Collect payment or establish a payment arrangement
5. Document the outcome
## Conversation Flow
### Step 1: Identity Verification
First, confirm you're speaking with the account holder.
"Am I speaking with {{customerName}}?"
If yes, proceed. If no:
"Is {{customerName}} available? I need to speak with them regarding an important account matter."
Do NOT discuss account details with anyone other than the account holder.
### Step 2: Disclosure (Required)
"This call is from {{companyName}} regarding your account and may be recorded for quality assurance. This is an attempt to collect a debt."
### Step 3: State the Purpose
"I'm calling about your account ending in {{accountLast4}}, which currently shows an outstanding balance of {{outstandingAmount}}. This amount was due on {{dueDate}}."
### Step 4: Listen and Respond
#### If they can pay in full:
"That's great. I can help you make that payment right now. Would you like to pay by card or bank transfer?"
→ Use process_payment tool
#### If they need a payment plan:
"I understand. Let me see what options we have available."
→ Check their eligibility and offer arrangements within your authority
#### If they dispute the debt:
"I understand you're disputing this. Let me document the details of your dispute."
→ Use log_dispute tool
"Your dispute has been logged with reference number [X]. You'll receive written confirmation within 5 business days. No collection activity will occur on this account while we investigate."
#### If they claim financial hardship:
"I'm sorry to hear you're going through a difficult time. Let me understand your situation better so we can find a workable solution."
→ Ask about income, expenses, and what they can realistically afford
→ Offer hardship programs if available
### Authorized Payment Arrangements
You can offer the following without supervisor approval:
- Full payment within 7 days: No additional fees
- 2-3 installment plan: Split over 30-90 days
- Reduced settlement: Up to {{settlementAuthority}}% discount for immediate payment
For arrangements beyond these limits:
"Let me check with my supervisor on that option. Can I call you back within 24 hours?"
→ Schedule callback and escalate
## Tone Guidelines
- Professional and firm, but never aggressive
- Empathetic to genuine hardship
- Patient with explanations
- Clear about consequences without threats
- Respectful at all times
## What You MUST Do
- Disclose this is a debt collection call
- Verify identity before discussing details
- Accurately state the amount owed
- Honor disputes and cease collection on disputed accounts
- Provide your callback number
- Respect cease-and-desist requests
## What You MUST NOT Do
- Threaten legal action you cannot take
- Discuss the debt with third parties
- Call outside permitted hours
- Use abusive or harassing language
- Misrepresent the debt amount
- Ignore dispute requests
- Promise to remove accurate credit reporting
- Call after a cease-and-desist request
## If Customer Becomes Hostile
"I understand you're frustrated. My goal is to find a solution that works for you. Would you prefer I call back at a better time?"
If abuse continues:
"I want to help, but I'll need to end this call if we can't have a respectful conversation. Is there a better time to discuss this?"
## Closing
After payment or arrangement:
"Thank you for working with me today. You'll receive confirmation at {{customerEmail}}. Is there anything else I can help with?"
If no resolution:
"I understand. Please keep in mind that the balance of {{outstandingAmount}} remains due. You can reach us at {{callbackNumber}} to make a payment at any time. Have a good day."
Tools
Get Account Details
{
"type": "function",
"function": {
"name": "get_account_details",
"description": "Retrieve account details and payment history for a customer",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "The customer account ID"
},
"phone": {
"type": "string",
"description": "Customer phone number for lookup"
}
},
"required": ["account_id"]
}
}
}
Process Payment
{
"type": "function",
"function": {
"name": "process_payment",
"description": "Process a payment for the outstanding balance",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"amount": {
"type": "number",
"description": "Payment amount in currency units"
},
"payment_method": {
"type": "string",
"enum": ["card", "bank_transfer", "upi", "auto_debit"],
"description": "Payment method"
},
"payment_date": {
"type": "string",
"description": "Date of payment (ISO 8601)"
}
},
"required": ["account_id", "amount", "payment_method"]
}
}
}
Create Payment Arrangement
{
"type": "function",
"function": {
"name": "create_arrangement",
"description": "Create a payment arrangement or installment plan",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"arrangement_type": {
"type": "string",
"enum": ["full_payment", "installment", "settlement", "hardship"],
"description": "Type of arrangement"
},
"total_amount": {
"type": "number",
"description": "Total amount to be paid"
},
"installments": {
"type": "array",
"items": {
"type": "object",
"properties": {
"amount": { "type": "number" },
"due_date": { "type": "string" }
}
},
"description": "Installment schedule"
},
"first_payment_date": {
"type": "string",
"description": "Date of first payment"
}
},
"required": ["account_id", "arrangement_type", "total_amount"]
}
}
}
Log Dispute
{
"type": "function",
"function": {
"name": "log_dispute",
"description": "Log a customer dispute and pause collection activity",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"dispute_type": {
"type": "string",
"enum": ["not_my_debt", "wrong_amount", "already_paid", "identity_theft", "other"],
"description": "Type of dispute"
},
"dispute_reason": {
"type": "string",
"description": "Detailed description of the dispute"
},
"supporting_info": {
"type": "string",
"description": "Any supporting information provided"
}
},
"required": ["account_id", "dispute_type", "dispute_reason"]
}
}
}
Log Promise to Pay
{
"type": "function",
"function": {
"name": "log_promise_to_pay",
"description": "Record a promise to pay commitment",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"promised_amount": {
"type": "number",
"description": "Amount customer promised to pay"
},
"promised_date": {
"type": "string",
"description": "Date customer promised to pay"
},
"payment_method": {
"type": "string",
"description": "How they plan to pay"
},
"follow_up_date": {
"type": "string",
"description": "Date to follow up if payment not received"
}
},
"required": ["account_id", "promised_amount", "promised_date"]
}
}
}
Transfer to Human
{
"type": "function",
"function": {
"name": "transfer_to_agent",
"description": "Transfer call to a human collections agent or supervisor",
"parameters": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"description": "Customer account ID"
},
"reason": {
"type": "string",
"enum": ["supervisor_request", "complex_negotiation", "legal_threat", "hardship", "escalation"],
"description": "Reason for transfer"
},
"notes": {
"type": "string",
"description": "Context for the receiving agent"
}
},
"required": ["account_id", "reason"]
}
}
}
Tool Handlers
func handleProcessPayment(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
amount := params["amount"].(float64)
method := params["payment_method"].(string)
// Validate payment against account balance
account, err := db.GetAccount(accountID)
if err != nil {
return "I'm having trouble accessing your account. Let me transfer you to a colleague.", err
}
if amount > account.OutstandingBalance {
return fmt.Sprintf("The amount of %.2f exceeds your outstanding balance of %.2f. Would you like to pay the full balance?",
amount, account.OutstandingBalance), nil
}
// For immediate card payments
if method == "card" {
return "I'll need your card details to process this payment. Please provide the 16-digit card number.", nil
}
// For bank transfers and UPI
if method == "bank_transfer" || method == "upi" {
paymentRef := generatePaymentReference(accountID)
return fmt.Sprintf(
"To complete the bank transfer of %.2f, please use reference number %s. "+
"Payment details will be sent to your registered mobile. "+
"The payment will reflect within 24 hours.",
amount, paymentRef), nil
}
return "Payment method not supported", nil
}
func handleCreateArrangement(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
arrangementType := params["arrangement_type"].(string)
totalAmount := params["total_amount"].(float64)
account, _ := db.GetAccount(accountID)
// Validate arrangement is within authorized limits
if !isWithinAuthority(arrangementType, totalAmount, account) {
return "This arrangement requires supervisor approval. Let me schedule a callback within 24 hours to confirm.", nil
}
// Create the arrangement
arrangement := &Arrangement{
AccountID: accountID,
Type: arrangementType,
TotalAmount: totalAmount,
CreatedAt: time.Now(),
Status: "active",
}
if installments, ok := params["installments"].([]any); ok {
arrangement.Installments = parseInstallments(installments)
}
err := db.CreateArrangement(arrangement)
if err != nil {
return "I wasn't able to set up the arrangement. Let me transfer you to a colleague who can help.", err
}
// Generate confirmation
confirmationNumber := arrangement.ID[:8]
// Send confirmation SMS/email
sendArrangementConfirmation(account, arrangement)
return fmt.Sprintf(
"Your payment arrangement has been set up. Confirmation number: %s. "+
"You'll receive the details via SMS and email. "+
"Your first payment of %.2f is due on %s.",
confirmationNumber,
arrangement.Installments[0].Amount,
arrangement.Installments[0].DueDate.Format("January 2"),
), nil
}
func handleLogDispute(params map[string]any) (string, error) {
accountID := params["account_id"].(string)
disputeType := params["dispute_type"].(string)
reason := params["dispute_reason"].(string)
dispute := &Dispute{
AccountID: accountID,
Type: disputeType,
Reason: reason,
Status: "open",
CreatedAt: time.Now(),
}
err := db.CreateDispute(dispute)
if err != nil {
return "I'm having trouble logging your dispute. Please contact us at our main number.", err
}
// Mark account as disputed - stop collections
db.UpdateAccountStatus(accountID, "disputed")
// Trigger dispute workflow
triggerDisputeWorkflow(dispute)
return fmt.Sprintf(
"Your dispute has been logged with reference number %s. "+
"All collection activity on this account is paused while we investigate. "+
"You'll receive written confirmation within 5 business days, and "+
"we'll complete our investigation within 30 days as required by law.",
dispute.ID[:8],
), nil
}
Compliance Requirements
FDCPA Compliance (United States)
type FDCPACompliance struct {
// Time restrictions
CallHoursStart int // 8 AM local time
CallHoursEnd int // 9 PM local time
// Disclosure requirements
MiniMirandaRequired bool // Must identify as debt collector
// Contact restrictions
HonorCeaseContact bool // Must stop if requested
NoWorkplaceCalls bool // Unless permitted
NoThirdPartyDisclosure bool // Cannot discuss with others
// Dispute handling
VerificationRequired bool // Must verify if disputed
PauseOnDispute bool // Must pause collection on dispute
}
func validateFDCPACompliance(call *Call) error {
// Check calling hours
localTime := call.Customer.GetLocalTime()
if localTime.Hour() < 8 || localTime.Hour() >= 21 {
return errors.New("call outside permitted hours (8 AM - 9 PM local)")
}
// Check cease and desist
if call.Customer.CeaseContactRequested {
return errors.New("customer has requested no contact")
}
// Check dispute status
if call.Account.DisputeStatus == "open" {
return errors.New("account has open dispute - collection paused")
}
return nil
}
RBI Guidelines (India)
type RBICompliance struct {
// Time restrictions (RBI Circular 2022)
CallHoursStart int // 8 AM
CallHoursEnd int // 7 PM (stricter than FDCPA)
// Frequency limits
MaxCallsPerDay int // Maximum 3 calls per day
MaxCallsPerWeek int // Maximum 7 calls per week
// Disclosure
AgentIDRequired bool // Must identify with ID
CompanyNameRequired bool
// Language
LocalLanguageOption bool // Must offer local language
// Recording
CallRecordingRequired bool
RetentionPeriod int // 2 years minimum
}
func validateRBICompliance(call *Call) error {
// Check calling hours (8 AM - 7 PM IST)
ist, _ := time.LoadLocation("Asia/Kolkata")
localTime := time.Now().In(ist)
if localTime.Hour() < 8 || localTime.Hour() >= 19 {
return errors.New("call outside RBI permitted hours (8 AM - 7 PM)")
}
// Check daily call frequency
callsToday := db.GetCallCountToday(call.Customer.Phone)
if callsToday >= 3 {
return errors.New("maximum 3 calls per day reached")
}
// Check weekly call frequency
callsThisWeek := db.GetCallCountThisWeek(call.Customer.Phone)
if callsThisWeek >= 7 {
return errors.New("maximum 7 calls per week reached")
}
return nil
}
Call Recording and Retention
type RecordingConfig struct {
Enabled bool
DisclosureMessage string
StorageDuration time.Duration
EncryptionEnabled bool
ConsentRequired bool
}
func setupCompliantRecording(call *Call) error {
config := RecordingConfig{
Enabled: true,
DisclosureMessage: "This call may be recorded for quality and training purposes.",
StorageDuration: 2 * 365 * 24 * time.Hour, // 2 years
EncryptionEnabled: true,
ConsentRequired: false, // One-party consent jurisdiction
}
// Start recording with disclosure
if config.Enabled {
call.RecordingID = startRecording(call.ID)
// Log recording start for audit
auditLog.Record(AuditEvent{
Type: "recording_started",
CallID: call.ID,
AccountID: call.Account.ID,
Timestamp: time.Now(),
})
}
return nil
}
Audit Trail
type CollectionAuditLog struct {
ID string
CallID string
AccountID string
Timestamp time.Time
EventType string
Details map[string]any
AgentID string
RecordingID string
}
func logCollectionEvent(event CollectionAuditLog) {
// Required for compliance
event.Timestamp = time.Now()
db.InsertAuditLog(event)
// Real-time compliance monitoring
if event.EventType == "dispute_raised" ||
event.EventType == "cease_contact" ||
event.EventType == "complaint" {
alertComplianceTeam(event)
}
}
Metrics and Analytics
Key Performance Indicators
type CollectionMetrics struct {
// Contact Metrics
TotalAttempts int
ContactRate float64 // Percentage of successful contacts
RightPartyContacts int // Reached actual account holder
// Collection Metrics
TotalCollected float64
CollectionRate float64 // Amount collected / Amount attempted
PaymentArrangements int
ArrangementValue float64
// Quality Metrics
AverageCallDuration time.Duration
DisputeRate float64
EscalationRate float64
ComplianceScore float64
// Promise Metrics
PromisesToPay int
PromiseKeptRate float64 // Promises fulfilled / Promises made
}
func calculateCollectionMetrics(campaignID string, period time.Duration) *CollectionMetrics {
calls := db.GetCampaignCalls(campaignID, period)
metrics := &CollectionMetrics{}
var totalAttemptedAmount float64
var totalCollected float64
var rightPartyContacts int
var disputes int
for _, call := range calls {
metrics.TotalAttempts++
totalAttemptedAmount += call.Account.OutstandingBalance
if call.ContactMade {
if call.RightPartyContact {
rightPartyContacts++
}
if call.PaymentMade {
totalCollected += call.PaymentAmount
}
if call.ArrangementMade {
metrics.PaymentArrangements++
metrics.ArrangementValue += call.ArrangementAmount
}
if call.DisputeRaised {
disputes++
}
}
metrics.AverageCallDuration += call.Duration
}
// Calculate rates
if metrics.TotalAttempts > 0 {
metrics.ContactRate = float64(rightPartyContacts) / float64(metrics.TotalAttempts)
metrics.AverageCallDuration /= time.Duration(metrics.TotalAttempts)
}
if totalAttemptedAmount > 0 {
metrics.CollectionRate = totalCollected / totalAttemptedAmount
}
if rightPartyContacts > 0 {
metrics.DisputeRate = float64(disputes) / float64(rightPartyContacts)
}
metrics.TotalCollected = totalCollected
metrics.RightPartyContacts = rightPartyContacts
return metrics
}
Dashboard Metrics
func getCollectionsDashboard(teamID string) *Dashboard {
today := time.Now().Truncate(24 * time.Hour)
return &Dashboard{
// Today's Performance
TodayStats: TodayStats{
CallsMade: db.CountCalls(teamID, today),
AmountCollected: db.SumPayments(teamID, today),
ArrangementsSetup: db.CountArrangements(teamID, today),
ContactRate: calculateContactRate(teamID, today),
},
// Agent Performance
AgentMetrics: getAgentMetrics(teamID, today),
// Account Buckets
AccountBuckets: []AccountBucket{
{Name: "0-30 DPD", Count: db.CountAccountsByDPD(0, 30)},
{Name: "31-60 DPD", Count: db.CountAccountsByDPD(31, 60)},
{Name: "61-90 DPD", Count: db.CountAccountsByDPD(61, 90)},
{Name: "90+ DPD", Count: db.CountAccountsByDPD(91, 999)},
},
// Compliance
ComplianceAlerts: getComplianceAlerts(teamID),
}
}
Promise-to-Pay Tracking
type PromiseToPayMetrics struct {
TotalPromises int
PromisesKept int
PromisesBroken int
PromisesPending int
KeptRate float64
AveragePromiseAge time.Duration
}
func trackPromiseToPayMetrics(period time.Duration) *PromiseToPayMetrics {
promises := db.GetPromises(period)
metrics := &PromiseToPayMetrics{
TotalPromises: len(promises),
}
for _, p := range promises {
switch p.Status {
case "kept":
metrics.PromisesKept++
case "broken":
metrics.PromisesBroken++
case "pending":
metrics.PromisesPending++
}
}
if metrics.TotalPromises > 0 {
metrics.KeptRate = float64(metrics.PromisesKept) /
float64(metrics.PromisesKept + metrics.PromisesBroken)
}
return metrics
}
Campaign Management
Outbound Campaign Setup
func runCollectionsCampaign(campaignID string) {
campaign, _ := db.GetCampaign(campaignID)
accounts := db.GetCampaignAccounts(campaignID)
// Sort by priority (higher DPD, higher balance first)
sort.Slice(accounts, func(i, j int) bool {
scoreI := accounts[i].DaysPastDue * int(accounts[i].Balance)
scoreJ := accounts[j].DaysPastDue * int(accounts[j].Balance)
return scoreI > scoreJ
})
for _, account := range accounts {
// Pre-call compliance check
if err := validateCompliance(account); err != nil {
log.Printf("Skipping %s: %v", account.ID, err)
continue
}
// Check optimal calling time
if !isOptimalCallTime(account) {
scheduleForLater(account)
continue
}
// Prepare context
variables := map[string]string{
"customerName": account.CustomerName,
"accountNumber": account.ID,
"accountLast4": account.ID[len(account.ID)-4:],
"outstandingAmount": formatCurrency(account.Balance),
"daysPastDue": strconv.Itoa(account.DaysPastDue),
"minimumPayment": formatCurrency(account.MinimumPayment),
"dueDate": account.DueDate.Format("January 2, 2006"),
"lastPaymentDate": account.LastPaymentDate.Format("January 2"),
"lastPaymentAmount": formatCurrency(account.LastPaymentAmount),
"previousArrangements": summarizeArrangements(account.Arrangements),
"settlementAuthority": strconv.Itoa(campaign.SettlementAuthority),
"companyName": campaign.CompanyName,
"callbackNumber": campaign.CallbackNumber,
"customerEmail": account.Email,
}
// Initiate call
initiateCall(campaign.AgentID, account.Phone, variables)
// Respect pacing
time.Sleep(campaign.PacingDelay)
}
}
API Integration
# Initiate collection call
curl -X POST https://api.edesy.in/v1/calls \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "collections_agent",
"to": "+919876543210",
"variables": {
"customerName": "Rahul Kumar",
"accountNumber": "ACC-2024-12345",
"outstandingAmount": "Rs. 15,450",
"daysPastDue": "45",
"minimumPayment": "Rs. 5,000",
"dueDate": "December 15, 2024",
"companyName": "FinCorp Services"
},
"compliance": {
"record_call": true,
"disclosure_required": true
},
"webhook_url": "https://your-server.com/webhooks/collection-complete"
}'
Best Practices
1. Right Party Contact Verification
// Always verify identity before discussing account
func verifyIdentity(session *Session) bool {
// Ask for identifying information
session.TTS.Speak("For security, could you please confirm the last 4 digits of your PAN card?")
response := session.WaitForResponse()
if verifyPAN(session.Account.PAN, response) {
return true
}
// Alternative verification
session.TTS.Speak("Let me try another verification. What is your date of birth?")
response = session.WaitForResponse()
return verifyDOB(session.Account.DOB, response)
}
2. Empathetic Communication
// Phrases that acknowledge customer situation
var empathyPhrases = map[string][]string{
"financial_hardship": {
"I understand times can be tough financially.",
"I appreciate you sharing that with me.",
"Let's see what options we have to work with your situation.",
},
"confusion": {
"I completely understand this can be confusing.",
"Let me explain that more clearly.",
"That's a very fair question.",
},
"frustration": {
"I hear your frustration, and I want to help.",
"I understand this isn't the call you wanted to receive.",
"Let's work together to find a solution.",
},
}
3. Tiered Escalation
type EscalationTier struct {
Level int
AuthorizedDiscount float64
ArrangementTerms int // months
RequiresApproval bool
}
var escalationTiers = []EscalationTier{
{Level: 1, AuthorizedDiscount: 0, ArrangementTerms: 3, RequiresApproval: false},
{Level: 2, AuthorizedDiscount: 0.10, ArrangementTerms: 6, RequiresApproval: false},
{Level: 3, AuthorizedDiscount: 0.25, ArrangementTerms: 12, RequiresApproval: true},
{Level: 4, AuthorizedDiscount: 0.50, ArrangementTerms: 24, RequiresApproval: true},
}
func getAuthorizedArrangement(account *Account, requestedDiscount float64) *EscalationTier {
// Determine tier based on account age, amount, and customer history
customerScore := calculateCustomerScore(account)
for _, tier := range escalationTiers {
if requestedDiscount <= tier.AuthorizedDiscount {
return &tier
}
}
return nil // Requires supervisor
}
4. Optimal Contact Timing
func determineOptimalContactTime(account *Account) time.Time {
// Analyze past successful contacts
successfulContacts := db.GetSuccessfulContacts(account.ID)
if len(successfulContacts) > 0 {
// Return time when we've reached them before
return analyzeSuccessfulTimes(successfulContacts)
}
// Default optimal windows by segment
if account.Segment == "salaried" {
// Evening after work hours
return nextTimeInWindow(18, 20, account.Timezone)
} else if account.Segment == "business" {
// Late morning
return nextTimeInWindow(10, 12, account.Timezone)
}
// Default: mid-day
return nextTimeInWindow(11, 14, account.Timezone)
}
5. Follow-up Automation
type FollowUpRule struct {
Trigger string
DelayHours int
Action string
MaxAttempts int
}
var followUpRules = []FollowUpRule{
{Trigger: "promise_to_pay", DelayHours: 24, Action: "reminder_sms", MaxAttempts: 1},
{Trigger: "arrangement_due", DelayHours: -24, Action: "reminder_call", MaxAttempts: 2},
{Trigger: "payment_failed", DelayHours: 4, Action: "call", MaxAttempts: 3},
{Trigger: "no_contact", DelayHours: 48, Action: "retry_call", MaxAttempts: 5},
{Trigger: "voicemail", DelayHours: 24, Action: "retry_different_time", MaxAttempts: 3},
}
func scheduleFollowUp(account *Account, trigger string) {
rule := findRule(trigger)
if rule == nil {
return
}
// Check attempt limits
attempts := db.GetFollowUpAttempts(account.ID, trigger)
if attempts >= rule.MaxAttempts {
log.Printf("Max follow-up attempts reached for %s", account.ID)
return
}
followUpTime := time.Now().Add(time.Duration(rule.DelayHours) * time.Hour)
db.ScheduleFollowUp(FollowUp{
AccountID: account.ID,
ScheduledAt: followUpTime,
Action: rule.Action,
Trigger: trigger,
})
}
Testing
Compliance Test Scenarios
func TestFDCPACompliance(t *testing.T) {
agent := setupTestAgent()
// Test calling hours
t.Run("RejectsCallOutsideHours", func(t *testing.T) {
call := simulateCall(agent, CallConfig{
Time: time.Date(2024, 1, 1, 7, 0, 0, 0, time.Local), // 7 AM
})
assert.Equal(t, "blocked", call.Status)
assert.Contains(t, call.BlockReason, "outside permitted hours")
})
// Test Mini-Miranda disclosure
t.Run("IncludesDebtDisclosure", func(t *testing.T) {
call := simulateCall(agent, CallConfig{
Time: time.Date(2024, 1, 1, 10, 0, 0, 0, time.Local),
})
assert.Contains(t, call.Transcript, "attempt to collect a debt")
})
// Test dispute handling
t.Run("PausesOnDispute", func(t *testing.T) {
agent.Process("I dispute this debt")
account := db.GetAccount(testAccountID)
assert.Equal(t, "disputed", account.Status)
assert.True(t, agent.ToolCalled("log_dispute"))
})
// Test cease contact
t.Run("HonorsCeaseContact", func(t *testing.T) {
agent.Process("Stop calling me")
assert.Contains(t, agent.LastResponse, "won't receive any more calls")
})
}
Payment Flow Testing
func TestPaymentArrangement(t *testing.T) {
agent := setupTestAgent()
t.Run("CreatesValidArrangement", func(t *testing.T) {
agent.Process("I can pay 5000 rupees now and the rest next month")
assert.True(t, agent.ToolCalled("create_arrangement"))
arrangement := db.GetLatestArrangement(testAccountID)
assert.Equal(t, "installment", arrangement.Type)
assert.Len(t, arrangement.Installments, 2)
})
t.Run("EscalatesOverLimit", func(t *testing.T) {
agent.Process("I can only pay 30% of the total")
assert.Contains(t, agent.LastResponse, "supervisor approval")
assert.False(t, agent.ToolCalled("create_arrangement"))
})
}
Next Steps
- Customer Support - General support use cases
- Call Recording - Recording configuration
- Function Calling - Tool integration
- Webhooks - Call completion events
- Indian Languages - Hindi, Tamil collection agents