Skip to main content

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 context
  • POST /v1/calls/end - Save context after call

You can implement this in three ways:

  1. Twilio Functions (serverless, easiest)
  2. Twilio Studio (visual flow, no code)
  3. External Webhooks (full control, any language)

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:

MetricBeforeAfterImprovement
Average Handle Time8 min5.2 min-35%
Repeat Questions4.2 per call1.8 per call-57%
Customer Satisfaction72%89%+17 points
First Call Resolution68%84%+16 points
Agent Productivity6 calls/hour8.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:


Questions? Contact support or view API docs.

Ready to start? Sign up for free →