code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / twilio

Twilio WhatsApp API Integration: Node.js & Next.js Tutorial 2025

Learn how to integrate Twilio WhatsApp API with Next.js and Node.js. Step-by-step tutorial covering sandbox setup, webhooks, message templates, and production deployment with complete code examples.

Twilio WhatsApp Integration with Node.js & Next.js: Complete Implementation Guide

Learn how to build a production-ready WhatsApp messaging application using the Twilio WhatsApp API with Next.js and Node.js (Express). This comprehensive tutorial guides you through setting up the Twilio WhatsApp Sandbox, implementing webhooks for two-way messaging, and deploying your application to production.

By the end of this guide, you'll have a fully functional WhatsApp integration that sends messages from a web interface, processes incoming messages via webhooks, and handles media attachments. Perfect for building customer support systems, notification services, or automated WhatsApp chatbots.

WhatsApp Business API Requirements & Compliance

⚠️ Compliance Notice: WhatsApp enforces strict opt-in and messaging window requirements. Violations may result in account suspension.

Opt-In Requirement

WhatsApp requires explicit user opt-in before you can send messages. Gather opt-in consent via web page, mobile app, SMS, or during signup flow. Source: Twilio WhatsApp API Documentation

Warning: Sending messages without opt-in may result in users blocking your business and suspension of your WhatsApp Business Account.

24-Hour Customer Service Window

A customer service window begins when a user sends a message to your application. During the 24 hours following their most recent message, you can send freeform messages. Outside this window, use pre-approved message templates only. Source: Twilio WhatsApp Conversational Messaging

Prerequisites

  • Node.js: Version 14, 16, 18, 20, or 22 LTS installed. The Twilio Node.js library officially supports these versions. (Download) Source: Twilio Node.js Library Documentation
  • npm or yarn: Package manager for Node.js.
  • Twilio Account: Free or paid Twilio account. (Sign up)
  • WhatsApp Account: Active WhatsApp account on a smartphone for testing.
  • Ngrok (Recommended for Local Development): Expose your local server for Twilio webhooks. (Download)
  • Code Editor: Such as Visual Studio Code.
  • Meta Business Manager Account: Required for production WhatsApp Business Platform usage. Source: Twilio WhatsApp Self-Signup Guide

⚠️ Security Warning: Never use the Twilio Node.js library in a front-end application. Doing so exposes your Twilio credentials to end-users as part of the bundled HTML/JavaScript sent to their browser. Source: Twilio Node.js Security Guidelines

System Architecture

The application consists of two main parts:

  1. Next.js Frontend: A web interface that lets users trigger outbound WhatsApp messages.
  2. Node.js (Express) Backend: An API server that handles frontend requests, interacts with the Twilio API to send messages, and processes incoming message webhooks from Twilio.

Diagram Description: The user interacts with the Next.js frontend in their browser. The frontend sends API calls (e.g., /send-message) to the Node.js/Express backend API. The backend API communicates with the Twilio API to send a WhatsApp message to the end user. When the user replies via WhatsApp, Twilio sends a webhook notification (e.g., to /webhook/twilio) back to the Node.js/Express API. The API processes the incoming message, potentially sends a TwiML response back to Twilio, which then relays the reply message to the WhatsApp user.


1. Setting Up Your Next.js and Node.js Project Environment

Create a monorepo structure to manage both frontend and backend code within a single project directory.

1.1. Create Project Directory:

Open your terminal and create the main project directory:

bash
mkdir nextjs-twilio-whatsapp-guide
cd nextjs-twilio-whatsapp-guide

1.2. Initialize Backend (Node.js/Express):

Navigate into a new backend directory and initialize a Node.js project.

bash
mkdir backend
cd backend
npm init -y

1.3. Install Backend Dependencies:

Install Express for the server, the Twilio Node helper library, dotenv for environment variables, and nodemon for development auto-reloading.

bash
npm install express twilio dotenv
npm install --save-dev nodemon

Recommended Versions (as of January 2025):

  • twilio: ^5.0.0 (supports Node.js 14 – 22 LTS)
  • express: ^4.19.2 (stable release)
  • dotenv: ^16.4.5 (latest stable)
  • nodemon: ^3.1.0 (dev dependency)

Note: The Twilio Node.js library v5.x introduced breaking changes from v4.x. Ensure your code follows the v5.x API patterns. Source: Twilio Node.js Library

1.4. Configure Backend Scripts:

Open backend/package.json and add the following scripts:

json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "twilio": "^5.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

Note: These versions reflect January 2025 stable releases. Always verify compatibility with your Node.js version.

1.5. Create Backend Directory Structure:

Create the necessary directories for organizing the backend code.

bash
mkdir src
mkdir src/routes
mkdir src/controllers
mkdir src/services
mkdir src/middleware
  • src/: Contains all backend source code.
  • src/routes/: Defines API endpoints.
  • src/controllers/: Handles request logic.
  • src/services/: Encapsulates business logic (like interacting with Twilio).
  • src/middleware/: Contains Express middleware functions (e.g., error handling, validation).

1.6. Set up Environment Variables (.env):

Create a .env file in the backend directory. This file stores sensitive credentials and configuration. Never commit this file to version control.

dotenv
# backend/.env

# Twilio Credentials
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=YOUR_AUTH_TOKEN_HERE
TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886 # Your Twilio Sandbox number

# Server Configuration
PORT=3001 # Port for the backend API

# Add other environment variables as needed

Create a .gitignore file in the backend directory to prevent sensitive files from being committed:

text
# backend/.gitignore
node_modules
.env
npm-debug.log

1.7. Initialize Frontend (Next.js):

Navigate back to the root project directory and create the Next.js frontend app.

bash
cd .. # Go back to nextjs-twilio-whatsapp-guide
npx create-next-app@latest frontend

Follow the prompts (e.g., choosing TypeScript/JavaScript, ESLint, Tailwind CSS). This guide assumes you chose JavaScript when running create-next-app. The provided frontend code uses JavaScript and JSX (.js files). If you chose TypeScript (.tsx), adjust types accordingly, though the core logic remains similar.

1.8. Configure Frontend Environment Variables:

Next.js uses .env.local for environment variables. Create this file in the frontend directory. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser; others are only available server-side (during build or SSR/API routes).

dotenv
# frontend/.env.local

# URL of your backend API (adjust if deployed)
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api

Ensure .env.local is included in frontend/.gitignore (create the file if it doesn't exist).

text
# frontend/.gitignore
node_modules
.env*.local
# ... other ignores generated by create-next-app

Your project structure should now look like this:

text
nextjs-twilio-whatsapp-guide/
├── backend/
│   ├── node_modules/
│   ├── src/
│   │   ├── controllers/
│   │   ├── middleware/
│   │   ├── routes/
│   │   ├── services/
│   │   └── server.js  # (Create this next)
│   ├── .env
│   ├── .gitignore
│   └── package.json
└── frontend/
    ├── node_modules/
    ├── pages/
    ├── public/
    ├── styles/
    ├── .env.local
    ├── .gitignore
    ├── next.config.js
    └── package.json

2. Configuring Twilio WhatsApp Sandbox and API Credentials

Before writing code to interact with Twilio, configure your account and the WhatsApp Sandbox.

2.1. Obtain Your Twilio Account SID and Auth Token:

  1. Log in to the Twilio Console.
  2. On the main dashboard, find your Account SID and Auth Token.
  3. Copy these values and paste them into your backend/.env file for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.

2.2. Activate the Twilio WhatsApp Sandbox for Testing:

The Twilio WhatsApp Sandbox provides a free testing environment with a shared Twilio number, allowing you to develop and test WhatsApp messaging without requiring WhatsApp Business Account verification.

⚠️ Sandbox Limitations:

  • Development/testing only – not for production use
  • Uses a shared Twilio number across all Sandbox users
  • Requires explicit opt-in from recipients via keyword (e.g., join velvet-unicorn)
  • Limited to testing with opted-in phone numbers only
  • No custom branding or sender profile

For production applications, migrate to a dedicated WhatsApp Business Account (WABA). See Section 9 for production migration details.

  1. In the Twilio Console, navigate to Messaging > Try it out > Send a WhatsApp message.

  2. Follow the on-screen instructions to activate the Sandbox. This usually involves selecting a Sandbox number.

  3. Note the Sandbox Number (e.g., +14155238886). Add this number, prefixed with whatsapp:, to your backend/.env file as TWILIO_WHATSAPP_NUMBER.

    dotenv
    TWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886
  4. Note your unique Sandbox Keyword (e.g., join velvet-unicorn).

2.3. Opt-in to the Sandbox:

To receive messages from the Sandbox number on your personal WhatsApp account, opt-in:

  1. Open WhatsApp on your smartphone.
  2. Send a message containing your unique Sandbox Keyword (e.g., join velvet-unicorn) to the Twilio Sandbox Number.
  3. You should receive a confirmation message from Twilio.

⚠️ Recipient Opt-In Required: Every recipient phone number must send the join message to the Sandbox before you can send them messages. This satisfies WhatsApp's explicit opt-in requirement for testing.

2.4. WhatsApp Media Support:

WhatsApp messages via Twilio support the following media types:

Access incoming media via the MediaUrl0 field in webhook requests. See Section 3.3 for implementation details.

2.5. Configure the Webhook URL (Placeholder):

Twilio needs a URL to send notifications when your Sandbox number receives a message (incoming webhook). Set up the actual endpoint later, but know where to configure it:

  1. Go to Messaging > Try it out > Send a WhatsApp message in the Twilio Console.
  2. Find the "Sandbox settings" tab.
  3. Locate the field labeled "When a message comes in". You'll eventually put the public URL of your backend webhook endpoint here (e.g., https://your-ngrok-url.io/api/webhook/twilio). Generate this URL using ngrok during development.

⚠️ HTTPS Required: Twilio requires HTTPS for webhook URLs in production. Use proper SSL certificates (e.g., Let's Encrypt); Twilio rejects self-signed certificates. Source: Twilio Webhooks Security

Supported TLS Ciphers: Ensure your server supports Twilio's required TLS cipher suites. See Twilio TLS Cipher Documentation for the complete list.

WhatsApp Business Platform Limitations (Production)

As of January 2023, WhatsApp imposes the following limits on phone numbers and WhatsApp Business Accounts (WABAs):

Phone Number Limits per Meta Business Manager:

  • Unverified Meta Business Manager: Maximum 2 phone numbers across all WABAs
  • Verified Meta Business Manager: Up to 20 phone numbers (exception requests for 50 available via support ticket; higher limits require justification and WhatsApp approval)

WABA Limits:

  • Verified Meta Business Manager: Maximum 20 WABAs
  • Official Business Account (OBA): Up to 1,000 WABAs

Businesses onboarded before January 2023 with higher limits are generally exempted. Source: Twilio WhatsApp Business Platform Overview


3. Building the Express Backend API for WhatsApp Messaging

Build the Express API to handle sending and receiving messages.

3.1. Create Your Express Server with WhatsApp Endpoints:

Create the main server file backend/src/server.js.

javascript
// backend/src/server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const messageRoutes = require('./routes/messageRoutes'); // Create this next
const errorHandler = require('./middleware/errorHandler'); // Create this next

const app = express();
const PORT = process.env.PORT || 3001;

// Middleware
app.use(express.json()); // Parse JSON request bodies
// IMPORTANT: urlencoded middleware must come before routes that handle webhooks
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded request bodies (needed for Twilio webhooks)

// Basic Route for Testing
app.get('/', (req, res) => {
  res.send('Twilio WhatsApp Backend is running!');
});

// API Routes
app.use('/api', messageRoutes);

// Error Handling Middleware (Should be the LAST middleware applied)
app.use(errorHandler);

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

3.2. Implement the Twilio WhatsApp Service Layer:

Create a service to encapsulate Twilio interactions in backend/src/services/twilioService.js.

javascript
// backend/src/services/twilioService.js
const twilio = require('twilio');

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioWhatsAppNumber = process.env.TWILIO_WHATSAPP_NUMBER;

// Validate essential environment variables
if (!accountSid || !authToken || !twilioWhatsAppNumber) {
  console.error("Error: Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_NUMBER environment variables.");
  // In production, exit immediately to prevent undefined behavior
  // process.exit(1);
}

let client;
try {
    client = twilio(accountSid, authToken);
} catch (error) {
    console.error("Error initializing Twilio client:", error);
    // Handle client initialization error appropriately
}


/**
 * Sends a WhatsApp message using Twilio.
 * @param {string} to - The recipient's WhatsApp number (e.g., 'whatsapp:+15551234567').
 * @param {string} body - The message content.
 * @param {string[]} [mediaUrl] - Optional array of media URLs (max 16 MB per message).
 * @returns {Promise<object>} - The Twilio message object.
 * @throws {Error} - Twilio API errors (see error codes below).
 */
async function sendWhatsAppMessage(to, body, mediaUrl = []) {
  // Ensure client was initialized
  if (!client) {
      throw new Error("Twilio client not initialized. Check your credentials.");
  }

  console.log(`Sending message to ${to}: "${body}" from ${twilioWhatsAppNumber}`);

  try {
    const messageOptions = {
      body: body,
      from: twilioWhatsAppNumber,
      to: to,
    };

    // Add media URLs if provided
    if (mediaUrl && mediaUrl.length > 0) {
      messageOptions.mediaUrl = mediaUrl;
    }

    const message = await client.messages.create(messageOptions);
    console.log(`Message sent successfully. SID: ${message.sid}, Status: ${message.status}`);
    return message;
  } catch (error) {
    console.error(`Failed to send message to ${to}:`, error);
    // Log specific Twilio error details
    if (error.code) {
      console.error(`Twilio Error Code: ${error.code}${error.message}`);
      // Common error codes:
      // 21211: Invalid 'To' phone number
      // 21408: Permission to send to this number denied
      // 21610: Message cannot be sent (recipient not opted in)
      // 63007: Sandbox number not configured correctly
      // See: https://www.twilio.com/docs/api/errors
    }
    // Re-throw the error to be handled by the controller/error handler
    throw error;
  }
}

module.exports = {
  sendWhatsAppMessage,
  // Add other Twilio-related functions here if needed
};

Key Enhancements:

  • Added mediaUrl parameter support for sending images, audio, and PDFs
  • Added detailed error code logging for common Twilio errors
  • Added message status logging for debugging
  • Improved error messages with actionable details

Common Twilio Error Codes Source: Twilio Error Codes:

  • 21211: Invalid 'To' phone number format
  • 21408: Permission denied (number not authorized)
  • 21610: Attempt to send to unsubscribed recipient
  • 63007: Sandbox configuration error
  • 21614: 'To' number is not a valid mobile number

3.3. Create Message Controller:

Create backend/src/controllers/messageController.js to handle request logic.

javascript
// backend/src/controllers/messageController.js
const twilioService = require('../services/twilioService');
const { MessagingResponse } = require('twilio').twiml;

/**
 * Controller to handle sending an outbound WhatsApp message.
 */
async function sendOutboundMessage(req, res, next) {
  const { to, body } = req.body; // Expect 'to' (e.g., '+15551234567') and 'body'

  // Basic Input Validation
  if (!to || !body) {
    // Use status code 400 for Bad Request
    return res.status(400).json({ success: false, message: "Include both 'to' and 'body' in your request." });
  }

  // Basic E.164 format check
  if (!/^\+\d+$/.test(to)) {
      return res.status(400).json({ success: false, message: "Invalid phone number format. Use E.164 format (e.g., +15551234567)." });
  }

  // Add 'whatsapp:' prefix if not present
  const formattedTo = `whatsapp:${to}`;

  try {
    const message = await twilioService.sendWhatsAppMessage(formattedTo, body);
    res.status(200).json({ success: true, message: "Message sent successfully.", sid: message.sid });
  } catch (error) {
    // Pass error to the error handling middleware
    next(error);
  }
}

/**
 * Controller to handle incoming WhatsApp messages via Twilio webhook.
 */
function handleIncomingMessage(req, res, next) {
  // Twilio sends data in the request body (parsed by urlencoded middleware)
  const { From, Body } = req.body;

  if (!From || Body === undefined) { // Check for undefined Body as well
      console.warn("Received incomplete webhook request:", req.body);
      // Respond politely but indicate an issue processing
      res.writeHead(400, { 'Content-Type': 'text/xml' });
      res.end('<Response><Message>Could not process your message.</Message></Response>');
      return;
  }

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

  // --- Basic Echo Bot Logic ---
  // Create a TwiML response
  const twiml = new MessagingResponse();
  twiml.message(`Thanks for your message! You said: "${Body}"`);

  // Send the TwiML response back to Twilio
  res.writeHead(200, { 'Content-Type': 'text/xml' });
  res.end(twiml.toString());

  // --- Add more complex logic here ---
  // e.g., Parse the message, interact with a database, call another API, etc.
  // If processing takes time, respond immediately with 200 OK and process asynchronously.
}

module.exports = {
  sendOutboundMessage,
  handleIncomingMessage,
};

Why TwiML? Twilio Markup Language (TwiML) is an XML-based language that instructs Twilio on how to handle calls or messages. The MessagingResponse object generates this XML easily.

TwiML Constraints:

  • Maximum response time: 15 seconds (Twilio times out after 15 seconds)
  • Message body limit: 1,600 characters for WhatsApp
  • TwiML response must be valid XML
  • Content-Type must be text/xml or application/xml

Source: Twilio TwiML for Programmable Messaging

3.4. Define API Routes:

Create backend/src/routes/messageRoutes.js to map endpoints to controllers.

javascript
// backend/src/routes/messageRoutes.js
const express = require('express');
const messageController = require('../controllers/messageController');
const validateTwilioRequest = require('../middleware/validateTwilioRequest'); // Create this next

const router = express.Router();

// POST /api/send-message - Endpoint to trigger sending a message
router.post('/send-message', messageController.sendOutboundMessage);

// POST /api/webhook/twilio - Endpoint for Twilio to send incoming message events
// Apply Twilio request validation middleware ONLY to this route
router.post('/webhook/twilio', validateTwilioRequest, messageController.handleIncomingMessage);

module.exports = router;

4. Securing Your WhatsApp Webhooks with Twilio Request Validation

Verify that incoming webhook requests genuinely originate from Twilio.

4.1. Create Validation Middleware:

Create backend/src/middleware/validateTwilioRequest.js.

javascript
// backend/src/middleware/validateTwilioRequest.js
const twilio = require('twilio');

function validateTwilioRequest(req, res, next) {
  const twilioSignature = req.headers['x-twilio-signature'];
  const authToken = process.env.TWILIO_AUTH_TOKEN;

  if (!authToken) {
      console.error("TWILIO_AUTH_TOKEN is not set. Cannot validate request.");
      return res.status(500).send("Internal Server Error: Configuration missing.");
  }

  // Construct the full URL Twilio requested (protocol + host + original URL)
  // IMPORTANT: Relying on 'x-forwarded-proto' requires trusting the immediate upstream proxy (e.g., ngrok, load balancer) to set it correctly and securely. Misconfiguration can bypass validation.
  const protocol = req.headers['x-forwarded-proto'] || req.protocol;
  const url = `${protocol}://${req.get('host')}${req.originalUrl}`;

  // Use the raw body if available (some body parsers might interfere)
  // However, Twilio's helper expects the *parsed* key-value object for validation.
  const params = req.body || {}; // Use parsed body, ensure it's an object

  console.log('Validating Twilio Request:');
  console.log('  URL:', url);
  // Avoid logging sensitive params in production if possible
  // console.log('  Params:', params);
  console.log('  Signature:', twilioSignature);


  // Use the helper function to validate the request
  const requestIsValid = twilio.validateRequest(
    authToken,
    twilioSignature,
    url,
    params // Pass the parsed parameters
  );

  if (requestIsValid) {
    console.log('Twilio request is valid.');
    next(); // Proceed to the next middleware/controller
  } else {
    console.warn('Twilio request validation failed.');
    // Respond with 403 Forbidden if validation fails
    return res.status(403).type('text/plain').send('Forbidden: Invalid Twilio signature');
  }
}

module.exports = validateTwilioRequest;

Why is this critical? Without validation, anyone could send fake requests to your webhook endpoint, potentially triggering unwanted actions or exhausting resources. This middleware uses your Auth Token and the request details to compute a signature and compares it to the one sent by Twilio (x-twilio-signature header).

4.2. Apply Middleware to Route:

We already applied this middleware selectively to the /webhook/twilio route in messageRoutes.js.


5. Implementing Error Handling

Implement a consistent error handling strategy for robust applications.

5.1. Create Error Handling Middleware:

Create backend/src/middleware/errorHandler.js.

javascript
// backend/src/middleware/errorHandler.js

function errorHandler(err, req, res, next) {
  // Log the full error for debugging, especially in development
  console.error("An error occurred:", err);

  // Default error status and message
  let statusCode = 500;
  let message = "Internal Server Error";

  // Check for Twilio-specific errors (often have status and code)
  if (err.status && typeof err.status === 'number') {
    statusCode = err.status;
    message = err.message || "An error occurred while processing your request.";
    // Map certain Twilio error codes (err.code) to user-friendly messages
  } else if (err.name === 'ValidationError') { // Example: Handle Mongoose validation errors
      statusCode = 400;
      message = err.message;
  }
  // Add more specific error checks here (e.g., database errors, custom app errors)


  // Send a standardized JSON error response
  // Avoid sending stack trace in production
  res.status(statusCode).json({
      success: false,
      message: message,
      // Only include stack in development for debugging purposes
      stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
      // Optionally include an error code if available and useful
      code: err.code || undefined
  });
}

module.exports = errorHandler;

Why this approach? This middleware catches errors passed via next(error) from controllers or other middleware. It logs the error and sends a consistent JSON response to the client, preventing sensitive stack traces from leaking in production.

5.2. Apply Middleware:

We already applied this middleware as the last piece of middleware in server.js. Its position is important – define it after all routes.


6. Creating the Next.js Frontend for WhatsApp Messaging

Create a simple form in the Next.js app to send messages.

6.1. Modify Index Page:

Replace the content of frontend/pages/index.js (or index.tsx if using TypeScript) with the following:

jsx
// frontend/pages/index.js (or index.tsx)
import React, { useState } from 'react';

export default function HomePage() {
  const [toNumber, setToNumber] = useState('');
  const [messageBody, setMessageBody] = useState('');
  const [status, setStatus] = useState(''); // To show feedback
  const [isLoading, setIsLoading] = useState(false);

  const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

  const handleSubmit = async (event) => {
    event.preventDefault();
    setIsLoading(true);
    setStatus('Sending…');

    if (!apiBaseUrl) {
        setStatus('Error: Frontend API Base URL is not configured.');
        setIsLoading(false);
        console.error("NEXT_PUBLIC_API_BASE_URL is not set in frontend environment variables.");
        return;
    }

    // Basic frontend validation for E.164 format
    if (!/^\+\d{10,}$/.test(toNumber)) {
        setStatus('Error: Invalid phone number format. Use E.164 format (e.g., +15551234567).');
        setIsLoading(false);
        return;
    }


    try {
      const response = await fetch(`${apiBaseUrl}/send-message`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          // Send only the E.164 number; backend adds 'whatsapp:' prefix
          to: toNumber,
          body: messageBody,
        }),
      });

      const data = await response.json(); // Always try to parse JSON

      if (response.ok && data.success) {
        setStatus(`Message sent successfully! SID: ${data.sid}`);
        setToNumber(''); // Clear fields on success
        setMessageBody('');
      } else {
        // Use message from API response if available
        setStatus(`Error: ${data.message || response.statusText || 'Failed to send message.'}`);
        console.error("API Error Response:", data);
      }
    } catch (error) {
      console.error('Send message fetch error:', error);
      setStatus(`Error: ${error.message || 'Network error or server unavailable.'}`);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div style={{ padding: '20px', maxWidth: '500px', margin: 'auto' }}>
      <h1>Send WhatsApp Message via Twilio</h1>
      <form onSubmit={handleSubmit}>
        <div style={{ marginBottom: '15px' }}>
          <label htmlFor="toNumber" style={{ display: 'block', marginBottom: '5px' }}>
            To Number (E.164 format, e.g., +15551234567):
          </label>
          <input
            type="tel"
            id="toNumber"
            value={toNumber}
            onChange={(e) => setToNumber(e.target.value)}
            required
            placeholder="+15551234567"
            style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
          />
        </div>
        <div style={{ marginBottom: '15px' }}>
          <label htmlFor="messageBody" style={{ display: 'block', marginBottom: '5px' }}>
            Message:
          </label>
          <textarea
            id="messageBody"
            value={messageBody}
            onChange={(e) => setMessageBody(e.target.value)}
            required
            rows={4}
            style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
          />
        </div>
        <button
          type="submit"
          disabled={isLoading || !toNumber || !messageBody} // Also disable if fields are empty
          style={{
              padding: '10px 15px',
              cursor: (isLoading || !toNumber || !messageBody) ? 'not-allowed' : 'pointer',
              opacity: (isLoading || !toNumber || !messageBody) ? 0.6 : 1
            }}
        >
          {isLoading ? 'Sending…' : 'Send Message'}
        </button>
      </form>
      {status && (
        <p style={{ marginTop: '15px', border: '1px solid #ccc', padding: '10px', background: status.startsWith('Error:') ? '#ffe0e0' : '#e0ffe0' }}>
          Status: {status}
        </p>
      )}
       <p style={{ marginTop: '20px', fontSize: '0.9em', color: '#555' }}>
        Ensure your backend server is running ({apiBaseUrl || 'API URL missing!'}) and the recipient has opted-in to the Twilio Sandbox.
      </p>
    </div>
  );
}

Explanation: This React component creates a simple form. On submission, it makes a POST request to the backend's /api/send-message endpoint using the fetch API, sending the phone number and message body. It handles loading states and displays success or error messages.

Note on Styling: This example uses inline styles (style={{ … }}) for simplicity. For larger applications, consider using CSS Modules (built into Next.js) or a utility-first framework like Tailwind CSS (if selected during create-next-app) for better maintainability and organization.


7. Testing Your WhatsApp Integration with Ngrok

7.1. Start the Backend Server:

Open a terminal in the backend directory.

bash
cd backend
npm run dev # Uses nodemon for auto-restarts

The backend starts on http://localhost:3001 (or the port specified in .env).

7.2. Start the Frontend Development Server:

Open another terminal in the frontend directory.

bash
cd ../frontend # Assuming you are in the backend directory
npm run dev

The frontend starts on http://localhost:3000.

7.3. Test Sending Messages:

  1. Open http://localhost:3000 in your browser.
  2. Enter your personal WhatsApp number (that you opted-in with) in E.164 format (e.g., +15551234567) in the "To Number" field.
  3. Enter a message in the "Message" field.
  4. Click "Send Message".
  5. Check your WhatsApp – you should receive the message from the Twilio Sandbox number.
  6. Check the backend terminal – you should see logs for sending the message. If there's an error, check the browser console and backend logs for details.

7.4. Test Receiving Messages (Webhook):

  1. Expose Backend with Ngrok: If you haven't already, install ngrok. Open a third terminal and run:

    bash
    # Replace 3001 if your backend runs on a different port
    ngrok http 3001

    Ngrok provides a public HTTPS URL (e.g., https://abcd-1234.ngrok.io). Copy this HTTPS URL.

  2. Configure Twilio Webhook:

    • Go to Twilio Console > Messaging > Try it out > Send a WhatsApp message > Sandbox settings.
    • Paste the ngrok HTTPS URL into the "When a message comes in" field, appending your webhook path: https://<your-ngrok-id>.ngrok.io/api/webhook/twilio
    • Ensure the method is set to HTTP POST.
    • Click Save.
  3. Send a Message to Twilio:

    • From your personal WhatsApp account, send any message to the Twilio Sandbox number.
  4. Verify Reception:

    • Check the terminal where your backend server (npm run dev) is running. You should see logs:
      • Validating Twilio Request: (followed by details)
      • Twilio request is valid.
      • Incoming message from whatsapp:+15551234567: <Your message text>
    • Check your WhatsApp – you should receive the echo reply: Thanks for your message! You said: "<Your message text>"
    • Check the terminal where ngrok is running. You should see POST /api/webhook/twilio requests with 200 OK responses. If you see errors (like 403), recheck the validation steps and ngrok URL.

8. Common Issues and Troubleshooting WhatsApp Integration

  • Invalid Credentials: Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_NUMBER in backend/.env are correct and restart the server after changes. Check for typos.
  • Incorrect Phone Number Format: Twilio expects E.164 format (+ followed by country code and number, e.g., +14155238886) prefixed with whatsapp: when sending via the API. The From number in webhooks already has the whatsapp: prefix. Ensure the frontend sends the E.164 part (like +1…) and the backend adds the prefix.
  • Sandbox Opt-In: The recipient number must have sent the join <your-keyword> message to the Sandbox number first. Resend the join message if you haven't interacted recently or changed sandboxes.
  • Ngrok Issues: Ensure ngrok is running and forwarding to the correct backend port (e.g., 3001). The URL in Twilio must be the HTTPS version provided by ngrok and include the full path /api/webhook/twilio. Ngrok URLs change each time you restart it, so update Twilio accordingly.
  • Webhook Validation Failure (403 Forbidden):
    • Verify TWILIO_AUTH_TOKEN in backend/.env is absolutely correct.
    • Ensure the URL used in twilio.validateRequest (constructed within the middleware) exactly matches the URL Twilio sends the request to (check the ngrok terminal for the incoming request path). Pay attention to http vs https. Check if a proxy/load balancer correctly sets x-forwarded-proto if you rely on it.
    • Ensure express.urlencoded({ extended: true }) middleware is used before your webhook route handler in backend/src/server.js, as Twilio sends webhook data as application/x-www-form-urlencoded. It should appear before app.use('/api', messageRoutes); if your webhook is under /api.
  • Firewall Issues: If deploying, ensure your server's firewall allows incoming connections on the port your backend runs on (e.g., 3001 or 80/443) and specifically allows traffic from Twilio's IP ranges (though webhook validation is the primary security measure). Twilio IP Ranges
  • Dependencies: Install all dependencies (npm install) correctly in both backend and frontend directories. Check package-lock.json or yarn.lock for consistency.
  • API URL Mismatch: Verify NEXT_PUBLIC_API_BASE_URL in frontend/.env.local points precisely to the running backend API endpoint (e.g., http://localhost:3001/api for local dev, or your deployed backend URL). Restart the Next.js dev server after changing .env.local.
  • CORS Issues: If deploying frontend and backend to different domains, configure CORS (Cross-Origin Resource Sharing) on the backend (e.g., using the cors npm package in Express) to allow requests from your frontend's domain.

9. Deploying Your WhatsApp Application to Production

Deploy both the Next.js frontend and the Node.js backend to production for real-world WhatsApp messaging.

Recommended Deployment Strategy: Vercel for Next.js

Vercel provides seamless hosting for Next.js applications with built-in serverless function support, making it ideal for deploying both your frontend and backend API routes together.

  1. Combine into one Vercel Project (Optional but common): Move the backend's API routes (/api/send-message, /api/webhook/twilio) into the Next.js pages/api directory. This way, Vercel deploys both frontend and backend together.
    • Adjust imports (../../services/twilioService etc.) and potentially merge dependencies into the root package.json if you restructure.
    • The twilioService.js and middleware could live in a lib or utils folder within the Next.js project.
    • Webhook validation needs careful path checking within the serverless function context.
  2. Deploy Frontend to Vercel: Connect your Git repository (containing the frontend directory, or the combined project) to Vercel. Vercel automatically detects Next.js and deploys it.
  3. Deploy Backend (if separate):
    • Vercel Serverless: If using the combined approach above, it deploys automatically with the frontend.
    • Separate Hosting (e.g., Render, Fly.io, AWS EC2/Lambda, Google Cloud Run): Deploy the backend directory as a standard Node.js application. Configure a build command (npm install) and a start command (npm start).
  4. Environment Variables: Configure all necessary environment variables (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_NUMBER, NEXT_PUBLIC_API_BASE_URL – pointing to the deployed backend URL) in your hosting provider's settings (e.g., Vercel Environment Variables). Ensure frontend public variables (NEXT_PUBLIC_*) are configured correctly for the frontend deployment.
  5. Update Webhook URL: Once the backend deploys to a stable public URL, update the Twilio Sandbox webhook setting to point to this new URL (e.g., https://your-deployed-app.com/api/webhook/twilio). Ngrok is only for local development.
  6. Production Twilio Number: For production, move beyond the Sandbox. Purchase a Twilio phone number capable of WhatsApp and register it with the WhatsApp Business API via Twilio. Update TWILIO_WHATSAPP_NUMBER accordingly. This involves a more formal setup process.

Related Resources:


Frequently Asked Questions (FAQ)

How do I migrate from Twilio Sandbox to production WhatsApp?

To use WhatsApp in production, you need to register a WhatsApp Business Account (WABA) through Meta Business Manager and connect it to Twilio. This requires business verification and approval from WhatsApp. Visit the Twilio Console to start the registration process.

Can I send WhatsApp messages without user opt-in?

No. WhatsApp strictly requires explicit user consent before you can send them messages. Violating this policy can result in account suspension. Users must opt-in through your website, app, or by messaging your number first.

What are WhatsApp message templates and when do I need them?

Message templates are pre-approved message formats required for business-initiated messages outside the 24-hour customer service window. Templates are useful for appointment reminders, order confirmations, and notifications.

How much does it cost to send WhatsApp messages via Twilio?

Twilio charges per message sent and received. Pricing varies by country and message type (session messages vs. template messages). Check Twilio's pricing page for current rates.

Can I send images and files via WhatsApp API?

Yes, the Twilio WhatsApp API supports sending images (JPG, PNG), audio files (MP3, AAC), documents (PDF), and more, up to 16 MB per message.


This comprehensive guide covered everything from initial setup to production deployment of a Twilio WhatsApp integration using Next.js and Node.js. You now have the foundation to build sophisticated WhatsApp messaging applications for customer engagement, support automation, and notification systems.

Frequently Asked Questions

How to send a WhatsApp message with Twilio and Next.js?

Create a Next.js frontend with a form to collect the recipient's number and message. This frontend sends a POST request to your Node.js backend, which uses the Twilio Node.js library to send the message via the Twilio API. The backend must have your Twilio credentials configured as environment variables. Remember, for testing you'll use the Twilio Sandbox number.

What is the purpose of ngrok in Twilio WhatsApp integration?

Ngrok creates a public, secure tunnel to your locally running backend server, essential for receiving Twilio webhooks during development. Twilio needs a public URL to send webhook notifications when messages arrive at your WhatsApp Sandbox number. Since your local server isn't publicly accessible, ngrok bridges this gap.

Why does Twilio require webhook validation for WhatsApp messages?

Webhook validation ensures that incoming requests to your backend actually originate from Twilio and haven't been spoofed by a malicious third party. The validation process involves cryptographic signatures to verify the authenticity of the request, protecting your application from unauthorized access and actions.

When should I use a Twilio Sandbox for WhatsApp?

Use the Twilio Sandbox during development and testing of your WhatsApp integration. It provides a shared WhatsApp number and doesn't require the full WhatsApp Business API setup. However, remember that Sandbox numbers require explicit opt-in from users, and the Sandbox has other limitations compared to a full production WhatsApp number.

Can I integrate Twilio WhatsApp with a Next.js frontend?

Yes, you can effectively combine Twilio WhatsApp with a Next.js frontend. The frontend handles user interaction, sending messages and receiving status updates, while a Node.js/Express backend manages the Twilio API interaction and webhooks, providing a user-friendly and responsive messaging experience.

What is TwiML, and how is it used with WhatsApp?

TwiML (Twilio Markup Language) is an XML-based language used to instruct Twilio how to respond to messages. In a WhatsApp context, TwiML lets you define automated replies, create interactive menus, and control various aspects of message handling logic. It's used in your backend to dynamically generate responses to incoming messages.

How to set up a Twilio WhatsApp Sandbox?

Go to the Twilio Console, navigate to Messaging > Try it out > Send a WhatsApp message, and follow the instructions to activate the Sandbox. This involves selecting a Sandbox number and noting your unique Sandbox keyword, which recipients will use for opting in.

What is the architecture of a Next.js, Node.js, Twilio application?

It involves a Next.js frontend, a Node.js/Express backend, and the Twilio API. The frontend sends API calls to the backend, which interacts with Twilio to send messages. Twilio sends webhooks back to the backend when a message arrives for the application.

How to receive incoming WhatsApp messages with Twilio?

Twilio sends incoming messages as webhooks to a URL you specify in your Sandbox settings. Configure your backend to handle POST requests to a specific endpoint and ensure to verify the authenticity of these requests with webhook validation to maintain security and prevent misuse.

How to handle errors in Twilio WhatsApp integration?

Implement a robust error handling middleware in your Node.js backend using try-catch blocks. Handle common Twilio-related errors (like invalid recipient numbers or incorrect credentials) and provide informative error messages. Logging errors properly helps diagnose issues.

How to opt a user into my Twilio WhatsApp Sandbox?

Users must send a WhatsApp message to the Sandbox number. This message must contain the unique Sandbox keyword provided by Twilio when you activate the Sandbox. Users will then receive a confirmation, and you'll be able to send messages to their number.

What are the prerequisites for integrating Twilio WhatsApp?

You'll need a Twilio account, an active WhatsApp account, and a smartphone for testing. On the development side, install Node.js, npm or yarn, and choose a code editor. For local testing, ngrok is highly recommended. Ngrok creates a public tunnel to your local development server for testing webhooks from Twilio.

Why does my frontend need environment variables in a Twilio app?

Frontend environment variables securely store configuration values like the backend API URL, preventing the exposure of sensitive information like API keys within the client-side code. Next.js uses files like `.env.local` for this purpose.

How to secure Twilio webhooks?

Implement Twilio's request validation middleware in your Node.js/Express backend. This middleware verifies that incoming webhook requests are indeed from Twilio, protecting your application from unauthorized actions. Always use HTTPS for your backend server, as it's a fundamental requirement for any webhook endpoint.