Skip to main content

Best Practices Guide

Production-ready patterns and recommendations for Sticky Calls API.


Table of Contents

  1. Confidence Scoring
  2. Variable Management
  3. Intent Tracking
  4. Error Handling
  5. Performance
  6. Security
  7. Testing
  8. 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

RecommendationConfidenceActionExample Greeting
reuse≥0.5Use context automatically"Welcome back, John! I see your account balance is $1,234."
confirm0.3-0.5Ask to confirm identity first"Hello! Could you confirm your name for me?"
ignore<0.3Treat 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 &gt;= 0.7) {
greet("Welcome back, John!")
} else if (confidence &gt;= 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

  1. Generate new API key
  2. Update environment variable in all servers
  3. Deploy new configuration
  4. Revoke old API key
  5. 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 &gt;= 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 &gt;= 0.7) {
// High confidence - minimal authentication
return {
authenticated: true,
method: 'caller_id',
customer: stickyContext.match.customer_ref
};
} else if (confidence &gt;= 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:

Support: nate@bananaintelligence.ai