Add Caller Memory to Twilio Calls
Complete guide to adding persistent caller memory and context to your Twilio voice applications.
The Problem
When customers call your Twilio number multiple times, each call starts from zero. They have to re-explain their issue, verify their identity again, and navigate the same IVR menus—even if they called 5 minutes ago.
This creates frustration:
- Customers repeat themselves
- Agents ask redundant questions
- Average handle time increases by 2-3 minutes
- Customer satisfaction drops
The solution: Add caller memory to remember previous interactions and personalize every call.
What You'll Build
In this guide, you'll learn how to:
- Identify returning callers automatically
- Retrieve their conversation history and context
- Personalize IVR prompts based on previous calls
- Save conversation summaries for future reference
- Handle both high-confidence and low-confidence matches
Expected outcome:
- 25-35% faster call resolution
- 40-50% reduction in repeat questions
- Improved customer satisfaction
- Better agent efficiency
Time to implement: 15-20 minutes
Prerequisites
- Active Twilio account
- Sticky Calls API key (get one free)
- Basic knowledge of Twilio (Functions, Studio, or webhooks)
- Optional: Node.js or Python for webhook examples
Quick Start Overview
Here's the high-level flow:
Incoming Call → Twilio receives call
→ Make API call to Sticky Calls (identify caller)
→ Receive context (if returning caller)
→ Personalize experience based on context
→ Handle call normally
→ Save context at end of call
API Endpoints Used:
POST /v1/calls/start- Identify caller and get contextPOST /v1/calls/end- Save context after call
You can implement this in three ways:
- Twilio Functions (serverless, easiest)
- Twilio Studio (visual flow, no code)
- External Webhooks (full control, any language)
Approach 1: Twilio Functions (Recommended)
Twilio Functions are serverless Node.js functions that run in Twilio's infrastructure. This is the easiest way to add caller memory.
Step 1: Create Caller Identification Function
In Twilio Console → Functions → Services → Create new Service → Add Function:
Function Name: identify-caller
Path: /identify-caller
Code:
exports.handler = async function(context, event, callback) {
const axios = require('axios');
// Get caller's phone number
const from = event.From;
const callSid = event.CallSid;
try {
// Call Sticky Calls API to identify caller
const response = await axios.post(
'https://api.stickycalls.com/v1/calls/start',
{
call_id: callSid,
identity_hints: {
ani: from
}
},
{
headers: {
'Authorization': `Bearer ${context.STICKY_CALLS_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
const data = response.data;
// Return context to TwiML
const twiml = new Twilio.twiml.VoiceResponse();
if (data.identity && data.identity.confidence >= 0.7) {
// High confidence - returning caller
const lastIssue = data.variables?.last_issue?.value || '';
twiml.say({
voice: 'alice'
}, `Welcome back! I see you previously called about ${lastIssue}. How can I help you today?`);
} else {
// New caller or low confidence
twiml.say({
voice: 'alice'
}, 'Welcome to our support line. How can I help you today?');
}
// Continue to next step in your IVR
twiml.redirect('/next-step');
callback(null, twiml);
} catch (error) {
console.error('Error identifying caller:', error);
// Fallback to standard greeting on error
const twiml = new Twilio.twiml.VoiceResponse();
twiml.say('Welcome to our support line.');
twiml.redirect('/next-step');
callback(null, twiml);
}
};
Step 2: Add Environment Variable
In your Twilio Function Service → Environment Variables:
STICKY_CALLS_API_KEY = sk_test_your_api_key_here
Step 3: Add Dependencies
In Service Settings → Dependencies:
axios: 1.6.0
Step 4: Configure Phone Number Webhook
In Twilio Console → Phone Numbers → Select your number → Voice Configuration:
- A call comes in: Function → [Your Service] → /identify-caller
- HTTP Method: POST
Done! Now every incoming call will identify the caller and personalize the greeting.
Approach 2: Twilio Studio (No-Code)
Twilio Studio lets you build call flows visually without writing code.
Step 1: Create Studio Flow
In Twilio Console → Studio → Create new Flow
Step 2: Add HTTP Request Widget
Drag "Make HTTP Request" widget onto canvas.
Configuration:
- Widget Name: Identify_Caller
- Method: POST
- URL:
https://api.stickycalls.com/v1/calls/start - Content Type: Application/JSON
Request Body:
{
"call_id": "{{trigger.call.CallSid}}",
"identity_hints": {
"ani": "{{trigger.call.From}}"
}
}
Headers:
Add header:
- Key: Authorization
- Value: Bearer sk_test_your_api_key_here
Step 3: Add Branch Logic
Add "Split Based On..." widget after HTTP request:
Variable to test: {{widgets.Identify_Caller.parsed.identity.confidence}}
Branches:
- >= 0.7: High confidence (returning caller)
- < 0.7: New caller
Step 4: Personalize Greeting
In high confidence branch, add "Say/Play" widget:
Welcome back! I see you called about {{widgets.Identify_Caller.parsed.variables.last_issue.value}}. How can I help?
In low confidence branch:
Welcome! How can I help you today?
Step 5: Connect to Phone Number
Publish flow, then configure phone number to use this Studio Flow.
Approach 3: External Webhooks
For full control, use your own server to handle webhooks.
Python Example (Flask)
from flask import Flask, request
from twilio.twiml.voice_response import VoiceResponse
import requests
import os
app = Flask(__name__)
STICKY_CALLS_API_KEY = os.environ['STICKY_CALLS_API_KEY']
@app.route('/voice', methods=['POST'])
def voice():
# Get caller info
from_number = request.form['From']
call_sid = request.form['CallSid']
response = VoiceResponse()
try:
# Call Sticky Calls API
api_response = requests.post(
'https://api.stickycalls.com/v1/calls/start',
json={
'call_id': call_sid,
'identity_hints': {
'ani': from_number
}
},
headers={
'Authorization': f'Bearer {STICKY_CALLS_API_KEY}',
'Content-Type': 'application/json'
},
timeout=3
)
data = api_response.json()
# Check confidence
if data.get('identity', {}).get('confidence', 0) >= 0.7:
# Returning caller
last_issue = data.get('variables', {}).get('last_issue', {}).get('value', 'a previous issue')
response.say(f"Welcome back! Last time you called about {last_issue}.")
else:
# New caller
response.say("Welcome! How can we help you today?")
except Exception as e:
print(f"Error: {e}")
# Fallback
response.say("Welcome! How can we help you?")
# Continue with your IVR logic
response.redirect('/ivr-menu')
return str(response)
if __name__ == '__main__':
app.run(debug=True)
Node.js Example (Express)
const express = require('express');
const twilio = require('twilio');
const axios = require('axios');
const app = express();
app.use(express.urlencoded({ extended: false }));
const STICKY_CALLS_API_KEY = process.env.STICKY_CALLS_API_KEY;
app.post('/voice', async (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
try {
// Call Sticky Calls API
const response = await axios.post(
'https://api.stickycalls.com/v1/calls/start',
{
call_id: req.body.CallSid,
identity_hints: {
ani: req.body.From
}
},
{
headers: {
'Authorization': `Bearer ${STICKY_CALLS_API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 3000
}
);
const data = response.data;
if (data.identity && data.identity.confidence >= 0.7) {
const lastIssue = data.variables?.last_issue?.value || 'a previous issue';
twiml.say(`Welcome back! Last time: ${lastIssue}`);
} else {
twiml.say('Welcome! How can we help?');
}
} catch (error) {
console.error('Error:', error);
twiml.say('Welcome!');
}
twiml.redirect('/ivr');
res.type('text/xml');
res.send(twiml.toString());
});
app.listen(3000);
Personalizing the Caller Experience
Once you have caller context, you can personalize in many ways:
1. Dynamic IVR Prompts
Standard greeting:
"Press 1 for billing, 2 for technical support, 3 for sales"
Personalized greeting (returning caller with billing issue):
"Welcome back! Press 1 to continue with your billing issue, or press 2 for something else"
2. Smart Routing
Route based on conversation history:
if (data.open_intents && data.open_intents.includes('billing_issue')) {
// Route directly to billing queue
twiml.dial('+15551234567'); // Billing team
} else if (data.variables?.preferred_agent) {
// Route to preferred agent
twiml.dial(data.variables.preferred_agent.value);
} else {
// Standard routing
twiml.redirect('/ivr-menu');
}
3. Skip Verification
For high-confidence callers, skip identity verification:
if (data.identity.confidence >= 0.9) {
// Skip verification, go straight to agent
twiml.say("I've verified your identity. Connecting you to an agent.");
twiml.dial('+15551234567');
} else {
// Standard verification flow
twiml.redirect('/verify-identity');
}
4. Display Context to Agents
Pass context to agent screen via call metadata:
twiml.dial({
callerId: from_number
}, function(dial) {
dial.queue({
url: '/queue-callback',
reservationSid: 'WRxxx',
workflowSid: 'WWxxx',
postWorkActivitySid: 'WAxxx',
}, 'support');
});
// In queue callback, add context as task attributes
taskAttributes = {
customer_ref: data.customer_ref,
last_issue: data.variables?.last_issue?.value,
confidence: data.identity.confidence,
call_count: data.variables?.call_count?.value || 1
};
Saving Context After Calls
At the end of every call, save context for next time.
Using Twilio Status Callbacks
Configure your phone number or TwiML app with a Status Callback URL.
Status Callback Function:
exports.handler = async function(context, event, callback) {
const axios = require('axios');
// Only process completed calls
if (event.CallStatus !== 'completed') {
return callback(null, 'OK');
}
const callSid = event.CallSid;
const from = event.From;
// Get call details from your system
// (You'd typically fetch this from a database or session storage)
const callSummary = event.CallSummary || 'Customer called for support';
const resolution = event.Resolution || '';
const intent_status = event.IntentStatus || 'resolved';
try {
await axios.post(
'https://api.stickycalls.com/v1/calls/end',
{
call_id: callSid,
customer_ref: event.CustomerRef, // From start call response
intent: event.Intent || 'general_inquiry',
intent_status: intent_status,
variables: {
last_issue: callSummary,
resolution: resolution,
call_date: new Date().toISOString(),
agent_notes: event.AgentNotes || ''
}
},
{
headers: {
'Authorization': `Bearer ${context.STICKY_CALLS_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
console.log('Context saved successfully');
} catch (error) {
console.error('Error saving context:', error);
}
callback(null, 'OK');
};
Advanced Patterns
Multi-Signal Identification
Combine phone number with customer ID for higher confidence:
const response = await axios.post(
'https://api.stickycalls.com/v1/calls/start',
{
call_id: callSid,
identity_hints: {
ani: from,
external_ids: {
customer_id: customerIdFromDatabase
}
}
},
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
);
Confidence Thresholds
Use different personalization levels based on confidence:
if (confidence >= 0.9) {
// Very high - skip verification
twiml.say("Welcome back! Connecting you now.");
twiml.dial(agent_number);
} else if (confidence >= 0.7) {
// High - personalize but verify
twiml.say("Welcome back! For security, please verify your account.");
twiml.redirect('/quick-verify');
} else if (confidence >= 0.3) {
// Medium - mention possibility
twiml.say("It looks like you may have called before. Let's verify your account.");
twiml.redirect('/full-verify');
} else {
// Low - treat as new caller
twiml.redirect('/new-caller-flow');
}
Error Handling
Always provide fallbacks:
try {
const response = await callStickyCallsAPI();
// Use response
} catch (error) {
if (error.response?.status === 429) {
// Rate limit - retry after delay
await sleep(1000);
return retryCall();
} else if (error.code === 'ECONNABORTED') {
// Timeout - use default flow
console.error('API timeout, using default flow');
} else {
console.error('API error:', error);
}
// Always provide fallback greeting
twiml.say('Welcome! How can we help you today?');
}
Testing Your Implementation
1. Test with Known Number
Call from your mobile phone:
First call: Should receive standard greeting Second call (after saving context): Should receive personalized greeting
2. Check Confidence Scores
Log the confidence scores to understand matching quality:
console.log(`Caller ${from}: Confidence ${data.identity.confidence}`);
Typical scores:
- 0.9-1.0: Same phone, called recently, high certainty
- 0.7-0.9: Same phone, called before, good match
- 0.3-0.7: Possible match, verify recommended
- 0.0-0.3: New caller or very old call
3. Test Error Scenarios
- Disconnect internet to test timeout handling
- Use invalid API key to test auth errors
- Call with null/undefined values
4. Debug Logs
Enable detailed logging:
console.log('API Request:', {
call_id: callSid,
from: from
});
console.log('API Response:', data);
Metrics & ROI
Expected improvements after implementing caller memory:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Average Handle Time | 8 min | 5.2 min | -35% |
| Repeat Questions | 4.2 per call | 1.8 per call | -57% |
| Customer Satisfaction | 72% | 89% | +17 points |
| First Call Resolution | 68% | 84% | +16 points |
| Agent Productivity | 6 calls/hour | 8.5 calls/hour | +42% |
Real customer example:
"We added caller memory to our Twilio support line. Within the first week, our average handle time dropped from 7.5 minutes to 4.8 minutes. Customers love not having to repeat themselves."
— Contact Center Manager, E-commerce Company
Cost savings calculation:
- Before: 1,000 calls/day × 8 min = 8,000 agent minutes
- After: 1,000 calls/day × 5.2 min = 5,200 agent minutes
- Savings: 2,800 minutes/day = 46.7 hours/day
- Monthly savings: ~940 hours at $20/hour = $18,800/month
Common Issues & Solutions
Issue: API Timeout
Symptoms: Slow response times, timeout errors
Solutions:
- Increase timeout to 5 seconds
- Implement caching for frequently called numbers
- Use async/await properly
Issue: Low Confidence Scores
Symptoms: Confidence always < 0.5 even for repeat callers
Solutions:
- Ensure phone numbers are in E.164 format (+14155551234)
- Save customer_ref from start call and use in end call
- Include external_ids for better matching
Issue: Context Not Saving
Symptoms: Context not available on next call
Solutions:
- Verify customer_ref from start call is used in end call
- Check API key has write permissions
- Ensure variables object is properly formatted JSON
Next Steps
Now that you've added caller memory to Twilio:
- Twilio Flex Integration - Full contact center implementation
- Generic IVR Guide - Platform-agnostic patterns
- Reduce AHT Guide - Measure business impact
- API Reference - Complete API documentation
- Best Practices - Production optimization tips
Questions? Contact support or view API docs.
Ready to start? Sign up for free →