code examples
code examples
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:
- Next.js Frontend: A web interface that lets users trigger outbound WhatsApp messages.
- 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:
mkdir nextjs-twilio-whatsapp-guide
cd nextjs-twilio-whatsapp-guide1.2. Initialize Backend (Node.js/Express):
Navigate into a new backend directory and initialize a Node.js project.
mkdir backend
cd backend
npm init -y1.3. Install Backend Dependencies:
Install Express for the server, the Twilio Node helper library, dotenv for environment variables, and nodemon for development auto-reloading.
npm install express twilio dotenv
npm install --save-dev nodemonRecommended 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:
{
"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.
mkdir src
mkdir src/routes
mkdir src/controllers
mkdir src/services
mkdir src/middlewaresrc/: 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.
# 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 neededCreate a .gitignore file in the backend directory to prevent sensitive files from being committed:
# backend/.gitignore
node_modules
.env
npm-debug.log1.7. Initialize Frontend (Next.js):
Navigate back to the root project directory and create the Next.js frontend app.
cd .. # Go back to nextjs-twilio-whatsapp-guide
npx create-next-app@latest frontendFollow 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).
# frontend/.env.local
# URL of your backend API (adjust if deployed)
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/apiEnsure .env.local is included in frontend/.gitignore (create the file if it doesn't exist).
# frontend/.gitignore
node_modules
.env*.local
# ... other ignores generated by create-next-appYour project structure should now look like this:
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.json2. 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:
- Log in to the Twilio Console.
- On the main dashboard, find your Account SID and Auth Token.
- Copy these values and paste them into your
backend/.envfile forTWILIO_ACCOUNT_SIDandTWILIO_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.
-
In the Twilio Console, navigate to Messaging > Try it out > Send a WhatsApp message.
-
Follow the on-screen instructions to activate the Sandbox. This usually involves selecting a Sandbox number.
-
Note the Sandbox Number (e.g.,
+14155238886). Add this number, prefixed withwhatsapp:, to yourbackend/.envfile asTWILIO_WHATSAPP_NUMBER.dotenvTWILIO_WHATSAPP_NUMBER=whatsapp:+14155238886 -
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:
- Open WhatsApp on your smartphone.
- Send a message containing your unique Sandbox Keyword (e.g.,
join velvet-unicorn) to the Twilio Sandbox Number. - 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:
- Images: JPG, JPEG, PNG
- Audio: AAC, AMR, MP3, MP4, OGG
- Documents: PDF
- Size Limit: 16 MB per message Source: Twilio WhatsApp API Documentation
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:
- Go to Messaging > Try it out > Send a WhatsApp message in the Twilio Console.
- Find the "Sandbox settings" tab.
- 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.
// 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.
// 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
mediaUrlparameter 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.
// 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/xmlorapplication/xml
Source: Twilio TwiML for Programmable Messaging
3.4. Define API Routes:
Create backend/src/routes/messageRoutes.js to map endpoints to controllers.
// 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.
// 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.
// 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:
// 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.
cd backend
npm run dev # Uses nodemon for auto-restartsThe 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.
cd ../frontend # Assuming you are in the backend directory
npm run devThe frontend starts on http://localhost:3000.
7.3. Test Sending Messages:
- Open
http://localhost:3000in your browser. - Enter your personal WhatsApp number (that you opted-in with) in E.164 format (e.g.,
+15551234567) in the "To Number" field. - Enter a message in the "Message" field.
- Click "Send Message".
- Check your WhatsApp – you should receive the message from the Twilio Sandbox number.
- 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):
-
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 3001Ngrok provides a public HTTPS URL (e.g.,
https://abcd-1234.ngrok.io). Copy this HTTPS URL. -
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.
-
Send a Message to Twilio:
- From your personal WhatsApp account, send any message to the Twilio Sandbox number.
-
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/twiliorequests with200 OKresponses. If you see errors (like 403), recheck the validation steps and ngrok URL.
- Check the terminal where your backend server (
8. Common Issues and Troubleshooting WhatsApp Integration
- Invalid Credentials: Ensure
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN, andTWILIO_WHATSAPP_NUMBERinbackend/.envare 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 withwhatsapp:when sending via the API. TheFromnumber in webhooks already has thewhatsapp: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_TOKENinbackend/.envis 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 tohttpvshttps. Check if a proxy/load balancer correctly setsx-forwarded-protoif you rely on it. - Ensure
express.urlencoded({ extended: true })middleware is used before your webhook route handler inbackend/src/server.js, as Twilio sends webhook data asapplication/x-www-form-urlencoded. It should appear beforeapp.use('/api', messageRoutes);if your webhook is under/api.
- Verify
- 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 bothbackendandfrontenddirectories. Checkpackage-lock.jsonoryarn.lockfor consistency. - API URL Mismatch: Verify
NEXT_PUBLIC_API_BASE_URLinfrontend/.env.localpoints precisely to the running backend API endpoint (e.g.,http://localhost:3001/apifor 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
corsnpm 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.
- Combine into one Vercel Project (Optional but common): Move the backend's API routes (
/api/send-message,/api/webhook/twilio) into the Next.jspages/apidirectory. This way, Vercel deploys both frontend and backend together.- Adjust imports (
../../services/twilioServiceetc.) and potentially merge dependencies into the rootpackage.jsonif you restructure. - The
twilioService.jsand middleware could live in aliborutilsfolder within the Next.js project. - Webhook validation needs careful path checking within the serverless function context.
- Adjust imports (
- Deploy Frontend to Vercel: Connect your Git repository (containing the
frontenddirectory, or the combined project) to Vercel. Vercel automatically detects Next.js and deploys it. - 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
backenddirectory as a standard Node.js application. Configure a build command (npm install) and a start command (npm start).
- 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. - 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. - 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_NUMBERaccordingly. This involves a more formal setup process.
Related Resources:
- How to send SMS with Twilio - Learn SMS integration with Twilio
- Building chatbots with Node.js - Extend your WhatsApp integration with AI
- Next.js API routes tutorial - Deep dive into Next.js backend patterns
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.