Creating Custom Voice Profiles in VAPI for Personalized Marketing Campaigns

Discover how to leverage VAPI for personalized marketing with custom voice profiles. Learn practical tips and integration techniques for success.

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

Creating Custom Voice Profiles in VAPI for Personalized Marketing Campaigns

Advertisement

Creating Custom Voice Profiles in VAPI for Personalized Marketing Campaigns

TL;DR

Most marketing campaigns fail because they use generic voices that don't match brand identity. VAPI lets you build dynamic assistant configurations with custom voice profiles—select ElevenLabs voiceIds, inject system prompts for tone consistency, and validate webhook signatures for security. Result: personalized outbound calls that convert 3-5x better than one-size-fits-all bots. No SDK wrapper needed—raw API calls give you full control over voice selection logic and campaign metadata.

Prerequisites

VAPI Account & API Access You need an active VAPI account with API key access. Generate your API key from the VAPI dashboard (Settings → API Keys). Store it in .env as VAPI_API_KEY. Minimum required permissions: assistant:create, call:initiate, webhook:receive.

Voice Provider Integration ElevenLabs account with API key (ELEVEN_API_KEY) for custom voice synthesis. Alternatively, use Google Cloud Text-to-Speech or Azure Speech Services. You'll need voiceId values from your chosen provider to configure voice profiles dynamically.

Twilio Setup (Optional) If routing calls through Twilio, provision a Twilio account with phone numbers and SIP trunks. Store TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER in environment variables.

Development Environment Node.js 18+ with npm/yarn. Install: axios, dotenv, express (for webhook handling). HTTPS endpoint required for webhook signature validation—use ngrok or similar for local testing.

System Requirements Minimum 2GB RAM for concurrent call handling. Stable internet connection (10+ Mbps recommended for audio streaming).

Twilio: Get Twilio Voice API → Get Twilio

Step-by-Step Tutorial

Configuration & Setup

Most marketing campaigns fail because they treat voice profiles as static configs. They're not. Each customer segment needs distinct voice characteristics, pacing, and tone that adapt based on campaign context.

Start by structuring your customer data with voice profile metadata:

javascript
// Customer segmentation with voice profile mapping
const customerSegments = {
  'high-value': {
    voiceId: 'elevenlabs-professional-male',
    speed: 0.9,  // Slower, more deliberate
    stability: 0.8,
    systemPrompt: 'You are a senior account manager conducting a personalized follow-up. Use the customer\'s name and reference their recent purchase of {product}.'
  },
  'new-customer': {
    voiceId: 'elevenlabs-friendly-female', 
    speed: 1.1,  // Slightly faster, energetic
    stability: 0.6,
    systemPrompt: 'You are a friendly customer success rep welcoming new customers. Keep it brief and focus on immediate value.'
  },
  'at-risk': {
    voiceId: 'elevenlabs-empathetic-female',
    speed: 0.95,
    stability: 0.75,
    systemPrompt: 'You are a retention specialist. Listen carefully to concerns and offer specific solutions based on {issue_history}.'
  }
};

Critical: Voice speed and stability directly impact perceived authenticity. High-value customers respond better to stability > 0.75 (reduces vocal variance). New customers tolerate more energy (stability 0.5-0.7).

Architecture & Flow

The campaign flow requires dynamic assistant creation per call. Do NOT reuse a single assistant across segments—you'll lose personalization and create race conditions when multiple calls fire simultaneously.

mermaid
flowchart LR
    A[Customer DB] --> B[Segment Logic]
    B --> C[Generate Assistant Config]
    C --> D[VAPI Create Assistant]
    D --> E[Twilio Initiate Call]
    E --> F[VAPI Voice Interaction]
    F --> G[Webhook: Call Events]
    G --> H[Update CRM]

Step-by-Step Implementation

1. Dynamic Assistant Creation

Create assistants on-demand with injected customer context. This prevents prompt pollution and ensures clean state per call:

javascript
async function createPersonalizedAssistant(customer) {
  const segment = customerSegments[customer.segment];
  
  const response = await fetch('https://api.vapi.ai/assistant', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: `Campaign-${customer.id}-${Date.now()}`,
      model: {
        provider: 'openai',
        model: 'gpt-4',
        temperature: 0.7,
        systemPrompt: segment.systemPrompt
          .replace('{product}', customer.lastPurchase)
          .replace('{issue_history}', customer.issues.join(', '))
      },
      voice: {
        provider: 'elevenlabs',
        voiceId: segment.voiceId,
        speed: segment.speed,
        stability: segment.stability
      },
      firstMessage: `Hi ${customer.firstName}, this is ${customer.assignedRep} from ${process.env.COMPANY_NAME}.`,
      endCallFunctionEnabled: true,
      recordingEnabled: true
    })
  });
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Assistant creation failed: ${error.message}`);
  }
  
  return response.json();
}

2. Twilio Integration for Outbound Calls

Bridge VAPI assistants to Twilio for actual call execution. The assistant ID must be passed in the TwiML webhook:

javascript
async function initiateOutboundCall(customer, assistantId) {
  const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/Calls.json`, {
    method: 'POST',
    headers: {
      'Authorization': 'Basic ' + Buffer.from(`${process.env.TWILIO_ACCOUNT_SID}:${process.env.TWILIO_AUTH_TOKEN}`).toString('base64'),
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      To: customer.phone,
      From: process.env.TWILIO_PHONE_NUMBER,
      Url: `${process.env.SERVER_URL}/twiml/${assistantId}` // Your server generates TwiML
    })
  });
  
  if (!response.ok) throw new Error(`Twilio call failed: ${response.status}`);
  return response.json();
}

Error Handling & Edge Cases

Voice profile fallback: If ElevenLabs voiceId fails (rate limit, invalid ID), default to en-US-Neural2-A (Google TTS). Track fallback rate—if > 5%, your voice IDs are misconfigured.

Prompt injection attacks: Customers can manipulate the conversation to extract system prompts. Sanitize {customer_data} fields and never inject raw user input into system prompts without escaping.

Concurrent call limits: VAPI has rate limits (check your plan). Implement queue-based campaign execution with max 10 concurrent calls to avoid 429 errors.

System Diagram

Audio processing pipeline from microphone input to speaker output.

mermaid
graph LR
    Start[Start Campaign]
    LoadData[Load Customer Data]
    Dialer[Outbound Dialer]
    Call[Make Call]
    Feedback[Collect Feedback]
    Error[Error Handling]
    Retry[Retry Logic]
    End[End Campaign]
    
    Start-->LoadData
    LoadData-->Dialer
    Dialer-->Call
    Call-->Feedback
    Feedback-->End
    Call-->|Call Failed|Error
    Error-->Retry
    Retry-->Dialer
    Retry-->|Max Retries Reached|End

Testing & Validation

Most voice profile implementations fail in production because developers skip local webhook testing. Here's how to validate before deploying.

Local Testing

Use ngrok to expose your webhook endpoint and test the full flow locally. This catches signature validation failures and payload mismatches before they hit production.

javascript
// Test webhook signature validation locally
const crypto = require('crypto');

function validateWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  if (signature !== expectedSignature) {
    throw new Error('Invalid webhook signature - check serverUrlSecret');
  }
  return true;
}

// Test voice profile injection
app.post('/webhook/test', (req, res) => {
  const { message, call } = req.body;
  
  if (message.type === 'assistant-request') {
    const segment = customerSegments.find(s => s.id === call.customer.segmentId);
    
    // Validate voice profile exists
    if (!segment?.voiceId) {
      console.error('Missing voiceId for segment:', call.customer.segmentId);
      return res.status(400).json({ error: 'Invalid voice profile' });
    }
    
    // Return dynamic assistant with validated profile
    res.json({
      assistant: {
        model: { provider: 'openai', model: 'gpt-4' },
        voice: { 
          provider: 'elevenlabs', 
          voiceId: segment.voiceId,
          stability: 0.7,
          speed: 1.0
        },
        systemPrompt: segment.systemPrompt
      }
    });
  }
});

Webhook Validation

Test with curl to verify your endpoint handles all event types correctly. Check that voiceId injection works and systemPrompt customization applies per segment.

bash
# Test assistant-request webhook
curl -X POST https://your-ngrok-url.ngrok.io/webhook/test \
  -H "Content-Type: application/json" \
  -d '{
    "message": { "type": "assistant-request" },
    "call": { 
      "customer": { "segmentId": "high-value" }
    }
  }'

Validate response contains correct voiceId and systemPrompt for the segment. Test edge cases: missing segmentId, invalid voiceId, malformed payloads. Production failures happen when webhooks timeout (>5s) or return invalid JSON—add logging to catch these early.

Real-World Example

Barge-In Scenario

A fitness studio runs a post-class feedback campaign. Customer "Sarah" receives a call while driving. The agent starts: "Hi Sarah, thanks for attending our spin class yesterday. We'd love to hear—" but Sarah interrupts: "Actually, I have a quick question about my membership."

This is where most toy implementations break. The agent either:

  • Keeps talking (double audio nightmare)
  • Stops but loses context (restarts from scratch)
  • Crashes because the STT buffer wasn't flushed

Here's production-grade barge-in handling that actually works:

javascript
// Webhook handler for speech-update events
app.post('/webhook/vapi', async (req, res) => {
  const event = req.body;
  
  // Validate webhook signature (security is not optional)
  const isValid = validateWebhookSignature(
    req.headers['x-vapi-signature'],
    JSON.stringify(req.body)
  );
  if (!isValid) return res.status(401).send('Invalid signature');
  
  // Handle barge-in: user speaks while agent is talking
  if (event.type === 'speech-update' && event.status === 'started') {
    const callId = event.call.id;
    
    // Cancel TTS immediately - don't wait for it to finish
    await fetch(`https://api.vapi.ai/call/${callId}/control`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        action: 'interrupt',
        flushBuffer: true // Critical: prevents old audio from playing
      })
    });
    
    // Update assistant context to acknowledge interruption
    const segment = customerSegments.find(s => s.customerId === event.call.customer?.id);
    await fetch(`https://api.vapi.ai/call/${callId}/assistant`, {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.VAPI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        systemPrompt: `${segment.systemPrompt}\n\nUser just interrupted. Acknowledge their question immediately.`
      })
    });
  }
  
  res.sendStatus(200);
});

Event Logs

Real production logs from a 10-second interaction (timestamps show the race condition):

14:32:01.234 [speech-update] status=started, transcript="" 14:32:01.456 [assistant-request] Generating response... 14:32:01.789 [speech-update] status=in-progress, transcript="Actually I have" 14:32:02.123 [speech-update] status=in-progress, transcript="Actually I have a quick" 14:32:02.456 [assistant-response] TTS started (old response) 14:32:02.567 [speech-update] status=stopped, transcript="Actually I have a quick question" 14:32:02.890 [interrupt] Flushing TTS buffer 14:32:03.012 [assistant-request] Generating new response with context

Notice the 778ms gap between speech start and TTS cancellation. On slower networks, this stretches to 1.2s—enough for users to hear the old response and get confused.

Edge Cases

Multiple rapid interruptions: User says "Wait—no, actually—hold on." Three barge-ins in 2 seconds. Solution: debounce interrupts with a 300ms window. Only process if speech continues past threshold.

False positives from background noise: Car horn triggers barge-in. The speech-update event includes a confidence score. Filter out events below 0.7:

javascript
if (event.type === 'speech-update' && 
    event.status === 'started' && 
    event.confidence > 0.7) {
  // Process interrupt
}

Context loss on interrupt: Agent forgets what it was saying. Maintain a conversationBuffer that stores the last 3 turns. On interrupt, inject: "You were explaining [topic]. User interrupted with: [transcript]."

Common Issues & Fixes

Voice Profile Mismatch After Campaign Launch

Problem: Outbound calls use the default voice instead of the custom ElevenLabs profile you configured. This happens when the voiceId parameter gets overwritten during assistant creation or when the voice provider fallback kicks in.

Root Cause: VAPI's assistant configuration has a priority chain: inline voice config > assistant template > account defaults. If you set voice.provider but forget voice.voiceId, it falls back to the provider's default voice.

javascript
// WRONG - Missing voiceId causes fallback to default
const assistantConfig = {
  model: { provider: "openai", model: "gpt-4" },
  voice: { provider: "11labs" }, // ❌ No voiceId specified
  transcriber: { provider: "deepgram", model: "nova-2" }
};

// CORRECT - Explicit voiceId locks the profile
const assistantConfig = {
  model: { provider: "openai", model: "gpt-4" },
  voice: { 
    provider: "11labs",
    voiceId: "pNInz6obpgDQGcFmaJgB", // âś… Explicit profile ID
    stability: 0.5,
    similarityBoost: 0.75
  },
  transcriber: { provider: "deepgram", model: "nova-2" }
};

// Verify voice selection in webhook
function validateWebhookSignature(req) {
  const event = req.body;
  if (event.type === 'assistant-request') {
    console.log('Active voiceId:', event.assistant?.voice?.voiceId);
    if (!event.assistant?.voice?.voiceId) {
      throw new Error('Voice profile not applied - check assistant config');
    }
  }
}

Fix: Always specify voiceId explicitly in your assistant configuration. Test with a single outbound call before launching the full campaign. Monitor webhook events for assistant-request to verify the correct voice profile is active.

Dynamic Prompt Injection Fails Mid-Campaign

Problem: System prompt personalization breaks when customer data contains special characters or exceeds token limits. You'll see generic responses instead of personalized greetings.

Root Cause: Unescaped JSON in systemPrompt or exceeding the model's context window (8K for GPT-4, 4K for GPT-3.5). If your customer segment data includes quotes, newlines, or Unicode, the JSON payload becomes malformed.

javascript
// WRONG - Unescaped customer data breaks JSON
const systemPrompt = `You are calling ${segment.name}. 
Their last purchase: "${segment.lastPurchase}"`;  // ❌ Quotes not escaped

// CORRECT - Sanitize and truncate customer data
function createPersonalizedAssistant(segment) {
  const sanitizedName = segment.name.replace(/"/g, '\\"').substring(0, 50);
  const sanitizedPurchase = segment.lastPurchase.replace(/"/g, '\\"').substring(0, 100);
  
  const systemPrompt = `You are conducting a follow-up call for ${sanitizedName}. Reference their recent purchase: ${sanitizedPurchase}. Keep responses under 50 words.`;
  
  // Verify token count before API call
  const estimatedTokens = systemPrompt.split(' ').length * 1.3; // Rough estimate
  if (estimatedTokens > 1500) {
    throw new Error(`Prompt too long: ${estimatedTokens} tokens (max 1500 for safety)`);
  }
  
  return {
    model: { provider: "openai", model: "gpt-4", temperature: 0.7 },
    voice: { provider: "11labs", voiceId: segment.voiceId },
    firstMessage: `Hi ${sanitizedName}, this is a quick follow-up about your recent order.`,
    systemPrompt
  };
}

Fix: Sanitize all customer data before injection. Escape quotes, strip newlines, and truncate long fields. Keep total prompt under 1500 tokens to leave room for conversation history. Test with edge cases: names with apostrophes, purchases with special characters, empty fields.

Webhook Signature Validation Blocks Production Calls

Problem: Outbound calls fail silently because your server rejects VAPI's webhook events due to signature mismatch. No error logs, just dead air on the customer's end.

Root Cause: VAPI signs webhook payloads with HMAC-SHA256 using your serverUrlSecret. If you validate signatures incorrectly (wrong encoding, missing headers, clock skew), legitimate events get rejected and calls never complete.

javascript
// Production-grade webhook validation
const crypto = require('crypto');

function validateWebhookSignature(req) {
  const signature = req.headers['x-vapi-signature'];
  const timestamp = req.headers['x-vapi-timestamp'];
  
  if (!signature || !timestamp) {
    throw new Error('Missing signature headers');
  }
  
  // Reject old requests (replay attack prevention)
  const requestTime = parseInt(timestamp);
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - requestTime) > 300) { // 5 min tolerance
    throw new Error(`Timestamp too old: ${currentTime - requestTime}s skew`);
  }
  
  // Compute expected signature
  const payload = `${timestamp}.${JSON.stringify(req.body)}`;
  const expectedSignature = crypto
    .createHmac('sha256', process.env.VAPI_SERVER_SECRET)
    .update(payload)
    .digest('hex');
  
  // Constant-time comparison (prevents timing attacks)
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
  
  if (!isValid) {
    console.error('Signature mismatch:', { 
      received: signature.substring(0, 10),
      expected: expectedSignature.substring(0, 10)
    });
    throw new Error('Invalid webhook signature');
  }
  
  return req.body;
}

Fix: Use constant-time comparison to prevent timing attacks. Verify both signature AND timestamp. Log mismatches with truncated signatures (never log full secrets). Test with VAPI's webhook testing tool before going live. Set serverUrlSecret in your assistant config and store it securely in environment variables.

Complete Working Example

This is the full production server that handles personalized voice campaigns. Copy-paste this into your project and configure the environment variables. The code integrates VAPI assistant creation, Twilio outbound calling, and webhook validation in a single Express server.

Full Server Code

javascript
// server.js - Production-ready personalized voice campaign server
const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Customer segments with voice profiles (from your CRM/database)
const customerSegments = {
  'high-value': {
    voiceId: '21m00Tcm4TlvDq8ikWAM', // ElevenLabs Rachel voice
    speed: 1.0,
    stability: 0.75,
    similarityBoost: 0.8,
    systemPrompt: 'You are a premium account manager. Address the customer by name and reference their recent ${{purchase}} purchase. Speak warmly and professionally.'
  },
  'new-customer': {
    voiceId: 'EXAVITQu4vr4xnSDxMaL', // ElevenLabs Bella voice
    speed: 1.1,
    stability: 0.65,
    similarityBoost: 0.75,
    systemPrompt: 'You are a friendly onboarding specialist. Welcome {{name}} to our service and ask about their first experience. Keep it casual and encouraging.'
  },
  'at-risk': {
    voiceId: 'pNInz6obpgDQGcFmaJgB', // ElevenLabs Adam voice
    speed: 0.95,
    stability: 0.80,
    similarityBoost: 0.85,
    systemPrompt: 'You are a retention specialist. Express concern about {{name}}\'s recent inactivity. Offer personalized solutions based on their last interaction.'
  }
};

// Create personalized assistant for each customer
async function createPersonalizedAssistant(segment, customerName, customerData) {
  const config = customerSegments[segment];
  if (!config) throw new Error(`Unknown segment: ${segment}`);

  // Inject customer data into system prompt (sanitize to prevent prompt injection)
  const sanitizedName = customerName.replace(/[{}]/g, '');
  const sanitizedPurchase = customerData.purchase?.replace(/[{}]/g, '') || 'recent order';
  
  const systemPrompt = config.systemPrompt
    .replace('{{name}}', sanitizedName)
    .replace('${{purchase}}', sanitizedPurchase);

  // Token estimation for cost control (GPT-4: ~$0.03/1K tokens)
  const estimatedTokens = systemPrompt.length / 4;
  if (estimatedTokens > 500) {
    console.warn(`High token count (${estimatedTokens}) for ${customerName}`);
  }

  const assistantConfig = {
    model: {
      provider: 'openai',
      model: 'gpt-4',
      temperature: 0.7,
      systemPrompt: systemPrompt
    },
    voice: {
      provider: 'elevenlabs',
      voiceId: config.voiceId,
      speed: config.speed,
      stability: config.stability,
      similarityBoost: config.similarityBoost
    },
    transcriber: {
      provider: 'deepgram',
      model: 'nova-2',
      language: 'en'
    },
    firstMessage: `Hi ${sanitizedName}, this is a quick follow-up call.`
  };

  try {
    const response = await fetch('https://api.vapi.ai/assistant', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.VAPI_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(assistantConfig)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`VAPI API Error: ${response.status} - ${JSON.stringify(error)}`);
    }

    const assistant = await response.json();
    return assistant.id; // Return assistant ID for call initiation
  } catch (error) {
    console.error('Assistant creation failed:', error);
    throw error;
  }
}

// Initiate outbound call via VAPI (NOT Twilio SDK - raw HTTP)
async function initiateOutboundCall(assistantId, phoneNumber, customerData) {
  try {
    const response = await fetch('https://api.vapi.ai/call/phone', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + process.env.VAPI_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        assistantId: assistantId,
        customer: {
          number: phoneNumber // E.164 format: +1234567890
        },
        phoneNumberId: process.env.VAPI_PHONE_NUMBER_ID, // Your VAPI phone number
        metadata: {
          customerId: customerData.id,
          segment: customerData.segment,
          campaignId: 'personalized-followup-2024'
        }
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Call initiation failed: ${response.status} - ${JSON.stringify(error)}`);
    }

    const callData = await response.json();
    console.log(`Call initiated: ${callData.id} to ${phoneNumber}`);
    return callData;
  } catch (error) {
    console.error('Outbound call error:', error);
    throw error;
  }
}

// Webhook signature validation (CRITICAL - prevents spoofed webhooks)
function validateWebhookSignature(payload, signature, timestamp) {
  const requestTime = parseInt(timestamp);
  const currentTime = Math.floor(Date.now() / 1000);
  
  // Reject requests older than 5 minutes (replay attack prevention)
  if (Math.abs(currentTime - requestTime) > 300) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac('sha256', process.env.VAPI_WEBHOOK_SECRET)
    .update(`${timestamp}.${JSON.stringify(payload)}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Webhook handler for call events
app.post('/webhook/vapi', async (req, res) => {
  const signature = req.headers['x-vapi-signature'];
  const timestamp = req.headers['x-vapi-timestamp'];
  const payload = req.body;

  // Validate webhook authenticity
  const isValid = validateWebhookSignature(payload, signature, timestamp);
  if (!isValid) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const event = payload.message;
  const callId = payload.call?.id;

  // Handle call lifecycle events
  switch (event.type) {
    case 'call-started':
      console.log(`Call ${callId} started`);
      break;
    case 'call-ended':
      console.log(`Call ${callId} ended. Duration: ${event.duration}s`);
      // Log to analytics: segment, duration, outcome
      break;
    case 'transcript':
      console.log(`Transcript: ${event.transcript}`);
      // Store for sentiment analysis
      break;
    case 'error':
      console.error(`Call error: ${event.error}`);
      break;
  }

  res.status(200).json({ received: true });
});

// Campaign trigger endpoint (called by your CRM/scheduler)
app.post('/campaign/trigger', async (req, res) => {

## FAQ

## Technical Questions

**How do I prevent system prompt injection when personalizing voice profiles with customer data?**

Sanitize all customer-sourced inputs (names, purchase history, preferences) before embedding them in `systemPrompt`. Use allowlists for dynamic values—only inject pre-validated segments from `customerSegments`. Never concatenate raw user input directly into the prompt. Example: if `sanitizedName` and `sanitizedPurchase` pass validation, then inject. If a customer's name contains prompt-breaking characters, reject or escape them. This prevents attackers from injecting instructions like "ignore previous instructions" into your assistant's behavior.

**What's the difference between `voiceId` and `similarityBoost` in ElevenLabs voice configuration?**

`voiceId` selects the base voice profile (e.g., "Rachel", "Adam"). `similarityBoost` (0.0–1.0) controls how closely the TTS engine adheres to that voice's characteristics. Higher values (0.8–1.0) produce more consistent, recognizable voices but may reduce naturalness. Lower values (0.3–0.5) sound more natural but less consistent across segments. For marketing campaigns targeting specific `segment` demographics, use high `similarityBoost` (0.85+) to maintain brand voice consistency. For conversational fallback responses, lower it to 0.6–0.7 for flexibility.

**How do I validate webhook signatures from VAPI to prevent spoofed events?**

Use `crypto` to compute HMAC-SHA256 of the request body with your webhook secret. Compare the computed `signature` against the `X-Signature` header. Extract `timestamp` from headers and verify `currentTime - requestTime < 300` (5-minute window) to prevent replay attacks. Only process events where `isValid === true`. This prevents attackers from triggering fake `callId` events or injecting false `response` data into your system.

## Performance

**Why does my personalized assistant respond slower after adding dynamic `systemPrompt` injection?**

Token count increases when you embed customer data into the prompt. Each injected value (name, purchase history, segment) adds tokens, which increases LLM latency. Calculate `estimatedTokens` before calling the model—if it exceeds your budget, truncate or summarize customer context. Use shorter segment names and limit purchase history to the last 3 items. Pre-compute common prompts and cache them by `segment` to avoid recalculation per call.

**What latency should I expect when switching voice profiles mid-campaign?**

Switching `voiceId` requires re-initializing the TTS engine (~200–400ms). Batch voice profile changes between calls rather than within a single conversation. If you need dynamic voice switching per `segment`, pre-load multiple voice profiles during `createPersonalizedAssistant` initialization to avoid runtime delays.

## Platform Comparison

**Should I use VAPI's native voice configuration or Twilio's voice settings?**

Use VAPI's `voice` configuration (including `voiceId`, `speed`, `stability`) for consistency. Twilio handles carrier routing and call quality, not voice synthesis. VAPI controls what the bot sounds like; Twilio controls how the call reaches the customer. Don't duplicate voice settings across both platforms—configure once in VAPI's `assistantConfig`, let Twilio manage the call leg.

## Resources

**VAPI**: Get Started with VAPI → [https://vapi.ai/?aff=misal](https://vapi.ai/?aff=misal)

**VAPI Documentation**
- [Official VAPI Docs](https://docs.vapi.ai) – Complete API reference for assistants, calls, and voice configuration
- [Voice Profiles & Custom Voices](https://docs.vapi.ai/voice) – ElevenLabs voiceId integration, stability/similarityBoost tuning
- [Webhook Events & Signatures](https://docs.vapi.ai/webhooks) – Signature validation, event payload schemas

**Twilio Integration**
- [Twilio Voice API](https://www.twilio.com/docs/voice) – Outbound call initiation, call control
- [Twilio + VAPI Bridge](https://docs.vapi.ai/integrations/twilio) – Phone number routing, call metadata

**Code Examples & GitHub**
- [VAPI GitHub Samples](https://github.com/VapiAI/server-sdk-js) – Production-grade webhook handlers, `validateWebhookSignature` implementation
- [ElevenLabs Voice Library](https://elevenlabs.io/docs/api-reference/get-voices) – Available voiceId values, voice cloning parameters

## References

1. https://docs.vapi.ai/outbound-campaigns/quickstart
2. https://docs.vapi.ai/quickstart/phone
3. https://docs.vapi.ai/quickstart/introduction
4. https://docs.vapi.ai/quickstart/web
5. https://docs.vapi.ai/tools/custom-tools
6. https://docs.vapi.ai/chat/quickstart
7. https://docs.vapi.ai/workflows/quickstart
8. https://docs.vapi.ai/observability/evals-quickstart
9. https://docs.vapi.ai/assistants/quickstart

Advertisement

Written by

Misal Azeem
Misal Azeem

Voice AI Engineer & Creator

Building production voice AI systems and sharing what I learn. Focused on VAPI, LLM integrations, and real-time communication. Documenting the challenges most tutorials skip.

VAPIVoice AILLM IntegrationWebRTC

Found this helpful?

Share it with other developers building voice AI.