I’m having an issue with my Node.js server that automates my Twilio calls. I’m trying to update a Monday.com status based on call actions; however, when I hangup the call or don’t answer, the callStatus
endpoint is called with a complete
status, which isn’t true. Within Twilio’s documentation, there are final call statuses (no-answer
, complete
, failed
, and canceled
), I just can’t figure out how to get Twilio to send these to my endpoint.
I think it has to have something to do with my /initiateCall
endpoint. This receives a contactName
; if it was set in the call, it means we already have their name. Otherwise, the call needs to collect that data by calling the /askName
endpoint.
As a side-note, I’m using Google TTS to synthesize a more natural sounding voice. That’s what the .play()
calls are using.
router.post('/initiateCall', async (req, res) => {
const { text, to, contactName, pulseId, linkedPulseId } = req.body;
console.log('Request:', req.body);
if (!text || !to) {
return res
.status(400)
.send('Please provide both "text" and "to" in the request body.');
}
try {
const response = await axios.post(`${baseUrl}/googleTTS/synthesize`, {
text: text,
});
const audioUrl = response.data.url;
console.log('audioUrl:', audioUrl);
const twiml = new twilio.twiml.VoiceResponse();
if (contactName) {
twiml.play(audioUrl);
twiml.pause({ length: 1 }); // Optional pause for effect
} else {
twiml.play(audioUrl);
twiml.pause({ length: 1 });
twiml.redirect(`${baseUrl}/twilio/askName`);
}
const call = await client.calls.create({
from: process.env.TWILIO_PHONE_NUMBER,
to: to,
twiml: twiml.toString(),
statusCallback: `${baseUrl}/twilio/callStatus?pulseId=${pulseId}&linkedPulseId=${linkedPulseId}`,
statusCallbackEvent: [
'initiated',
'ringing',
'answered',
'completed',
'no-answer',
'busy',
'failed',
'canceled',
],
});
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.');
}
});
The /askName
route is below.
router.post('/askName', async (req, res) => {
const twiml = new twilio.twiml.VoiceResponse();
const retry = req.query.retry;
if (req.body.CallStatus && req.body.CallStatus !== 'in-progress') {
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');
res.type('text/xml');
res.send(twiml.toString());
});
I added a check for CallStatus
. Within the Twilio logs, I noticed that it was being called, despite the call never even being picked up (I hang up during the call).
Then I try to handle the statuses with my /callStatus
:
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);
console.log('Received Call Status:', callStatus);
console.log('Call SID:', callSid);
console.log('Call Request Body:', req.body);
if (pulseId && linkedPulseId) {
try {
let statusText;
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);
}
} 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.');
});
I can never get the callStatus to have a failed, incomplete status.
I’m not really sure where to go from here. I’ve been testing and playing around with it for most of the day. Any thoughts?
I tried using <Dial>
(twiml.dial()
), but I think that’s just used to connect an external call — or I’m way off with interpreting the documentation. I was using it to include a /finalStatus
endpoint to see if that would be called. I separated out the completion and in-progress statuses while using that, but the endpoint was never called at all, despite being accessible on the server.