Complete Guide to Twilio Missed Call Handling: Call Forwarding, Voicemail, and Notifications

Twilio missed call handler cover image with phon icon, twilio icon, telegram icon.

Credit: Development work provided by Andrew Henke (co-founder of RGC, and Owner of AHCTS)
Moral support and QA provided by Austin Kruger (co-founder of RGC)

 

If you’re using Twilio to manage business calls, chances are you’re already taking advantage of features like call routing, forwarding, and simultaneous dialing. But one frustrating limitation of Twilio’s default setup is what happens when no one answers the phone — calls are usually forwarded to the voicemail of the last dialed number, which is often a personal cell phone. This results in unprofessional voicemail greetings, poor brand perception, and lost leads.

In this guide, we’ll show you how to take full control of the missed call experience by setting up a professional voicemail system entirely within Twilio — no external hosting or servers required.

You’ll learn how to:

  • Set up call forwarding and simultaneous dialing to multiple numbers
  • Detect when no one answers and route the call to a custom voicemail
  • Use Twilio’s built-in voice recording and transcription features
  • Automatically send voicemail alerts to Telegram with recording and transcription (or email if preferred)

Whether you’re building a custom phone system for your startup, agency, or client, this solution gives you a fully branded, automated way to handle missed calls — and ensures you never lose touch with potential customers or clients.

Best of all, everything is done using Twilio Functions, Assets, and Environment Variables — so there’s no need to host your own server or deal with complex infrastructure. This is a scalable, cloud-based solution that works for solo founders, remote teams, and growing businesses.

Before we jump into creating the Twilio Functions, here’s a visual representation of how the call logic flows through the system:

RGC Twilio missed calls overview flow chart

Let’s dive in. 👇

🛠️ What You’ll Need

  • A Twilio account with at least one phone number
  • Two voice recordings:
    • A **welcome message** (played when the call connects)
    • A **voicemail message** (played if no one answers)
      *(Or use Twilio’s built-in `say` command to auto-generate these with AI voice)*
  • (Optional) A Telegram bot setup to receive alerts:

Step 1: Create A Twilio Service

  • From the develop menu or search bar find “Services” once you are in the services menu click the button to “Create Service”
  • Give your service a name that best represents your goal, something like “{yourbusiness} – phone line

Step 2: Upload Your Welcome and Voicemail Recordings (Assets)

  • Once you have created the service you should see a button that says “Add +” in the top left. Click Add and select the option for Upload file. Then upload both of your voice records to the service (make sure to name the files before you upload to a short name that helps you quickly identify the difference between the welcome, and voicemail message)

Step 3: Creating Environment Variables

  • If you are not going to use the telegram notification functionality just check the box the says “Add my Twilio Credentials to ENV”
  • Optional for telegram bot notifications: create Environment Variables from the settings & more section:

RGC Twilio Call Handler Environment Variables


Step 4: Create Dependencies In Your Service

  • Click “Dependencies” from the bottom left hand side

RGC Twilio missed calls dependencies

Module Version
@twilio/runtime-handler2.0.1
ffmpeg-staticlatest
xmldom0.6.0
twilio5.0.3
lodash4.17.21
util0.12.5
kylatest
  • After you have added all of these you should have 2 pages of dependencies

Step 5: Creating The Functions

Note: You are able to name these functions whatever you like, but we would recommend that you use the names we have in these functions at least until after you have run successful tests on the completed service.

Function NameRole
/inboundCallEntry point. Plays welcome message and dials users
/noAnswerTriggered if no one answers. Plays voicemail message and records
/voicemailHandles the recording, transcription, and notification

Function #1: Voicemail Function

This function is the last step in this project, it downloads the voice recording as a file, transcribes it and adds the details to the channel you created for voicemail notifications in telegram. This is our preferred notification method but if you would like to trigger alerts via email instead that is completely possible. Click here for how to send email notifications for voicemail

  • Next Click the “Add” button again and select the option for “Function”
  • For the path name enter “voicemail”
  • Now that you’ve added your variables click back on the “voicemail” tab and paste this into the code editor:
				
					exports.handler = async function(context, event, callback) {
 console.log("Sent to voicemail");
 const twiml = new Twilio.twiml.VoiceResponse();


 // Dynamically import ky.
 let ky;
 try {
   ky = (await import('ky')).default;
 } catch (error) {
   console.error('Error importing ky:', error);
   return callback(error);
 }


 // Built-in modules.
 const { spawn } = require('child_process');
 const ffmpegPath = require('ffmpeg-static');


 // Determine the recording SID.
 let recordingSid = event.RecordingSid;
 console.log(JSON.stringify(event));
 if (!recordingSid && event.RecordingUrl) {
   const parts = event.RecordingUrl.split('/');
   recordingSid = parts[parts.length - 1];
 }
 console.log("recordingSid:", recordingSid);


 // Use the RecordingUrl from the event if available; otherwise, build it manually.
 let mediaUrl;
 if (event.RecordingUrl) {
   mediaUrl = event.RecordingUrl;
   if (!mediaUrl.endsWith('.mp3')) {
     mediaUrl += '.mp3';
   }
 } else {
   const accountSid = context.ACCOUNT_SID;
   mediaUrl = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Recordings/${recordingSid}.mp3`;
 }
 console.log("Constructed mediaUrl:", mediaUrl);


 // If the URL is publicly accessible, you may not need an auth header.
 // But if you do, you can construct it as shown below.
 const accountSid = context.ACCOUNT_SID;
 const basicAuth = Buffer.from(`${accountSid}:${context.AUTH_TOKEN}`).toString('base64');


 // A simple retry mechanism to download the MP3 file.
 async function downloadMp3(url, retries = 3, delayMs = 2000) {
   for (let attempt = 1; attempt <= retries; attempt++) {
     try {
       const response = await ky.get(url, {
         responseType: 'arrayBuffer',
         throwHttpErrors: false,
         headers: {
           // If authentication is not needed, you can comment this out.
           Authorization: `Basic ${basicAuth}`
         }
       });
       if (response.ok) {
         const arrayBuffer = await response.arrayBuffer();
         return Buffer.from(arrayBuffer);
       } else {
         console.warn(`Attempt ${attempt}: Received HTTP ${response.status}`);
         if (attempt < retries) {
           console.log(`Waiting ${delayMs}ms before retrying...`);
           await new Promise(resolve => setTimeout(resolve, delayMs));
         }
       }
     } catch (err) {
       console.error(`Attempt ${attempt} error:`, err);
       if (attempt < retries) {
         await new Promise(resolve => setTimeout(resolve, delayMs));
       }
     }
   }
   throw new Error(`Failed to download MP3 file after ${retries} attempts`);
 }


 // --- Download the MP3 file ---
 let mp3Buffer;
 try {
   mp3Buffer = await downloadMp3(mediaUrl);
   console.log("Successfully downloaded MP3 file");
 } catch (error) {
   console.error('Error downloading MP3 file:', error);
   return callback(error);
 }


 // --- Convert MP3 buffer to OGG/Opus format for Telegram using ffmpeg ---
 async function convertMp3ToOgg(inputBuffer) {
   return new Promise((resolve, reject) => {
     const ffmpeg = spawn(ffmpegPath, [
       '-i', 'pipe:0',       // read input from stdin
       '-c:a', 'libopus',    // encode using libopus
       '-b:a', '64000',      // set bitrate to 64kbps
       '-f', 'ogg',          // output format OGG
       'pipe:1'              // output to stdout
     ]);
      let outputData = [];
     ffmpeg.stdout.on('data', (chunk) => outputData.push(chunk));
     ffmpeg.stderr.on('data', (chunk) => console.error('ffmpeg stderr:', chunk.toString()));
      ffmpeg.on('error', (err) => reject(err));
     ffmpeg.on('close', (code) => {
       if (code !== 0) {
         return reject(new Error(`ffmpeg exited with code ${code}`));
       }
       resolve(Buffer.concat(outputData));
     });
      ffmpeg.stdin.write(inputBuffer);
     ffmpeg.stdin.end();
   });
 }
  let oggBuffer;
 try {
   oggBuffer = await convertMp3ToOgg(mp3Buffer);
   console.log("Successfully converted MP3 to OGG");
 } catch (error) {
   console.error('Error converting MP3 to OGG:', error);
   return callback(error);
 }


 // --- Gather call information ---
 const callTime = new Date().toLocaleString();
 const caller = event.From || 'Unknown';
 const transcription = event.TranscriptionText || 'No transcription available.';
 const caption = `Voicemail received on ${callTime}\nFrom: ${caller}`;


 // Retrieve Telegram credentials.
 const telegramBotToken = context.TELEGRAM_BOT_TOKEN;
 const telegramChatId = context.TELEGRAM_CHAT_ID;


 // --- Send the voice recording to Telegram using sendVoice ---
 const telegramVoiceUrl = `https://api.telegram.org/bot${telegramBotToken}/sendVoice`;
 try {
   // Use the built-in FormData and Blob (available in Node.js v18) to construct the multipart/form-data.
   const form = new FormData();
   form.append('chat_id', telegramChatId);
   const voiceBlob = new Blob([oggBuffer], { type: 'audio/ogg' });
   form.append('voice', voiceBlob, 'voicemail.ogg');
   form.append('caption', caption);
  
   const voiceResponse = await ky.post(telegramVoiceUrl, {
     body: form,
     throwHttpErrors: false
   });
   const voiceJson = await voiceResponse.json();
   if (!voiceJson.ok) {
     console.error('Telegram sendVoice API error:', voiceJson);
   } else {
     console.log('Telegram sendVoice API response:', voiceJson);
   }
 } catch (error) {
   console.error('Exception while sending voice to Telegram:', error);
 }


 // --- Send a second Telegram message with call info and transcription using sendMessage ---
 const telegramMessageUrl = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
 const messageText = `${caption}\nTranscription: ${transcription}`;
 try {
   const messageResponse = await ky.post(telegramMessageUrl, {
     json: {
       chat_id: telegramChatId,
       text: messageText
     },
     throwHttpErrors: false
   });
   const messageJson = await messageResponse.json();
   if (!messageJson.ok) {
     console.error('Telegram sendMessage API error:', messageJson);
   } else {
     console.log('Telegram sendMessage API response:', messageJson);
   }
 } catch (error) {
   console.error('Exception while sending message to Telegram:', error);
 }


 // --- Finalize the call ---
 twiml.say("Thank you for your message. Goodbye.");
 twiml.hangup();


 return callback(null, twiml);
};

				
			

Prefer email over Telegram? You can easily adapt this function to send email alerts instead. [Click here to jump to the email version].

 

  • Click Save

 

When the caller has completed recording their voicemail this function will download the voicemail recording and send a notification in Telegram that will look like this:

RGC Twilio missed call telegram notification example 


Function #2: The noAnswer Function

This function is what triggers the voicemail message if a call has not been answered in 20 seconds and sends them to the voicemail function we added earlier.

  • Now that the voicemail actions are done you will need to create another function. Click the top left button “Add +” again and select function
  • Name the function “noAnswer”
  • Paste the following code in the code editor:
				
					exports.handler = function(context, event, callback) {
 console.log("No Answer function activated");
 const twiml = new Twilio.twiml.VoiceResponse();
 // Check the Dial attempt status.
 // event.DialCallStatus will be "completed" if one of the numbers answered.
 if (event.DialCallStatus !== 'completed') {
   console.debug("Call status is not completed");
   // If no one answered within 20 seconds, play a voicemail greeting...
   twiml.play('YOUR VOICEMAIL ASSET URL'); 
  
   // Get the voicemail url from the Assets selection, right click and copy URL (Make sure its set to public).
   // The 'action' URL below can be another function that processes the recording.
   twiml.record({
     maxLength: 60, // Record up to 60 seconds of voicemail.
     action: 'URL FOR /voicemail', // URL to handle the recording Just right click the function on the left and click copy URL.
     transcribe: true, //There is a cost for transcriptions so if you don't need it set to false
     //recordingStatusCallbackMethod: 'POST'
   });
 } else {
   // If the call was answered, simply hang up.
   twiml.hangup();
 }
  return callback(null, twiml);
};

				
			
  • Click Save

 


Function #3: The inboundCall Function


This function is the first part of the service we have created and will forward all calls to the phone numbers you listed, if the call goes unanswered for 20 seconds it will play the Voicemail message you recorded and allow the caller to leave a voicemail.

  • Now you will need to click “Add +” once more and select “Function”
  • Name the function “inboundCall”
  • Paste the following function into the code editor:
				
					exports.handler = function(context, event, callback) {
 const twiml = new Twilio.twiml.VoiceResponse();
 console.log("New Inbound Call");
 console.log("Call from: " + event.From);
 // Play the welcome audio from your public asset.
 const audioUrl = 'YOUR WELCOME ASSET RECORDING';
 twiml.play(audioUrl);
  // Pause for 1 second.
 twiml.pause({ length: 1 });
 console.log("Pausing for 1 second complete");
 // Dial multiple numbers concurrently with a 20-second timeout.
 // The 'action' attribute specifies the URL to request when the Dial verb completes.
 const dial = twiml.dial({
   callerId: event.To,   // Use the original receiving number as the caller ID.
   timeout: 20,          // Wait 20 seconds for an answer.
   action: 'URL FOR THE “noAnswer” FUNCTION'  // Endpoint to handle no-answer.
 });
  // Add the numbers you want to receive calls in the same format, no dashes periods etc. .
 dial.number('+15555555555');
 dial.number('+15555555555');
  return callback(null, twiml);
};

				
			
  • Click Save
  • Check to make sure that /voicemail, /inboundcall, and all assets have visibility set to “Public”RGC Twilio missed call function asset permission change
  • Now click the “Deploy” button from the bottom left of the screen

RGC Twilio missed call deploy


Step 6: Attach The Function To Your Twilio Phone Number

  • From your Twilio console search for “Active Numbers”

RGC twilio missed call active numbers menu

  • Find the number you use for your business and click on it
  • Under “Configure with” section select the option “Webhook,TwiML Bin, Function, Studio Flow, Proxy Service”
  • In the “Call comes in” dropdown select “Function”
  • For the “Service” section select the service we create in the earlier steps
  • Environment “ui”
  • Function Path “/inboundCall or whatever you named the very last function we added
  • When all of this is set it should look like this:

RGC Twilio missed calls phone number settings

  • Click the “Save Configuration” from the bottom left

Step 7: Testing Your Setup

  • Replace team numbers with your own to test solo
  • Use Twilio’s logs to see function output and errors
  • Make sure your assets are set to “public”
  • Confirm transcription is turned on if needed

 

Common Issues

  • “401 Unauthorized” when downloading recordings? Make sure your Twilio credentials are correct and the URL is formatted correctly.
  • Voicemail not triggering? Check your `DialCallStatus` and timeout settings.

Once you’ve configured everything, place a test call and let it ring through without answering. If everything is working properly, you should hear your custom voicemail greeting after 20 seconds, be prompted to leave a message, and then receive either a Telegram or email notification with the recording and call details. This is a great way to ensure your voicemail system is working end-to-end before launching it in a live business environment.

About Us

At Rhino Group Consulting, we build solutions like this every day for our clients—whether it’s automating call routing, integrating CRMs, developing custom workflows, or tackling more advanced telephony setups. From startups to enterprise-level businesses, we’ve helped companies across industries improve their systems and stay connected with their customers.

Our team thrives on solving complex challenges, and we don’t stop until we find a solution that’s both stable and scalable. No matter the use case or how unconventional the problem, we treat each project as an opportunity to innovate, optimize, and deliver real results.

If you’re facing a bottleneck in your phone system, business automation, or software integrations, we’d love to help. Reach out to see how we can turn your vision into a robust and reliable system—custom-fit to your business goals.

Table of Contents

Need help with one of you guides? Send us your email and a member of our development team will reach out to assist: