How do you handle call rejections, no-answer, and voicemails

I’m using Twilio to automate my calls, but I’m running into an issue with the call not knowing how to handle declined or no-answer calls. When either of these events happen, the phone call is sent to voicemail (this is a default behavior for most phones). Twilio has no idea how to properly handle this event. I’ve tried adding in AMD support to detect machines, but shorter voicemails or too natural sounding voicemails default to “human.” The funny thing, when I pick up the call, I’m listed as “unknown” rather than human.

My call flow is:

Call Created -> Automated message plays -> Based on condition, it may route to "askName" for more details and confirmations

When I initiate my call, and do the original call creation, I add in the voicemail configuration. But there’s no way to handle this information appropriately. Twilio can’t recognize it as a voicemail quick enough. Moreover, /handleCall will always send me to /askName because there’s no way the call to wait for the AMD status to update.

How do you handle Twilio interacting with a voicemail and thinking it’s human? Or how to handle the call routing appropriately based on the machine detection if machine detection is so slow?

Here’s my code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>import { Router } from 'express';
import twilio from 'twilio';
import axios from 'axios';
import mondayService from '../monday/mondayService.js';
const router = Router();
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const awsS3Url = process.env.AWS_GOOGLE_S3_BASE_URL;
const recognizedNames = new Map(); // used to capture (in-memory) the SpeechResult
const exitedCalls = new Map();
const amdResults = new Map();
router.post('/initiateCall', async (req, res) => {
const { to, contactName, pulseId, linkedPulseId, patientName } = req.body;
console.log('Request:', req.body);
if (!to || !patientName) {
return res
.status(400)
.send('Please provide "to" and "patientName" in the request body.');
}
try {
const call = await client.calls.create({
from: process.env.TWILIO_PHONE_NUMBER,
to: to,
url: `${baseUrl}/twilio/handleCall?contactName=${encodeURIComponent(
contactName
)}&patientName=${encodeURIComponent(patientName)}`,
statusCallback: `${baseUrl}/twilio/callStatus?pulseId=${pulseId}&linkedPulseId=${linkedPulseId}`,
statusCallbackEvent: [
'initiated',
'ringing',
'answered',
'completed',
],
statusCallbackMethod: 'POST',
machineDetection: 'Enable',
asyncAmd: 'true',
asyncAmdStatusCallback: `${baseUrl}/twilio/amdStatus`,
timeout: 15,
machineDetectionSpeechThreshold: 2400,
machineDetectionSpeechEndThreshold: 500,
machineDetectionSpeechTimeout: 3000,
});
res.send({ message: 'Call initiated successfully', callSid: call.sid });
} catch (error) {
console.error('Error during call initiation:', error.message);
res.status(500).send('Failed to initiate call.');
}
});
router.post('/amdStatus', async (req, res) => {
const { CallSid, AnsweredBy } = req.body;
console.log(`AMD Status for call ${CallSid}: ${AnsweredBy}`);
if (CallSid && AnsweredBy) {
amdResults.set(CallSid, AnsweredBy);
if (
[
'machine_start',
'machine_end_beep',
'machine_end_silence',
'machine_end_other',
].includes(AnsweredBy)
) {
console.log('Detected voicemail or machine, hanging up.');
const twiml = new twilio.twiml.VoiceResponse();
twiml.hangup();
await client.calls(CallSid).update({ twiml: twiml.toString() });
}
}
res.sendStatus(200);
});
router.post('/handleCall', async (req, res) => {
const { contactName, patientName } = req.query;
const twiml = new twilio.twiml.VoiceResponse();
const messageText = contactName
? `Hi ${contactName}. This is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals. Thanks again for partnering with us, and have a great day!`
: `Hi, this is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals.`;
try {
const response = await axios.post(`${baseUrl}/googleTTS/synthesize`, {
text: messageText,
});
const audioUrl = response.data.url;
twiml.play(audioUrl);
twiml.pause({ length: 1 });
if (!contactName) {
twiml.redirect(`${baseUrl}/twilio/askName`);
}
} catch (error) {
console.error('Error during Google TTS synthesis:', error.message);
twiml.hangup();
}
res.type('text/xml');
res.send(twiml.toString());
});
router.post('/askName', async (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
const retryCount = parseInt(req.query.retryCount) || 0;
const retry = req.query.retry;
if (retryCount >= 3) {
console.log('Max retry limit reached, ending the call.');
twiml.hangup();
res.type('text/xml');
res.send(twiml.toString());
return;
}
twiml
.gather({
input: 'speech',
action: '/twilio/confirmName',
method: 'POST',
speechTimeout: '2',
language: 'en-US',
})
.play(
retry ? `${awsS3Url}/askNameRetry.wav` : `${awsS3Url}/askName.wav`
);
twiml.redirect(`/twilio/askName?retry=true&retryCount=${retryCount + 1}`);
res.type('text/xml');
res.send(twiml.toString());
});
router.post('/confirmName', async (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
const recognizedName = req.body.SpeechResult?.replace(/.$/, '').trim();
const digits = req.body.Digits;
const callSid = req.body.CallSid;
console.log('Received Request:', req.body);
if (recognizedName) {
recognizedNames.set(callSid, recognizedName);
const text = `It sounds like you said -- ${recognizedName}. If that sounds right, please press 1 to confirm. Press 2 to try again, or press 3 to end the call.`;
try {
const response = await axios.post(
`${baseUrl}/googleTTS/synthesize`,
{ text }
);
const audioUrl = response.data.url;
twiml
.gather({
action: '/twilio/handleConfirmation',
method: 'POST',
numDigits: 1,
})
.play(audioUrl);
twiml.redirect('/twilio/confirmName');
res.type('text/xml');
res.send(twiml.toString());
return;
} catch (error) {
console.error('Error during Google TTS synthesis:', error.message);
twiml.hangup();
res.status(500).send('Failed to synthesize speech.');
return;
}
}
if (digits) {
twiml.redirect('/twilio/handleConfirmation');
} else {
twiml.redirect('/twilio/askName?retry=true');
}
res.type('text/xml');
res.send(twiml.toString());
});
router.post('/handleConfirmation', (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
const digits = req.body.Digits;
const callSid = req.body.CallSid;
const confirmAudioUrl = `${awsS3Url}/thankYouGoodBye.wav`;
const exitAudioUrl = `${awsS3Url}/exitThankYou.wav`;
const retryInputAudioUrl = `${awsS3Url}/retryInput.wav`;
switch (digits) {
case '1': // User confirmed the name
twiml.play(confirmAudioUrl);
twiml.hangup();
break;
case '2': // User wants to retry
twiml.redirect('/twilio/askName?retry=true');
recognizedNames.delete(callSid);
break;
case '3': // User wants to exit
twiml.play(exitAudioUrl);
twiml.hangup();
recognizedNames.delete(callSid);
exitedCalls.set(callSid, true);
break;
default: // Invalid input, ask again
twiml.play(retryInputAudioUrl);
twiml.redirect('/twilio/confirmName');
break;
}
res.type('text/xml');
res.send(twiml.toString());
});
router.post('/callStatus', async (req, res) => {
const callStatus = req.body.CallStatus;
const callSid = req.body.CallSid;
const pulseId = req.query.pulseId;
const linkedPulseId = req.query.linkedPulseId;
const exitedCall = exitedCalls.get(callSid);
const answeredBy = amdResults.get(callSid);
console.log('Full request body:', JSON.stringify(req.body, null, 2));
console.log('Call SID:', req.body.CallSid);
console.log('Call Status:', req.body.CallStatus);
console.log('Call Duration:', req.body.CallDuration);
console.log('Answered By::', answeredBy);
if (pulseId && linkedPulseId) {
try {
let statusText;
if (
answeredBy &&
[
'machine_start',
'machine_end_beep',
'machine_end_silence',
'machine_end_other',
].includes(answeredBy)
) {
statusText = 'Incomplete: No Answer';
console.log(
`Call ${callSid} ended due to machine detection: ${answeredBy}`
);
} else if (
['initiated', 'ringing', 'answered', 'in-progress'].includes(
callStatus
)
) {
statusText = `In Progress: ${callStatus}`;
} else {
switch (callStatus) {
case 'completed':
const recognizedName = recognizedNames.get(callSid);
if (recognizedName) {
await mondayService.updateMondayContactName(
pulseId,
recognizedName
);
statusText = 'Completed: Name Received';
} else if (exitedCall) {
statusText = 'Completed: Call Exited';
} else {
statusText = 'Completed';
}
break;
case 'no-answer':
statusText = 'Incomplete: No Answer';
break;
case 'busy':
statusText = 'Incomplete: Busy';
break;
case 'failed':
statusText = 'Incomplete: Failed';
break;
case 'canceled':
statusText = 'Incomplete: Canceled';
break;
default:
statusText = 'Unknown Call Completion Status';
}
}
await mondayService.updateCallStatus(linkedPulseId, statusText);
console.log(`Call status updated to: ${statusText}`);
if (
[
'completed',
'no-answer',
'busy',
'failed',
'canceled',
].includes(callStatus)
) {
recognizedNames.delete(callSid);
exitedCalls.delete(callSid);
amdResults.delete(callSid);
}
} catch (error) {
console.error(
'Error updating call status on Monday.com:',
error.message
);
await mondayService.updateCallStatus(
linkedPulseId,
'Update Status Error'
);
}
} else {
console.log(
'Missing linkedPulseId or pulseId, cannot update Monday.com'
);
}
res.status(200).send('Call status received and processed.');
});
export default router;
</code>
<code>import { Router } from 'express'; import twilio from 'twilio'; import axios from 'axios'; import mondayService from '../monday/mondayService.js'; const router = Router(); const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const client = twilio(accountSid, authToken); const baseUrl = process.env.BASE_URL || 'http://localhost:3000'; const awsS3Url = process.env.AWS_GOOGLE_S3_BASE_URL; const recognizedNames = new Map(); // used to capture (in-memory) the SpeechResult const exitedCalls = new Map(); const amdResults = new Map(); router.post('/initiateCall', async (req, res) => { const { to, contactName, pulseId, linkedPulseId, patientName } = req.body; console.log('Request:', req.body); if (!to || !patientName) { return res .status(400) .send('Please provide "to" and "patientName" in the request body.'); } try { const call = await client.calls.create({ from: process.env.TWILIO_PHONE_NUMBER, to: to, url: `${baseUrl}/twilio/handleCall?contactName=${encodeURIComponent( contactName )}&patientName=${encodeURIComponent(patientName)}`, statusCallback: `${baseUrl}/twilio/callStatus?pulseId=${pulseId}&linkedPulseId=${linkedPulseId}`, statusCallbackEvent: [ 'initiated', 'ringing', 'answered', 'completed', ], statusCallbackMethod: 'POST', machineDetection: 'Enable', asyncAmd: 'true', asyncAmdStatusCallback: `${baseUrl}/twilio/amdStatus`, timeout: 15, machineDetectionSpeechThreshold: 2400, machineDetectionSpeechEndThreshold: 500, machineDetectionSpeechTimeout: 3000, }); res.send({ message: 'Call initiated successfully', callSid: call.sid }); } catch (error) { console.error('Error during call initiation:', error.message); res.status(500).send('Failed to initiate call.'); } }); router.post('/amdStatus', async (req, res) => { const { CallSid, AnsweredBy } = req.body; console.log(`AMD Status for call ${CallSid}: ${AnsweredBy}`); if (CallSid && AnsweredBy) { amdResults.set(CallSid, AnsweredBy); if ( [ 'machine_start', 'machine_end_beep', 'machine_end_silence', 'machine_end_other', ].includes(AnsweredBy) ) { console.log('Detected voicemail or machine, hanging up.'); const twiml = new twilio.twiml.VoiceResponse(); twiml.hangup(); await client.calls(CallSid).update({ twiml: twiml.toString() }); } } res.sendStatus(200); }); router.post('/handleCall', async (req, res) => { const { contactName, patientName } = req.query; const twiml = new twilio.twiml.VoiceResponse(); const messageText = contactName ? `Hi ${contactName}. This is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals. Thanks again for partnering with us, and have a great day!` : `Hi, this is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals.`; try { const response = await axios.post(`${baseUrl}/googleTTS/synthesize`, { text: messageText, }); const audioUrl = response.data.url; twiml.play(audioUrl); twiml.pause({ length: 1 }); if (!contactName) { twiml.redirect(`${baseUrl}/twilio/askName`); } } catch (error) { console.error('Error during Google TTS synthesis:', error.message); twiml.hangup(); } res.type('text/xml'); res.send(twiml.toString()); }); router.post('/askName', async (req, res) => { const twiml = new twilio.twiml.VoiceResponse(); const retryCount = parseInt(req.query.retryCount) || 0; const retry = req.query.retry; if (retryCount >= 3) { console.log('Max retry limit reached, ending the call.'); twiml.hangup(); res.type('text/xml'); res.send(twiml.toString()); return; } twiml .gather({ input: 'speech', action: '/twilio/confirmName', method: 'POST', speechTimeout: '2', language: 'en-US', }) .play( retry ? `${awsS3Url}/askNameRetry.wav` : `${awsS3Url}/askName.wav` ); twiml.redirect(`/twilio/askName?retry=true&retryCount=${retryCount + 1}`); res.type('text/xml'); res.send(twiml.toString()); }); router.post('/confirmName', async (req, res) => { const twiml = new twilio.twiml.VoiceResponse(); const recognizedName = req.body.SpeechResult?.replace(/.$/, '').trim(); const digits = req.body.Digits; const callSid = req.body.CallSid; console.log('Received Request:', req.body); if (recognizedName) { recognizedNames.set(callSid, recognizedName); const text = `It sounds like you said -- ${recognizedName}. If that sounds right, please press 1 to confirm. Press 2 to try again, or press 3 to end the call.`; try { const response = await axios.post( `${baseUrl}/googleTTS/synthesize`, { text } ); const audioUrl = response.data.url; twiml .gather({ action: '/twilio/handleConfirmation', method: 'POST', numDigits: 1, }) .play(audioUrl); twiml.redirect('/twilio/confirmName'); res.type('text/xml'); res.send(twiml.toString()); return; } catch (error) { console.error('Error during Google TTS synthesis:', error.message); twiml.hangup(); res.status(500).send('Failed to synthesize speech.'); return; } } if (digits) { twiml.redirect('/twilio/handleConfirmation'); } else { twiml.redirect('/twilio/askName?retry=true'); } res.type('text/xml'); res.send(twiml.toString()); }); router.post('/handleConfirmation', (req, res) => { const twiml = new twilio.twiml.VoiceResponse(); const digits = req.body.Digits; const callSid = req.body.CallSid; const confirmAudioUrl = `${awsS3Url}/thankYouGoodBye.wav`; const exitAudioUrl = `${awsS3Url}/exitThankYou.wav`; const retryInputAudioUrl = `${awsS3Url}/retryInput.wav`; switch (digits) { case '1': // User confirmed the name twiml.play(confirmAudioUrl); twiml.hangup(); break; case '2': // User wants to retry twiml.redirect('/twilio/askName?retry=true'); recognizedNames.delete(callSid); break; case '3': // User wants to exit twiml.play(exitAudioUrl); twiml.hangup(); recognizedNames.delete(callSid); exitedCalls.set(callSid, true); break; default: // Invalid input, ask again twiml.play(retryInputAudioUrl); twiml.redirect('/twilio/confirmName'); break; } res.type('text/xml'); res.send(twiml.toString()); }); router.post('/callStatus', async (req, res) => { const callStatus = req.body.CallStatus; const callSid = req.body.CallSid; const pulseId = req.query.pulseId; const linkedPulseId = req.query.linkedPulseId; const exitedCall = exitedCalls.get(callSid); const answeredBy = amdResults.get(callSid); console.log('Full request body:', JSON.stringify(req.body, null, 2)); console.log('Call SID:', req.body.CallSid); console.log('Call Status:', req.body.CallStatus); console.log('Call Duration:', req.body.CallDuration); console.log('Answered By::', answeredBy); if (pulseId && linkedPulseId) { try { let statusText; if ( answeredBy && [ 'machine_start', 'machine_end_beep', 'machine_end_silence', 'machine_end_other', ].includes(answeredBy) ) { statusText = 'Incomplete: No Answer'; console.log( `Call ${callSid} ended due to machine detection: ${answeredBy}` ); } else if ( ['initiated', 'ringing', 'answered', 'in-progress'].includes( callStatus ) ) { statusText = `In Progress: ${callStatus}`; } else { switch (callStatus) { case 'completed': const recognizedName = recognizedNames.get(callSid); if (recognizedName) { await mondayService.updateMondayContactName( pulseId, recognizedName ); statusText = 'Completed: Name Received'; } else if (exitedCall) { statusText = 'Completed: Call Exited'; } else { statusText = 'Completed'; } break; case 'no-answer': statusText = 'Incomplete: No Answer'; break; case 'busy': statusText = 'Incomplete: Busy'; break; case 'failed': statusText = 'Incomplete: Failed'; break; case 'canceled': statusText = 'Incomplete: Canceled'; break; default: statusText = 'Unknown Call Completion Status'; } } await mondayService.updateCallStatus(linkedPulseId, statusText); console.log(`Call status updated to: ${statusText}`); if ( [ 'completed', 'no-answer', 'busy', 'failed', 'canceled', ].includes(callStatus) ) { recognizedNames.delete(callSid); exitedCalls.delete(callSid); amdResults.delete(callSid); } } catch (error) { console.error( 'Error updating call status on Monday.com:', error.message ); await mondayService.updateCallStatus( linkedPulseId, 'Update Status Error' ); } } else { console.log( 'Missing linkedPulseId or pulseId, cannot update Monday.com' ); } res.status(200).send('Call status received and processed.'); }); export default router; </code>
import { Router } from 'express';
import twilio from 'twilio';
import axios from 'axios';
import mondayService from '../monday/mondayService.js';

const router = Router();

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
const awsS3Url = process.env.AWS_GOOGLE_S3_BASE_URL;

const recognizedNames = new Map(); // used to capture (in-memory) the SpeechResult
const exitedCalls = new Map();
const amdResults = new Map();

router.post('/initiateCall', async (req, res) => {
    const { to, contactName, pulseId, linkedPulseId, patientName } = req.body;

    console.log('Request:', req.body);

    if (!to || !patientName) {
        return res
            .status(400)
            .send('Please provide "to" and "patientName" in the request body.');
    }

    try {
        const call = await client.calls.create({
            from: process.env.TWILIO_PHONE_NUMBER,
            to: to,
            url: `${baseUrl}/twilio/handleCall?contactName=${encodeURIComponent(
                contactName
            )}&patientName=${encodeURIComponent(patientName)}`,
            statusCallback: `${baseUrl}/twilio/callStatus?pulseId=${pulseId}&linkedPulseId=${linkedPulseId}`,
            statusCallbackEvent: [
                'initiated',
                'ringing',
                'answered',
                'completed',
            ],
            statusCallbackMethod: 'POST',
            machineDetection: 'Enable',
            asyncAmd: 'true',
            asyncAmdStatusCallback: `${baseUrl}/twilio/amdStatus`,
            timeout: 15,
            machineDetectionSpeechThreshold: 2400,
            machineDetectionSpeechEndThreshold: 500,
            machineDetectionSpeechTimeout: 3000,
        });

        res.send({ message: 'Call initiated successfully', callSid: call.sid });
    } catch (error) {
        console.error('Error during call initiation:', error.message);
        res.status(500).send('Failed to initiate call.');
    }
});

router.post('/amdStatus', async (req, res) => {
    const { CallSid, AnsweredBy } = req.body;
    console.log(`AMD Status for call ${CallSid}: ${AnsweredBy}`);

    if (CallSid && AnsweredBy) {
        amdResults.set(CallSid, AnsweredBy);

        if (
            [
                'machine_start',
                'machine_end_beep',
                'machine_end_silence',
                'machine_end_other',
            ].includes(AnsweredBy)
        ) {
            console.log('Detected voicemail or machine, hanging up.');
            const twiml = new twilio.twiml.VoiceResponse();
            twiml.hangup();

            await client.calls(CallSid).update({ twiml: twiml.toString() });
        }
    }

    res.sendStatus(200);
});

router.post('/handleCall', async (req, res) => {
    const { contactName, patientName } = req.query;
    const twiml = new twilio.twiml.VoiceResponse();

    const messageText = contactName
        ? `Hi ${contactName}. This is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals. Thanks again for partnering with us, and have a great day!`
        : `Hi, this is West Coast Wound. I'm reaching out to express our sincere gratitude for referring ${patientName} to us. We truly appreciate your trust in our care, and we're committed to providing the highest level of service and support for your referrals.`;
    try {
        const response = await axios.post(`${baseUrl}/googleTTS/synthesize`, {
            text: messageText,
        });
        const audioUrl = response.data.url;

        twiml.play(audioUrl);
        twiml.pause({ length: 1 });

        if (!contactName) {
            twiml.redirect(`${baseUrl}/twilio/askName`);
        }
    } catch (error) {
        console.error('Error during Google TTS synthesis:', error.message);
        twiml.hangup();
    }

    res.type('text/xml');
    res.send(twiml.toString());
});

router.post('/askName', async (req, res) => {
    const twiml = new twilio.twiml.VoiceResponse();
    const retryCount = parseInt(req.query.retryCount) || 0;
    const retry = req.query.retry;

    if (retryCount >= 3) {
        console.log('Max retry limit reached, ending the call.');
        twiml.hangup();
        res.type('text/xml');
        res.send(twiml.toString());
        return;
    }

    twiml
        .gather({
            input: 'speech',
            action: '/twilio/confirmName',
            method: 'POST',
            speechTimeout: '2',
            language: 'en-US',
        })
        .play(
            retry ? `${awsS3Url}/askNameRetry.wav` : `${awsS3Url}/askName.wav`
        );

    twiml.redirect(`/twilio/askName?retry=true&retryCount=${retryCount + 1}`);

    res.type('text/xml');
    res.send(twiml.toString());
});

router.post('/confirmName', async (req, res) => {
    const twiml = new twilio.twiml.VoiceResponse();

    const recognizedName = req.body.SpeechResult?.replace(/.$/, '').trim();
    const digits = req.body.Digits;
    const callSid = req.body.CallSid;

    console.log('Received Request:', req.body);

    if (recognizedName) {
        recognizedNames.set(callSid, recognizedName);
        const text = `It sounds like you said --  ${recognizedName}. If that sounds right, please press 1 to confirm. Press 2 to try again, or press 3 to end the call.`;

        try {
            const response = await axios.post(
                `${baseUrl}/googleTTS/synthesize`,
                { text }
            );
            const audioUrl = response.data.url;

            twiml
                .gather({
                    action: '/twilio/handleConfirmation',
                    method: 'POST',
                    numDigits: 1,
                })
                .play(audioUrl);

            twiml.redirect('/twilio/confirmName');

            res.type('text/xml');
            res.send(twiml.toString());
            return;
        } catch (error) {
            console.error('Error during Google TTS synthesis:', error.message);
            twiml.hangup();
            res.status(500).send('Failed to synthesize speech.');
            return;
        }
    }

    if (digits) {
        twiml.redirect('/twilio/handleConfirmation');
    } else {
        twiml.redirect('/twilio/askName?retry=true');
    }

    res.type('text/xml');
    res.send(twiml.toString());
});

router.post('/handleConfirmation', (req, res) => {
    const twiml = new twilio.twiml.VoiceResponse();

    const digits = req.body.Digits;
    const callSid = req.body.CallSid;

    const confirmAudioUrl = `${awsS3Url}/thankYouGoodBye.wav`;
    const exitAudioUrl = `${awsS3Url}/exitThankYou.wav`;
    const retryInputAudioUrl = `${awsS3Url}/retryInput.wav`;

    switch (digits) {
        case '1': // User confirmed the name
            twiml.play(confirmAudioUrl);
            twiml.hangup();
            break;
        case '2': // User wants to retry
            twiml.redirect('/twilio/askName?retry=true');
            recognizedNames.delete(callSid);
            break;
        case '3': // User wants to exit
            twiml.play(exitAudioUrl);
            twiml.hangup();
            recognizedNames.delete(callSid);
            exitedCalls.set(callSid, true);
            break;
        default: // Invalid input, ask again
            twiml.play(retryInputAudioUrl);
            twiml.redirect('/twilio/confirmName');
            break;
    }

    res.type('text/xml');
    res.send(twiml.toString());
});

router.post('/callStatus', async (req, res) => {
    const callStatus = req.body.CallStatus;
    const callSid = req.body.CallSid;
    const pulseId = req.query.pulseId;
    const linkedPulseId = req.query.linkedPulseId;
    const exitedCall = exitedCalls.get(callSid);
    const answeredBy = amdResults.get(callSid);

    console.log('Full request body:', JSON.stringify(req.body, null, 2));
    console.log('Call SID:', req.body.CallSid);
    console.log('Call Status:', req.body.CallStatus);
    console.log('Call Duration:', req.body.CallDuration);
    console.log('Answered By::', answeredBy);

    if (pulseId && linkedPulseId) {
        try {
            let statusText;

            if (
                answeredBy &&
                [
                    'machine_start',
                    'machine_end_beep',
                    'machine_end_silence',
                    'machine_end_other',
                ].includes(answeredBy)
            ) {
                statusText = 'Incomplete: No Answer';
                console.log(
                    `Call ${callSid} ended due to machine detection: ${answeredBy}`
                );
            } else if (
                ['initiated', 'ringing', 'answered', 'in-progress'].includes(
                    callStatus
                )
            ) {
                statusText = `In Progress: ${callStatus}`;
            } else {
                switch (callStatus) {
                    case 'completed':
                        const recognizedName = recognizedNames.get(callSid);
                        if (recognizedName) {
                            await mondayService.updateMondayContactName(
                                pulseId,
                                recognizedName
                            );
                            statusText = 'Completed: Name Received';
                        } else if (exitedCall) {
                            statusText = 'Completed: Call Exited';
                        } else {
                            statusText = 'Completed';
                        }
                        break;
                    case 'no-answer':
                        statusText = 'Incomplete: No Answer';
                        break;
                    case 'busy':
                        statusText = 'Incomplete: Busy';
                        break;
                    case 'failed':
                        statusText = 'Incomplete: Failed';
                        break;
                    case 'canceled':
                        statusText = 'Incomplete: Canceled';
                        break;
                    default:
                        statusText = 'Unknown Call Completion Status';
                }
            }

            await mondayService.updateCallStatus(linkedPulseId, statusText);
            console.log(`Call status updated to: ${statusText}`);

            if (
                [
                    'completed',
                    'no-answer',
                    'busy',
                    'failed',
                    'canceled',
                ].includes(callStatus)
            ) {
                recognizedNames.delete(callSid);
                exitedCalls.delete(callSid);
                amdResults.delete(callSid);
            }
        } catch (error) {
            console.error(
                'Error updating call status on Monday.com:',
                error.message
            );
            await mondayService.updateCallStatus(
                linkedPulseId,
                'Update Status Error'
            );
        }
    } else {
        console.log(
            'Missing linkedPulseId or pulseId, cannot update Monday.com'
        );
    }

    res.status(200).send('Call status received and processed.');
});

export default router;

I’ve tried:

  1. Different machine detection configurations

  2. Setting timeOuts to try and wait for AMD and putting it in handleCall before the call is made, but this of course causes a poor experience for real call recipients

Any suggestions? I’m about to ditch Twilio altogether.

2

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật