Twilio Integration

Learn how to integrate Twilio webhooks with HooksDash for testing SMS and call events locally.

Setup

1. Create a HooksDash Endpoint

# Via CLI
hooksdash endpoints create --name "Twilio Development" --slug twilio-dev

# Your webhook URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/twilio-dev

2. Configure in Twilio

For Phone Numbers:

  1. Go to Twilio Console
  2. Navigate to Phone Numbers → Manage → Active numbers
  3. Click on your phone number
  4. Scroll to Messaging section
  5. Configure webhook: https://hooksdash-api.ibadsiddiqui.dev/api/h/twilio-dev
  6. Select HTTP POST
  7. Click Save

For Call Status:

  1. In the same phone number settings
  2. Scroll to Voice & Fax section
  3. Configure webhook for incoming calls or status callbacks
  4. Click Save

Implementation

SMS Handler

const express = require('express');
const twilio = require('twilio');

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post('/webhook', (req, res) => {
  const twiml = new twilio.twiml.MessagingResponse();

  const from = req.body.From;
  const body = req.body.Body;

  console.log(`SMS from ${from}: ${body}`);

  // Auto-reply logic
  if (body.toLowerCase().includes('help')) {
    twiml.message('Available commands: STATUS, HELP, INFO');
  } else if (body.toLowerCase().includes('status')) {
    twiml.message('System status: All services operational');
  } else {
    twiml.message('Thank you for your message!');
  }

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

app.listen(3000);

Voice Call Handler

const express = require('express');
const twilio = require('twilio');

const app = express();
app.use(express.urlencoded({ extended: false }));

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

  const from = req.body.From;
  console.log(`Call from ${from}`);

  // Greet the caller
  twiml.say(
    { voice: 'alice' },
    'Thank you for calling. Please leave a message after the beep.'
  );

  // Record the message
  twiml.record({
    maxLength: 30,
    action: '/recording',
  });

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

app.post('/recording', (req, res) => {
  const recordingUrl = req.body.RecordingUrl;
  console.log('Recording URL:', recordingUrl);

  const twiml = new twilio.twiml.VoiceResponse();
  twiml.say('Thank you. Goodbye!');

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

app.listen(3000);

TypeScript Implementation

import express from 'express';
import twilio from 'twilio';

const app = express();
app.use(express.urlencoded({ extended: false }));

interface TwilioSMSRequest extends express.Request {
  body: {
    MessageSid: string;
    From: string;
    To: string;
    Body: string;
    NumMedia?: string;
  };
}

app.post('/webhook', (req: TwilioSMSRequest, res) => {
  const twiml = new twilio.twiml.MessagingResponse();

  const { From, Body } = req.body;

  console.log(`SMS from ${From}: ${Body}`);

  // Process message
  const response = processMessage(Body);
  twiml.message(response);

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

function processMessage(message: string): string {
  const lowerMessage = message.toLowerCase();

  if (lowerMessage.includes('help')) {
    return 'Available commands: STATUS, HELP, INFO';
  }
  if (lowerMessage.includes('status')) {
    return 'System status: All services operational';
  }
  return 'Thank you for your message!';
}

app.listen(3000);

Common Webhook Parameters

SMS Webhooks

ParameterDescription
MessageSidUnique identifier for the message
FromPhone number that sent the message
ToYour Twilio phone number
BodyText content of the message
NumMediaNumber of media files attached
MediaUrl0URL of the first media file

Voice Webhooks

ParameterDescription
CallSidUnique identifier for the call
FromCaller's phone number
ToYour Twilio phone number
CallStatusStatus of the call (ringing, in-progress, completed)
Directioninbound or outbound

Testing Locally

1. Start Your Server

node server.js

2. Start HooksDash Tunnel

hooksdash forward --port 3000 --endpoint twilio-dev

3. Test SMS

Send an SMS to your Twilio phone number. The webhook will be forwarded to your local server!

4. Test Voice

Call your Twilio phone number to test voice webhooks.

Example Use Cases

SMS Bot

const commands = {
  subscribe: (from) => {
    // Add to subscription list
    subscriptions.add(from);
    return 'You are now subscribed to updates!';
  },
  unsubscribe: (from) => {
    // Remove from subscription list
    subscriptions.delete(from);
    return 'You have been unsubscribed.';
  },
  status: () => {
    return 'System status: All services operational';
  },
  help: () => {
    return 'Commands: SUBSCRIBE, UNSUBSCRIBE, STATUS, HELP';
  },
};

app.post('/webhook', (req, res) => {
  const twiml = new twilio.twiml.MessagingResponse();
  const { From, Body } = req.body;

  const command = Body.toLowerCase().trim();
  const handler = commands[command];

  const response = handler ? handler(From) : 'Unknown command. Text HELP for options.';

  twiml.message(response);

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

Two-Factor Authentication

const twilio = require('twilio');
const client = twilio(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_AUTH_TOKEN
);

// Send verification code
async function sendVerificationCode(phoneNumber) {
  const code = Math.floor(100000 + Math.random() * 900000);

  await client.messages.create({
    body: `Your verification code is: ${code}`,
    from: process.env.TWILIO_PHONE_NUMBER,
    to: phoneNumber,
  });

  return code;
}

// Verify code via SMS reply
app.post('/webhook', (req, res) => {
  const twiml = new twilio.twiml.MessagingResponse();
  const { From, Body } = req.body;

  const isValid = verifyCode(From, Body);

  if (isValid) {
    twiml.message('Verification successful!');
    // Grant access to user
  } else {
    twiml.message('Invalid code. Please try again.');
  }

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

Call Status Tracking

app.post('/call-status', (req, res) => {
  const { CallSid, CallStatus, CallDuration } = req.body;

  console.log(`Call ${CallSid} status: ${CallStatus}`);

  if (CallStatus === 'completed') {
    console.log(`Call duration: ${CallDuration} seconds`);
    // Log call details, update analytics, etc.
  }

  res.status(200).send('OK');
});

Best Practices

1. Validate Requests

Verify that requests come from Twilio:

const twilio = require('twilio');

app.post('/webhook', (req, res) => {
  const twilioSignature = req.headers['x-twilio-signature'];
  const url = `https://yourapp.com/webhook`;

  const isValid = twilio.validateRequest(
    process.env.TWILIO_AUTH_TOKEN,
    twilioSignature,
    url,
    req.body
  );

  if (!isValid) {
    return res.status(403).send('Invalid signature');
  }

  // Process webhook...
});

2. Respond with TwiML

Always respond with valid TwiML for SMS and voice webhooks:

app.post('/webhook', (req, res) => {
  const twiml = new twilio.twiml.MessagingResponse();
  twiml.message('Your response here');

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

3. Handle Media Messages

Check for media attachments in SMS:

app.post('/webhook', async (req, res) => {
  const { NumMedia } = req.body;

  if (parseInt(NumMedia) > 0) {
    const mediaUrl = req.body.MediaUrl0;
    console.log('Received media:', mediaUrl);
    // Download and process media
  }

  const twiml = new twilio.twiml.MessagingResponse();
  twiml.message('Media received!');

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

Troubleshooting

Webhook Not Receiving Messages

Issue: SMS sent to Twilio number but webhook not triggered

Solution:

  • Verify the webhook URL is configured in Twilio console
  • Check that HTTP POST is selected
  • Ensure the phone number has SMS capabilities
  • Check Twilio's debugger logs for delivery status

Invalid TwiML Response

Issue: 11200 HTTP retrieval failure error

Solution:

  • Ensure you're responding with valid TwiML XML
  • Set content type to text/xml
  • Return response within 10 seconds

Local Forwarding Issues

Issue: Webhook appears in HooksDash but not forwarded to localhost

Solution:

  • Ensure the CLI tunnel is running
  • Verify your local server is running and accessible
  • Check that you're using the correct port number

Resources