I’ve been a huge advocate of Twilio for a long time now and I look forward to any opportunity I have to integrate the service into an artist campaign. For a few years now, Twilio has offered a product called Functions which allows you to write simple serverless functions right from the comfort of the Twilio dashboard. These functions can then be used to respond to events such as incoming calls or texts.

The Twilio function for our campaign is wildly simple. All it needs to do is prompt the user to leave a message, record that message for a certain duration, and then send that recording over to another (Netlify powered) serverless function. We can do this by simply defining a voice response in the form of TwiML. Since Twilio functions automatically inject Twilio’s node library, you can simply start writing the responses and then providing them to the callback. The key here is making sure the record action points to the next serverless function.

exports.handler = function(context, event, callback) {

let twiml = new Twilio.twiml.VoiceResponse(); twiml.say("What do the Dead Men Say?"); twiml.record({

action: NETLIFY_FUNCTION_URL,

method: "GET",

timeout: 5,

maxLength: 5

}); callback(null, twiml);

};

Channel Recording to Client

As I mentioned, Twilio will be sending the recording response (along with a URL to the actual recording) over to an awaiting Netlify serverless function. The point of this function is to send that sound URL to the actual web app. Rather than hurting my brain with sockets and the such, I just use Pusher to send data from the server to the client. First, you need to configure Pusher.

const Pusher = require('pusher') let pusher = new Pusher({

appId: process.env.PUSHER_APP_ID,

key: process.env.PUSHER_KEY,

secret: process.env.PUSHER_SECRET,

cluster: 'us2',

encrypted: true

})

Then, since I’ll be writing an asynchronous serverless function, let’s define a little helper function which wraps the Pusher trigger function in a Promise so we know when it completes. The trigger function accepts a channel name, event name, and the data package (message) itself.

const triggerEvent = (message) => {

return new Promise((resolve, revoke) => {

pusher.trigger(

'trivium',

'new-recording',

message,

(err, req, res) => {

if (err) {

revoke(err)

} resolve()

}

);

})

}

Finally, we can write our serverless function. First, we create a message object with the single property of url and give it the value of the recording url we can pull from the parameters of the incoming Twilio event. (Pro tip: Append .mp3 to receive an MP3 instead of WAV file from Twilio.) Then, we can call our triggerEvent function. Once this completes, we’ll actually return a body of TwiML XML for Twilio so it knows to hangup the call. Cool, right?

exports.handler = async (event, context) => {

let message = {

"url": `${event.queryStringParameters['RecordingUrl']}.mp3`

} try{

await triggerEvent(message) return {

statusCode: 200,

headers: {

'Content-Type': 'application/xml'

},

body: '<?xml version="1.0" encoding="UTF-8"?><Response><Hangup/></Response>'

}

} catch (err) {

return {

statusCode: 500,

body: err.toString()

}

}

}

Once again, our recording is on the move. This time it is headed to the client via Pusher so let’s head there next to receive the recording and finally play it back in the browser.

Transform and Playback Recording