Best Practices Guide
Production-ready patterns and recommendations for Sticky Calls API.
Table of Contents
- Confidence Scoring
- Variable Management
- Intent Tracking
- Error Handling
- Performance
- Security
- Testing
- Common Patterns
Confidence Scoring
Understanding Confidence Levels
[━━━━━━━━━━━━━━━━━━━━] very_high (≥0.9) Auto-reuse with highest confidence
[━━━━━━━━━━━━━━━━░░░░] high (≥0.7) Auto-reuse with high confidence
[━━━━━━━━━━░░░░░░░░░░] medium (≥0.5) Auto-reuse recommended
[━━━━░░░░░░░░░░░░░░░░] low (<0.5) Confirm before using
Interpreting Recommendations
| Recommendation | Confidence | Action | Example Greeting |
|---|---|---|---|
reuse | ≥0.5 | Use context automatically | "Welcome back, John! I see your account balance is $1,234." |
confirm | 0.3-0.5 | Ask to confirm identity first | "Hello! Could you confirm your name for me?" |
ignore | <0.3 | Treat as new customer | "Hello! Thanks for calling. May I have your name?" |
Maximizing Confidence
1. Always Send External IDs
// ✅ GOOD: Send CRM ID for +0.4 confidence boost
{
"call_id": "call_123",
"identity_hints": {
"ani": "+14155551234",
"external_ids": {
"crm_id": "crm_12345",
"account_number": "ACC-789"
}
}
}
// Confidence: Mobile (0.5) + External ID (0.4) = 0.9 (very_high)
// ❌ BAD: Missing external IDs
{
"call_id": "call_123",
"identity_hints": {
"ani": "+14155551234"
}
}
// Confidence: Mobile (0.5) only = 0.5 (medium)
2. Include Telco Data When Available
// ✅ GOOD: Include telco data from Twilio Lookup
{
"call_id": "call_123",
"identity_hints": {
"ani": "+14155551234"
},
"telco": {
"line_type": "mobile" // +0.5 for mobile
}
}
// ❌ BAD: Unknown line type = lower confidence
{
"call_id": "call_123",
"identity_hints": {
"ani": "+14155551234"
}
// No telco data = defaults to unknown (0.0)
}
3. Don't Expose Confidence to Customers
// ❌ BAD: Exposing confidence to customer
"I'm 70% confident you're John Doe..."
// ✅ GOOD: Use confidence internally, natural greeting
if (confidence >= 0.7) {
greet("Welcome back, John!")
} else if (confidence >= 0.3) {
greet("Hello! Could you confirm your name for me?")
} else {
greet("Hello! May I have your name?")
}
Variable Management
What to Save
✅ Save:
- Identity: name, email, phone
- Account: account_number, balance, status, tier
- Preferences: language, timezone, communication_preference
- Recent activity: last_order_date, last_payment, last_issue
- Resolution notes: "Refunded $45.99 on 2026-02-01"
❌ Don't Save:
- Sensitive data: SSN, credit card numbers, passwords
- Temporary data: one-time codes, session IDs
- Redundant data: things already in your CRM
- Unstructured data: entire transcripts (save summary instead)
Variable Naming Conventions
// ✅ GOOD: Clear, consistent naming
{
"variables": {
"name": "John Doe",
"email": "john@example.com",
"account_balance": "$1,234.56",
"vip_tier": "Gold",
"preferred_language": "English",
"last_payment_date": "2026-02-01"
}
}
// ❌ BAD: Inconsistent, unclear naming
{
"variables": {
"n": "John Doe",
"mail": "john@example.com",
"bal": "1234.56",
"vip": "G",
"lang": "EN",
"lastpay": "2/1/26"
}
}
Variable Limits
- Maximum 100 variables per customer
- Key max length: 128 characters
- Value max length: 1,024 characters
- Default TTL: 30 days (auto-cleanup)
Best practice: Save meaningful variables, not everything. Quality > quantity.
Intent Tracking
When to Mark Intent as Open
✅ Mark intent_is_open: true when:
- Issue not fully resolved
- Waiting on something (refund processing, callback scheduled)
- Multi-call issue (research needed, escalation)
- Customer explicitly says "I'll call back about this"
// ✅ GOOD: Unresolved issue
{
"intent": "refund_request",
"intent_is_open": true, // Still pending
"variables": {
"refund_amount": "$99.99",
"refund_status": "processing",
"refund_submitted_date": "2026-02-01"
}
}
❌ Don't mark open when:
- Issue resolved this call
- Customer satisfied and doesn't need follow-up
- One-time question answered
// ✅ GOOD: Resolved issue
{
"intent": "billing_inquiry",
"intent_is_open": false, // Resolved
"variables": {
"inquiry_resolution": "Explained charge - customer satisfied"
}
}
Handling Open Intents
On next call:
if (startResponse.match?.open_intents?.length > 0) {
const openIntent = startResponse.match.open_intents[0];
// ✅ GOOD: Proactive follow-up
greet(`Welcome back, ${name}! I see you called about ${openIntent.intent}. ` +
`Has that been resolved, or would you like me to check on it?`);
}
// ❌ BAD: Ignoring open intents
greet(`Welcome back, ${name}! What can I help you with today?`);
// Customer has to explain issue again
Intent Naming
Use consistent, descriptive names:
// ✅ GOOD: Clear intent names
"billing_dispute"
"refund_request"
"technical_support"
"account_upgrade"
"payment_arrangement"
// ❌ BAD: Vague or inconsistent
"issue"
"problem"
"call1"
"misc"
Error Handling
Retry Logic
429 Too Many Requests:
async function callWithRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429) {
// Rate limited - wait and retry
const waitTime = Math.pow(2, i) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
throw error; // Other errors - fail immediately
}
}
throw new Error('Max retries exceeded');
}
500 Internal Server Error:
try {
const response = await startCall(callId, ani);
return response;
} catch (error) {
if (error.response?.status === 500) {
// Log error but proceed without context
logger.error('Sticky Calls API error', error);
return { match: null, confidence: null };
}
throw error;
}
Graceful Degradation
Always have a fallback:
// ✅ GOOD: Graceful degradation
let customerContext = null;
try {
const response = await stickyCallsAPI.start(callId, ani);
customerContext = response.match;
} catch (error) {
logger.error('Failed to fetch customer context', error);
// Continue without context - agent can collect info manually
}
// Proceed with call either way
if (customerContext) {
greet(`Welcome back, ${customerContext.variables.name}!`);
} else {
greet(`Hello! May I have your name?`);
}
Error Status Handling
function handleStickyCallsError(error) {
const status = error.response?.status;
switch (status) {
case 400:
// Bad request - check your request format
logger.error('Invalid request to Sticky Calls', error.response.data);
break;
case 401:
// Invalid API key - critical, alert immediately
logger.critical('Sticky Calls API key invalid!', error);
alertTeam('Invalid API key - check configuration');
break;
case 402:
// Insufficient credits - alert billing team
logger.error('Sticky Calls credits exhausted', error);
alertBillingTeam('Add credits to Sticky Calls account');
break;
case 429:
// Rate limit - retry with backoff
return 'retry';
case 500:
// Server error - log and proceed without context
logger.error('Sticky Calls server error', error);
break;
}
return 'fail';
}
Performance
Call ID Generation
// ✅ GOOD: Unique, collision-resistant
function generateCallId() {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `call_${timestamp}_${random}`;
}
// Example: call_1738442400000_x7k2p9
// ❌ BAD: Collision-prone
function badCallId() {
return `call_${Date.now()}`;
}
// Different calls in same millisecond = collision!
Idempotency
Use Idempotency-Key header for /calls/end:
// ✅ GOOD: Safe retries with idempotency key
await fetch('/v1/calls/end', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Idempotency-Key': `end_${callId}_attempt1`, // Same key on retry
'Content-Type': 'application/json'
},
body: JSON.stringify({...})
});
// If network fails and you retry with same key:
// - First request: Processes and saves context
// - Retry request: Returns cached response, no duplicate save
Parallel Requests
// ❌ BAD: Sequential (slow)
for (const call of calls) {
await startCall(call.id, call.ani);
}
// ✅ GOOD: Parallel (fast)
await Promise.all(
calls.map(call => startCall(call.id, call.ani))
);
Timeouts
// ✅ GOOD: Set reasonable timeouts
const response = await axios.post('/v1/calls/start', data, {
timeout: 5000 // 5 second timeout
});
// Don't wait forever if API is slow
Security
API Key Storage
// ✅ GOOD: Environment variable
const API_KEY = process.env.STICKY_CALLS_API_KEY;
// ❌ BAD: Hardcoded
const API_KEY = 'sk_prod_abc123...'; // NEVER do this!
// ❌ BAD: Committed to git
// .env file in repository // NEVER commit .env files!
API Key Rotation
- Generate new API key
- Update environment variable in all servers
- Deploy new configuration
- Revoke old API key
- Monitor for 401 errors
Never revoke before deploying new key!
Logging
// ✅ GOOD: Redact sensitive data
logger.info('Call started', {
call_id: callId,
ani: '***redacted***', // Don't log PII
confidence: response.confidence
});
// ❌ BAD: Logging PII
logger.info('Call started', {
call_id: callId,
ani: '+14155551234', // Phone number = PII
customer_name: 'John Doe' // Name = PII
});
HTTPS Only
// ✅ GOOD: Always HTTPS
const API_URL = 'https://api.stickycalls.com';
// ❌ BAD: HTTP (insecure)
const API_URL = 'http://...'; // API keys sent in plaintext!
Testing
Test vs Production Keys
// ✅ GOOD: Use test keys in development
const API_KEY = process.env.NODE_ENV === 'production'
? process.env.STICKY_CALLS_PROD_KEY // sk_prod_...
: process.env.STICKY_CALLS_TEST_KEY; // sk_test_...
// Test keys:
// - Separate credit balance
// - Separate data
// - No production impact
Integration Tests
describe('Sticky Calls Integration', () => {
it('should handle new customer', async () => {
const response = await stickyCallsAPI.start('test_new_001', '+19999999999');
expect(response.match).toBeNull();
expect(response.confidence).toBeNull();
expect(response.billable).toBe(true);
});
it('should recognize returning customer', async () => {
// Create customer
await stickyCallsAPI.start('test_001', '+14155551234');
await stickyCallsAPI.end('test_001', 'cust_test', {name: 'Test'}, 'test', false);
// Second call should match
const response = await stickyCallsAPI.start('test_002', '+14155551234');
expect(response.match).not.toBeNull();
expect(response.match.customer_ref).toBe('cust_test');
expect(response.confidence).toBeGreaterThan(0);
expect(response.billable).toBe(false);
});
});
Common Patterns
Pattern 1: Basic Call Flow
class CallHandler {
async handleCallStart(callId, ani, externalIds) {
// 1. Get customer context
const context = await stickyCallsAPI.start(callId, ani, externalIds);
// 2. Greet based on confidence
if (context.confidence >= 0.5) {
this.greet(`Welcome back, ${context.match.variables.name}!`);
// 3. Reference open intents
if (context.match.open_intents.length > 0) {
this.mention_open_intents(context.match.open_intents);
}
} else {
this.greet('Hello! May I have your name?');
}
// 4. Store context for end of call
this.currentCall = {
callId,
customerRef: context.match?.customer_ref || null,
variables: context.match?.variables || {}
};
}
async handleCallEnd(intent, intentIsOpen) {
// 5. Collect new information during call
const updatedVariables = {
...this.currentCall.variables,
...this.collectedInfo
};
// 6. Save context
await stickyCallsAPI.end(
this.currentCall.callId,
this.currentCall.customerRef || this.generateCustomerRef(),
updatedVariables,
intent,
intentIsOpen
);
}
}
Pattern 2: Multi-Channel Consistency
// Same customer across phone, chat, email
class CustomerContext {
async identify(channel, identifier, externalIds) {
if (channel === 'phone') {
return await stickyCallsAPI.start(callId, identifier, externalIds);
}
// For other channels, use external IDs only
// Phone is the "source of truth" for ANI matching
}
async save(callId, customerRef, data, intent, isOpen) {
await stickyCallsAPI.end(callId, customerRef, data, intent, isOpen);
// Also sync to your CRM
await this.crmSync(customerRef, data);
}
}
Pattern 3: Confidence-Based Authentication
async function authenticateCustomer(stickyContext) {
const confidence = stickyContext.confidence || 0;
if (confidence >= 0.7) {
// High confidence - minimal authentication
return {
authenticated: true,
method: 'caller_id',
customer: stickyContext.match.customer_ref
};
} else if (confidence >= 0.3) {
// Medium confidence - ask for verification
const verified = await askSecurityQuestion();
return {
authenticated: verified,
method: 'security_question',
customer: verified ? stickyContext.match.customer_ref : null
};
} else {
// Low/no confidence - full authentication
const credentials = await askFullAuth();
return {
authenticated: credentials.valid,
method: 'full_auth',
customer: credentials.customerId
};
}
}
Pattern 4: Intent Resolution Workflow
async function handleOpenIntent(openIntent) {
const intent = openIntent.intent;
const attempts = openIntent.attempt_count;
// Proactively ask about it
const response = await ask(
`I see you called ${attempts} time(s) about ${intent}. ` +
`Has that been resolved, or would you like me to help with it?`
);
if (response === 'resolved') {
// Mark as resolved on this call
return {
intent: intent,
intent_is_open: false,
variables: {
[`${intent}_resolution_date`]: new Date().toISOString().split('T')[0],
[`${intent}_resolution_method`]: 'customer_confirmed_resolved'
}
};
} else {
// Still open, increment attempts
return {
intent: intent,
intent_is_open: true,
variables: {
[`${intent}_last_followup`]: new Date().toISOString().split('T')[0]
}
};
}
}
Checklist
Pre-Production
- API key stored in environment variable (not hardcoded)
- Using HTTPS (not HTTP)
- Error handling implemented for all status codes
- Retry logic for 429 (rate limits)
- Graceful degradation for API failures
- Timeout configured (5-10 seconds recommended)
- Logging implemented (with PII redaction)
- Test coverage for happy path and error cases
- Unique call_id generation (timestamp + random)
- Idempotency-Key header for /calls/end
Production Monitoring
- Monitor 401 errors (invalid API key)
- Monitor 402 errors (insufficient credits)
- Monitor 429 errors (rate limits)
- Monitor 500 errors (server errors)
- Track billable vs non-billable calls
- Monitor average confidence scores
- Track open intent resolution rate
- Alert on API availability issues
Next Steps:
- Read Quick Start Guide for getting started
- Review OpenAPI Spec for full API reference
- Check AI Agent Guide for AI integration
Support: nate@bananaintelligence.ai