code examples
code examples
How to Send WhatsApp Messages with Node.js and MessageBird (2025 Guide)
Step-by-step tutorial: Build a Node.js Express app to send and receive WhatsApp messages programmatically using MessageBird API. Includes webhook setup, authentication, and production deployment.
MessageBird WhatsApp Integration with Node.js Express: Complete Implementation Guide
Important Branding Update: MessageBird rebranded to "Bird" in February 2024 (source). The company maintains legacy developer documentation at
developers.messagebird.comwhile new API documentation is atdocs.bird.com. The Node.js SDK package name remainsmessagebirdon npm. This guide uses the establishedmessagebirdpackage and references, but be aware that official support and new features are now under the Bird brand.
Note: While this guide's filename references Next.js and NextAuth, the implementation uses Express.js. If you need Next.js App Router (v14/15) or NextAuth.js v5 integration, you'll need to adapt this Express.js foundation to Next.js Route Handlers (app/api/*/route.ts). The core MessageBird integration patterns remain the same.
Learn how to send WhatsApp messages programmatically from your Node.js application using the MessageBird WhatsApp Business API. This comprehensive tutorial walks you through building a production-ready Express server that handles both outbound and inbound WhatsApp messaging.
By the end of this guide, you will have a working Node.js application that can:
- Send WhatsApp messages (text and templates) programmatically via the MessageBird API
- Receive incoming WhatsApp messages securely through webhook endpoints
- Verify webhook authenticity using HMAC-SHA256 signature validation
- Handle errors and implement production-grade logging
This integration enables you to automate WhatsApp business messaging for use cases like customer notifications, two-factor authentication, support automation, and conversational commerce—all from your Node.js backend.
What You'll Build
Goal: Create a Node.js/Express server that sends and receives WhatsApp messages programmatically through the MessageBird Conversations API.
Technologies:
- Node.js: JavaScript runtime for server-side application development
- Express.js: Minimal web framework for building REST APIs and webhook handlers
- MessageBird (now Bird): Communications platform providing WhatsApp Business API access (API documentation)
messagebirdNode.js SDK: Official SDK version 4.0.1 (released January 25, 2023) (GitHub). Note: This SDK hasn't been updated since January 2023. For production applications, monitor the repository for updates or consider direct REST API callsdotenv: Environment variable management for secure credential storagengrok: Development tool to expose localhost to the internet for webhook testing
System Architecture:
The typical message flow works as follows:
- Your application sends an HTTP POST request to your Node.js API endpoint (e.g.,
/send-whatsapp) - The Express server uses the MessageBird SDK to call the MessageBird Conversations API
- MessageBird delivers the WhatsApp message to the recipient
- When users reply, WhatsApp routes the message to MessageBird
- MessageBird sends a webhook POST request to your public URL (e.g.,
/webhook) - Your Node.js API verifies the webhook signature and processes the incoming message
Prerequisites:
- Node.js and npm: Version 14 or higher installed on your machine (Download Node.js)
- MessageBird Account: Free account to access the WhatsApp Business API (Sign up)
- WhatsApp Business Channel: Approved WhatsApp channel in your MessageBird account with a Channel ID (Setup guide)
- MessageBird API Key: Live API key from your dashboard (required for authentication)
- Webhook Testing Tool:
ngrokor similar for local development, HTTPS URL for production - JavaScript Knowledge: Basic understanding of Node.js, Express, and async/await patterns
Final Outcome: A production-ready Node.js server with endpoints to send outgoing WhatsApp messages and receive incoming messages via webhooks.
1. Setting Up Your Node.js WhatsApp Project
Initialize the Node.js project and install the necessary dependencies.
Step 1: Create Project Directory and Initialize
Open your terminal and run the following commands:
mkdir nodejs-messagebird-whatsapp
cd nodejs-messagebird-whatsapp
npm init -yThis creates a new directory, navigates into it, and initializes a package.json file with default settings.
Step 2: Install Dependencies
Install Express, the MessageBird SDK, and dotenv:
npm install express messagebird dotenvexpress: The web framework.messagebird: The official Node.js SDK for interacting with the MessageBird API.dotenv: To manage environment variables securely.
Step 3: Create Project Structure
Create the basic files and directories:
touch server.js .env .gitignoreYour initial structure should look like this:
nodejs-messagebird-whatsapp/
├── node_modules/
├── .env
├── .gitignore
├── package.json
├── package-lock.json
└── server.js
Step 4: Configure .gitignore
Add node_modules and .env to your .gitignore file to prevent committing them to version control:
# .gitignore
node_modules/
.envStep 5: Set Up Environment Variables
Open the .env file and add the following variables. Retrieve these values from your MessageBird Dashboard.
# .env
# Found in Dashboard → Developers → API access → Show key
MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY
# Found in Dashboard → Channels → WhatsApp → Your Channel Settings
MESSAGEBIRD_WHATSAPP_CHANNEL_ID=YOUR_WHATSAPP_CHANNEL_ID
# Generate a secure random string for webhook verification.
# Configure this exact string in the MessageBird Dashboard webhook settings.
MESSAGEBIRD_WEBHOOK_SIGNING_SECRET=YOUR_SECURE_RANDOM_WEBHOOK_SECRET
# The port your application will run on
PORT=3000MESSAGEBIRD_API_KEY: Go to your MessageBird Dashboard → Developers → API access. Use your Live API key.MESSAGEBIRD_WHATSAPP_CHANNEL_ID: Go to Channels → WhatsApp → Click on your configured channel. The Channel ID is displayed there.MESSAGEBIRD_WEBHOOK_SIGNING_SECRET: Create a strong, unique secret (e.g., using a password generator). Configure this exact secret in the MessageBird dashboard when setting up your webhook. This is crucial for security.PORT: The local port your Express server will listen on.
Step 6: Basic Express Server Setup
Open server.js and add the initial Express server configuration:
// server.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); // Initialize MessageBird SDK
const app = express();
const port = process.env.PORT || 3000;
// Middleware to parse JSON request bodies
app.use(express.json());
// Simple health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Start the server
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
// Verify environment variables are loaded (optional check)
if (!process.env.MESSAGEBIRD_API_KEY || !process.env.MESSAGEBIRD_WHATSAPP_CHANNEL_ID || !process.env.MESSAGEBIRD_WEBHOOK_SIGNING_SECRET) {
console.warn('One or more required environment variables are missing. Check your .env file.');
} else {
console.log('MessageBird API Key and Channel ID loaded.');
}
});Explanation:
require('dotenv').config();loads the variables from your.envfile. It's crucial to call this early.require('messagebird')(process.env.MESSAGEBIRD_API_KEY)initializes the MessageBird client with your API key.express()creates an Express application instance.app.use(express.json());enables the server to parse incoming JSON payloads, which we'll use for our API endpoint and webhook handler.- A basic
/healthendpoint is included as good practice for monitoring. app.listenstarts the server. We add a check to ensure the essential environment variables are loaded.
You can now run your basic server:
node server.jsYou should see Server listening on port 3000 and confirmation that the environment variables loaded.
2. Sending WhatsApp Messages Programmatically
Now we'll implement the core functionality to send WhatsApp messages through the MessageBird API.
Step 1: Create the Sending Function
Add a function within server.js to handle the API call to MessageBird. We'll use the conversations.start method for simplicity, which can initiate a conversation (often requires a pre-approved template if outside the 24-hour customer service window) or send a freeform message if within the window.
// server.js
// ... (previous code: dotenv, express, messagebird init, app init, port)
// Middleware
app.use(express.json());
// --- Sending Logic ---
/**
* Sends a WhatsApp message via MessageBird Conversations API.
* @param {string} recipient - The recipient's phone number in E.164 format (e.g., +14155552671).
* @param {string} text - The message content.
* @returns {Promise<object>} - The MessageBird API response object.
*/
async function sendWhatsAppMessage(recipient, text) {
console.log(`Attempting to send message to: ${recipient}`);
const params = {
to: recipient,
from: process.env.MESSAGEBIRD_WHATSAPP_CHANNEL_ID, // Your WhatsApp Channel ID
type: 'text',
content: {
text: text,
},
// Optional: Fallback channel if WhatsApp fails (requires configuration)
// reportUrl: 'YOUR_STATUS_REPORT_WEBHOOK_URL' // Optional: URL for status updates
};
return new Promise((resolve, reject) => {
messagebird.conversations.start(params, (err, response) => {
if (err) {
console.error('Error sending MessageBird message:', err);
// More specific error handling can be added here based on err.errors
return reject(err);
}
console.log('MessageBird API Response:', response);
resolve(response);
});
});
}
// --- API Endpoint ---
// (We will add the endpoint in the next section)
// --- Webhook Handler ---
// (We will add the webhook handler later)
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Start the server
app.listen(port, () => {
// ... (startup logging as before)
});Explanation:
- The
sendWhatsAppMessagefunction takes therecipientnumber andtextmessage. - It constructs the
paramsobject required by the MessageBirdconversations.startmethod.to: The destination WhatsApp number (must be in E.164 format).from: Your unique MessageBird WhatsApp Channel ID (loaded from.env).type: Specifies the message type (textfor now). Other types includehsm(for templates),image,video,audio,file,location.content: An object containing the message payload, specific to thetype. Fortext, it's{ text: '...' }.
messagebird.conversations.startinitiates the API call. It uses a callback pattern. We wrap it in aPromisefor easier use withasync/awaitin our API endpoint later.- Basic error logging is included in the callback. More detailed error handling based on
err.errorscould be implemented.
Important Note on Templates (HSMs) and the 24-Hour Window:
-
24-Hour Customer Service Window: According to WhatsApp Business Policy, you may reply to a user message without using a Message Template as long as it's within 24 hours of the last user message (WhatsApp Business Messaging Policy). Within this window, you can send free-form session messages. Outside this window, you must use pre-approved Message Templates.
-
Message Templates Required Outside 24-Hour Window: To initiate a conversation with a user who hasn't messaged you in the last 24 hours, WhatsApp requires you to use a pre-approved Message Template (also known as HSM - Highly Structured Message) (Meta WhatsApp Template Guidelines).
-
To send a template, you would change
typeto'hsm'and modify thecontentobject accordingly. Example:javascriptcontent: { hsm: { namespace: 'YOUR_TEMPLATE_NAMESPACE', // Found in MessageBird Dashboard templateName: 'your_template_name', language: { policy: 'deterministic', code: 'en_US' // Or other language code }, // Optional: Add components for variables, buttons, headers // components: [ ... ] } } -
Managing and submitting templates is done through the MessageBird Dashboard. (MessageBird Templates Guide)
-
For this guide's simplicity, we focus on the
texttype, assuming you're either replying within the 24-hour window or testing with a number where you've initiated contact manually first.
3. Building the API Layer to Send Messages
Let's create an HTTP endpoint that triggers the sendWhatsAppMessage function.
Step 1: Add the POST Endpoint
Add the following route handler in server.js before the app.listen call:
// server.js
// ... (previous code: includes, messagebird init, app init, middleware, sendWhatsAppMessage func)
// --- API Endpoint ---
app.post('/send-whatsapp', async (req, res) => {
const { recipient, message } = req.body; // Get recipient number and message from request body
// Basic Validation
if (!recipient || !message) {
return res.status(400).json({ error: 'Missing required fields: recipient and message' });
}
// Basic E.164 format check (can be improved)
if (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
return res.status(400).json({ error: 'Invalid recipient format. Use E.164 format (e.g., +14155552671).' });
}
try {
const response = await sendWhatsAppMessage(recipient, message);
res.status(200).json({ success: true, messageId: response.id, status: response.status });
} catch (error) {
console.error('API Error sending WhatsApp message:', error);
// Provide a generic error message to the client
res.status(500).json({ success: false, error: 'Failed to send WhatsApp message', details: error.message || error });
}
});
// --- Webhook Handler ---
// (We will add the webhook handler later)
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Start the server
app.listen(port, () => {
// ... (startup logging as before)
});Explanation:
app.post('/send-whatsapp', ...)defines a route that listens for POST requests on the/send-whatsapppath.const { recipient, message } = req.body;extracts the phone number and message content from the JSON request body.- Input Validation: Basic checks are added to ensure
recipientandmessageare provided and that therecipientlooks like an E.164 number. You should implement more robust validation in a production environment (e.g., using libraries likeexpress-validator). - The
sendWhatsAppMessagefunction is called usingawait. - If successful, a
200 OKresponse is sent back with the MessageBird message ID and status. - If an error occurs during the API call (caught by
catch), a500 Internal Server Erroris returned with error details (be cautious about exposing too much detail in production errors).
Step 2: Test the Sending Endpoint
-
Restart your server: If it's running, stop it (
Ctrl+C) and restart:node server.js. -
Use
curlor Postman: Send a POST request to your running server. Replace+YOUR_TEST_WHATSAPP_NUMBERwith a real WhatsApp number you can check (in E.164 format).Using
curl:bashcurl -X POST http://localhost:3000/send-whatsapp \ -H "Content-Type: application/json" \ -d '{ "recipient": "+YOUR_TEST_WHATSAPP_NUMBER", "message": "Hello from my Node.js MessageBird App! (Test)" }'Expected Response (Success):
json{ "success": true, "messageId": "mb_message_id_string", "status": "pending" }Expected Response (Error - e.g., Bad Number Format):
json{ "error": "Invalid recipient format. Use E.164 format (e.g., +14155552671)." } -
Check WhatsApp: You should receive the message on the test number shortly after a successful API call. Check your server logs (
console.log) for output from thesendWhatsAppMessagefunction and the MessageBird API response.
4. Receiving WhatsApp Messages with Webhooks
To receive incoming WhatsApp messages, you'll set up a webhook endpoint. MessageBird sends an HTTP POST request to your specified URL whenever a new message arrives, enabling two-way WhatsApp conversations.
Step 1: Expose Local Server with ngrok (Development Only)
During development, your localhost server isn't accessible from the public internet. ngrok creates a secure tunnel. Remember, ngrok provides temporary URLs suitable only for development; a permanent public URL is needed for production.
-
Install
ngrok: Follow instructions at ngrok.com. -
Run
ngrok: Open a new terminal window (keep your Node server running) and startngrok, pointing it to the port your Node app is running on (default 3000):bashngrok http 3000 -
Copy the HTTPS URL:
ngrokwill display forwarding URLs. Copy thehttpsURL (e.g.,https://random-string.ngrok-free.app). This is your public webhook URL for now.
Step 2: Configure Webhook in MessageBird Dashboard
- Navigate to your MessageBird Dashboard.
- Go to Channels in the left sidebar.
- Click on your configured WhatsApp channel.
- Scroll down to the Webhooks section.
- Click "Add webhook" or edit an existing one.
- URL: Paste the
https://URL fromngrok, followed by the path for your webhook handler (we'll use/webhook). Example:https://random-string.ngrok-free.app/webhook - Events: Select at least
message.createdandmessage.updated(for status updates). - Signing Key: This is critical for security. Paste the exact same secret you defined as
MESSAGEBIRD_WEBHOOK_SIGNING_SECRETin your.envfile. - Save the webhook configuration.
Step 3: Implement the Webhook Handler in Node.js
Add the following code to server.js to create the /webhook endpoint and handle incoming requests. This includes crucial signature verification based on Bird's official documentation.
Important: The webhook signature verification method has been updated to match Bird's official documentation (as of November 2024).
// server.js
// ... (previous code: includes, messagebird init, app init, middleware, send func, API endpoint)
const crypto = require('crypto'); // Node.js crypto module for verification
// --- Webhook Handler ---
// Middleware specific to the webhook route for raw body parsing
// We need the raw body buffer for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['messagebird-signature'];
const timestamp = req.headers['messagebird-request-timestamp'];
const requestBody = req.body; // This is a Buffer due to express.raw()
const requestUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; // Reconstruct full URL
if (!signature || !timestamp || !requestBody) {
console.warn('Webhook received without signature, timestamp, or body.');
return res.status(400).send('Missing signature or timestamp');
}
// 1. Verify Timestamp (Optional but recommended)
// Helps prevent replay attacks by ensuring the request is recent.
const now = Math.floor(Date.now() / 1000);
const fiveMinutes = 5 * 60; // 300 seconds tolerance
if (Math.abs(now - parseInt(timestamp, 10)) > fiveMinutes) {
console.warn(`Webhook timestamp deviation too large. Timestamp: ${timestamp}, Now: ${now}. Potential replay attack.`);
// Decide whether to reject or just log based on your tolerance for clock skew.
// Rejecting is generally safer.
// return res.status(400).send('Timestamp deviation too large');
}
try {
// 2. Base64 decode the signature from the header
const receivedDecodedSignature = Buffer.from(signature, 'base64');
// 3. Create SHA256 hash checksum of the request body as binary
const bodyHash = crypto.createHash('sha256').update(requestBody).digest(); // Binary buffer
// 4. Join timestamp, request URL, and body hash with newlines
// Format: timestamp\nurl\nbodyHash (as binary)
const payload = Buffer.concat([
Buffer.from(timestamp + '\n'),
Buffer.from(requestUrl + '\n'),
bodyHash
]);
// 5. Calculate HMAC-SHA256 using signing key
const signingSecret = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_SECRET;
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(payload)
.digest(); // Binary buffer
// 6. Compare signatures securely using constant-time comparison
if (receivedDecodedSignature.length !== expectedSignature.length ||
!crypto.timingSafeEqual(receivedDecodedSignature, expectedSignature)) {
console.error('Webhook signature verification failed!');
return res.status(403).send('Invalid signature');
}
} catch (e) {
// Handle potential errors during signature verification
console.error('Error verifying signature:', e);
return res.status(400).send('Invalid signature format');
}
// Signature is valid, proceed with processing the message
console.log('Webhook signature verified successfully!');
try {
const eventData = JSON.parse(requestBody.toString('utf8')); // Parse the JSON body *after* verification
console.log('Webhook Received Event Type:', eventData.type);
// console.log('Webhook Payload:', JSON.stringify(eventData, null, 2)); // Log the full payload if needed
if (eventData.type === 'message.created') {
const message = eventData.message;
const sender = message.from; // The end-user's WhatsApp number/ID
const contactId = eventData.contact?.id; // MessageBird Contact ID
const conversationId = eventData.conversation?.id; // MessageBird Conversation ID
console.log(`Incoming message from ${sender} (Contact ID: ${contactId}, Conv ID: ${conversationId}):`);
console.log(` -> Type: ${message.type}, Content:`, message.content);
// --- Add your business logic here ---
// Example: Look up user by sender number, save message to DB, trigger an automated reply, etc.
// if (message.type === 'text' && message.content.text?.toLowerCase() === 'hello') {
// sendWhatsAppMessage(sender, 'Hi there! You said hello.');
// }
// -------------------------------------
} else if (eventData.type === 'message.updated') {
// Handle status updates (e.g., delivered, read) for messages you sent
const messageId = eventData.message?.id;
const status = eventData.message?.status;
console.log(`Message status update for ID ${messageId}: ${status}`);
// Update message status in your database using messageId
} else {
console.log(`Received unhandled event type: ${eventData.type}`);
}
// Acknowledge receipt to MessageBird quickly.
// Perform time-consuming tasks asynchronously if needed.
res.status(200).send('OK');
} catch (parseError) {
console.error('Failed to parse webhook body:', parseError);
res.status(400).send('Invalid JSON payload');
}
});
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Start the server
app.listen(port, () => {
// ... (startup logging as before)
});Explanation of Webhook Handler & Verification:
cryptoModule: Imported for cryptographic functions (HMAC-SHA256).express.raw({ type: 'application/json' }): This middleware is applied only to the/webhookroute. It preventsexpress.json()(which we applied globally earlier) from parsing the body for this route. We need the raw, unparsed request body (as a Buffer) to correctly verify the signature.- Extract Headers and URL: Get the
messagebird-signatureandmessagebird-request-timestampheaders sent by MessageBird. Reconstruct the full request URL as required by Bird's verification method. - Timestamp Verification (Optional but Recommended): Check if the timestamp is recent (e.g., within 5 minutes) to prevent replay attacks. Rejecting old timestamps adds a layer of security.
- Signature Verification Process (updated to match Bird's official documentation):
- Base64 decode the signature from the header
- Create a SHA256 hash of the request body (as binary)
- Concatenate:
timestamp + "\n" + requestUrl + "\n" + bodyHashBinary - Calculate HMAC-SHA256 of the concatenated payload using your signing secret
- Compare using
crypto.timingSafeEqualto prevent timing attacks
- Process Valid Webhook: If signatures match:
- Parse the raw body buffer into a JSON object:
JSON.parse(requestBody.toString('utf8')). - Log the event type and payload.
- Handle
message.created: Extract message details (sendermessage.from, contentmessage.content, typemessage.type). This is where you add your core logic to respond or act on the incoming message (e.g., save to DB, call other services, send a reply). - Handle
message.updated: Log status updates likedeliveredorreadfor messages you sent previously. Use themessage.idto update the status in your database. - Handle other event types if needed.
- Parse the raw body buffer into a JSON object:
- Acknowledge Receipt: Send a
200 OKresponse back to MessageBird promptly. MessageBird expects a quick acknowledgment. Any time-consuming processing (database writes, external API calls) should ideally be done asynchronously (e.g., push the event data to a queue) after sending the 200 OK, to avoid timeouts. - Handle Failures: If the signature is invalid, timestamp is too skewed (if checking), or JSON parsing fails, return an appropriate error status (
403 Forbidden,400 Bad Request).
Step 4: Test Receiving Messages
- Restart your server:
node server.js. - Ensure
ngrokis running: Check the terminal where you startedngrok. - Send a WhatsApp Message: From the phone number you used for testing the sending endpoint (or any phone), send a message to your MessageBird WhatsApp number associated with the configured channel.
- Check Server Logs: Observe the terminal where your
node server.jsis running. You should see logs indicating:- Webhook signature verification success.
- The event type (
message.created). - Details of the incoming message (sender, content).
- Check
ngrokLogs: Thengrokterminal (or its web interface, usually athttp://localhost:4040) also shows incoming requests and their status codes, which is useful for debugging connection issues or seeing if MessageBird is hitting your endpoint.
5. Implementing Proper Error Handling and Logging
While basic console.log and console.error are used, production applications need more robust logging and error handling.
Error Handling Strategy:
-
Specific API Errors: In the
sendWhatsAppMessagecallback/promise rejection, inspect theerr.errorsarray provided by the MessageBird SDK for detailed error codes and descriptions. Log these details. Decide whether specific errors are retryable.javascript// Example within sendWhatsAppMessage error handling if (err) { const errorDetails = err.errors?.[0] || {}; console.error('Error sending MessageBird message. Code:', errorDetails.code, 'Description:', errorDetails.description, 'Parameter:', errorDetails.parameter); // Example: Check for a specific rate limit error code if needed // if (errorDetails.code === SOME_RATE_LIMIT_CODE) { /* Handle differently */ } return reject(err); // Reject with the full error object } -
Webhook Errors: Log signature verification failures clearly, including the received signature if possible (be mindful of logging sensitive data). Handle JSON parsing errors gracefully.
-
Centralized Express Error Handler: Implement Express middleware to catch unhandled errors in your routes. This should be the last middleware added.
javascript// Add this *after* all your routes, but *before* app.listen app.use((err, req, res, next) => { console.error('Unhandled Error:', err.stack || err); // Avoid sending stack traces to the client in production res.status(500).json({ success: false, error: 'Internal Server Error' }); });
Logging:
-
Replace
console.log/error: Use a dedicated logging library likewinstonorpino. These offer structured logging (JSON format is common), different log levels (info, warn, error, debug), and transport options (console, file, external logging services).bashnpm install winstonjavascript// Example winston setup (place near the top of server.js) const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Log 'info' level and above (info, warn, error) format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), // Log stack traces for errors winston.format.json() // Log as JSON ), defaultMeta: { service: 'whatsapp-integration' }, // Add service context transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), // Optional: Colorize console output winston.format.simple() // Simple format for console ) }), // Add file transport for production if needed // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }) ], exceptionHandlers: [ // Optional: Catch unhandled exceptions new winston.transports.Console(), // new winston.transports.File({ filename: 'exceptions.log' }) ], rejectionHandlers: [ // Optional: Catch unhandled promise rejections new winston.transports.Console(), // new winston.transports.File({ filename: 'rejections.log' }) ] }); // Replace console.log with logger.info, logger.warn, logger.error // Example: logger.info(`Server listening on port ${port}`); // Use backticks for interpolation // Example: logger.error('Webhook signature verification failed!', { receivedSignature: signature }); // Add context // Example within catch block: logger.error('API Error sending WhatsApp message', { error: error, recipient: recipient });
Retry Mechanisms (Conceptual):
- For transient network errors or specific rate-limiting errors when sending messages via
sendWhatsAppMessage, implement a retry strategy. - Use exponential backoff: Wait progressively longer times between retries (e.g., 1s, 2s, 4s, 8s) to avoid overwhelming the API.
- Use libraries like
async-retryorp-retryto simplify this implementation. - Be careful not to retry indefinitely or for non-recoverable errors (like invalid recipient number, which will always fail). Check MessageBird error codes to determine retry eligibility.
6. Creating a Database Schema and Data Layer (Conceptual)
Storing message history, user consent, and conversation state is often necessary for real applications.
Schema Example (Conceptual - using PostgreSQL syntax):
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY, -- Auto-incrementing primary key (use BIGSERIAL for high volume)
messagebird_message_id VARCHAR(255) UNIQUE, -- MessageBird's unique message ID (index this)
messagebird_conversation_id VARCHAR(255), -- MessageBird's conversation ID (index this)
channel_id VARCHAR(255) NOT NULL, -- Your MessageBird Channel ID
contact_msisdn VARCHAR(30) NOT NULL, -- End user's phone number (E.164, index this)
messagebird_contact_id VARCHAR(255), -- MessageBird's Contact ID (if available)
direction VARCHAR(10) NOT NULL CHECK (direction IN ('incoming', 'outgoing')), -- 'incoming' or 'outgoing'
type VARCHAR(50) NOT NULL, -- 'text', 'hsm', 'image', 'location', etc.
content JSONB NOT NULL, -- Store structured content (text, media URLs, HSM details) as JSONB for flexibility and querying
status VARCHAR(50) DEFAULT 'pending' NOT NULL, -- 'pending', 'sent', 'delivered', 'read', 'failed' (index this)
error_code INT, -- Store MessageBird error code on failure
error_description TEXT, -- Store MessageBird error description on failure
received_at TIMESTAMPTZ DEFAULT NOW(), -- Timestamp when the webhook was received (for incoming) or message was created in our system (for outgoing)
status_updated_at TIMESTAMPTZ, -- Timestamp of the last status update from MessageBird
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes for common lookups
CREATE INDEX idx_messages_messagebird_message_id ON messages(messagebird_message_id);
CREATE INDEX idx_messages_messagebird_conversation_id ON messages(messagebird_conversation_id);
CREATE INDEX idx_messages_contact_msisdn ON messages(contact_msisdn);
CREATE INDEX idx_messages_status ON messages(status);
CREATE INDEX idx_messages_created_at ON messages(created_at);
-- Optional: Table for user consent/opt-in status
CREATE TABLE whatsapp_consent (
contact_msisdn VARCHAR(30) PRIMARY KEY, -- User phone number
has_opted_in BOOLEAN DEFAULT FALSE NOT NULL,
opt_in_timestamp TIMESTAMPTZ,
opt_out_timestamp TIMESTAMPTZ,
last_updated_at TIMESTAMPTZ DEFAULT NOW()
);Data Layer:
- Implement functions (e.g., using an ORM like Prisma, Sequelize, or TypeORM, or a query builder like Knex.js) to interact with this database schema.
saveIncomingMessage(messageData): Called from the webhook handler (message.created) to insert a new record into themessagestable withdirection = 'incoming'.saveOutgoingMessage(messageData): Called before or after callingsendWhatsAppMessageto insert a record withdirection = 'outgoing'and the initialstatus(e.g., 'pending' or 'sent'). Store themessagebird_message_idreturned by the API.updateMessageStatus(messagebirdMessageId, status, timestamp): Called from the webhook handler (message.updated) to update thestatusandstatus_updated_atfields for an existing message based on itsmessagebird_message_id.checkConsent(contactMsisdn): Check thewhatsapp_consenttable before sending proactive messages (especially templates).recordConsent(contactMsisdn, optedIn): Update the consent status.
Integrating this data layer involves calling these functions within your /send-whatsapp endpoint and /webhook handler. Remember to handle database errors appropriately.
7. Production Deployment Guide
Before deploying your WhatsApp integration to production, implement these essential requirements:
- Environment Variables: Use production-grade secret management (AWS Secrets Manager, Azure Key Vault, environment variables in your hosting platform).
- HTTPS Required: MessageBird webhooks require HTTPS endpoints. Use a valid SSL/TLS certificate (free options: Let's Encrypt).
- Permanent Webhook URL: Replace ngrok with your production domain (e.g.,
https://api.yourdomain.com/webhook). - Process Manager: Use PM2, systemd, or your platform's process manager to keep your Node.js app running.
- Load Balancing: For high-traffic applications, deploy behind a load balancer.
- Monitoring: Implement application monitoring (New Relic, Datadog) and error tracking (Sentry).
- Rate Limiting: Implement rate limiting on your public endpoints to prevent abuse.
- Security Headers: Add security headers using
helmetmiddleware. - Webhook Retry Handling: MessageBird will retry failed webhook deliveries. Ensure your webhook handler is idempotent (can safely process the same event multiple times).
Additional Resources
- MessageBird Conversations API Documentation
- Bird API Documentation (New Brand)
- MessageBird Node.js SDK on GitHub
- Meta WhatsApp Business Platform Documentation
- WhatsApp Message Template Guidelines
- Bird Webhook Verification Documentation
- E.164 Phone Number Format
Conclusion
You now have a complete Node.js/Express application that can send and receive WhatsApp messages via the MessageBird (now Bird) Conversations API. This foundation can be extended with additional features like:
- Template message support for marketing campaigns
- Rich media messages (images, videos, documents)
- Interactive messages (buttons, lists)
- Multi-agent conversation routing
- Analytics and reporting
- Integration with CRM systems
Remember to follow WhatsApp's Business Policy and Commerce Policy when building customer-facing WhatsApp applications.
Frequently Asked Questions
How to send WhatsApp message with Node.js?
You can send WhatsApp messages programmatically using Node.js, Express.js, and the MessageBird Conversations API. The provided Node.js code demonstrates how to create an API endpoint that sends WhatsApp messages via the MessageBird SDK. This enables automated notifications, customer support, and conversational commerce directly within your app.
What is MessageBird Conversations API?
The MessageBird Conversations API allows developers to integrate WhatsApp messaging into their applications. It provides a way to send and receive WhatsApp messages, manage conversations, and track message status. This API is used with the MessageBird Node.js SDK for seamless integration with a Node.js backend.
Why use ngrok for WhatsApp integration?
Ngrok creates a public tunnel to your local server, making it accessible from the internet. This is crucial for local development as it allows MessageBird to send webhooks to your application while testing. For production deployments, you'll need a stable, public HTTPS URL.
When to use a WhatsApp Message Template?
WhatsApp Message Templates (HSMs) are required to initiate conversations with users outside the 24-hour customer service window. You must submit and have these templates pre-approved by MessageBird through their dashboard before using them in your integration.
How to set up MessageBird WhatsApp webhook?
Configure your webhook URL (ngrok for development, public HTTPS for production) in your MessageBird Dashboard, under Channels > WhatsApp > Your Channel. Set the event triggers to "message.created" and "message.updated" and provide a strong signing key that is the same value as your MESSAGEBIRD_WEBHOOK_SIGNING_SECRET variable.
What is a MessageBird Channel ID?
The MessageBird Channel ID is a unique identifier for your WhatsApp Business channel. This ID is essential for sending WhatsApp messages and is used within the API requests and should be stored as the MESSAGEBIRD_WHATSAPP_CHANNEL_ID variable.
Can I receive WhatsApp messages with Node.js?
Yes, you can receive WhatsApp messages by setting up a webhook endpoint in your Node.js application. The MessageBird Conversations API will send an HTTP POST request to your specified URL every time a new message is received on your WhatsApp Business number.
How to handle MessageBird webhook security?
Implement webhook signature verification using the `messagebird-signature` header to ensure the request originates from MessageBird and is not a fraudulent request. The provided Node.js example demonstrates this crucial security practice.
What Node.js libraries are used in this article?
The article utilizes Express.js to create the web server and API endpoints, the MessageBird Node.js SDK for interactions with the MessageBird API, dotenv to load environment variables from a .env file, and ngrok for webhook testing during local development.
How to verify webhook signature MessageBird?
Verify the signature by calculating a HMAC-SHA256 hash of the combined timestamp, your signing secret, and the raw request body, then compare this hash with the signature provided in the request header. The webhook handling logic shown in the Node.js example provides a detailed implementation of this verification process.
How to integrate WhatsApp into existing Node.js app?
Integrate WhatsApp messaging by installing the necessary libraries (Express, MessageBird SDK, dotenv) and implementing the API endpoint and webhook handler logic as described in the article. You will need to configure your MessageBird account, obtain your API key and Channel ID, and set up a public URL for the webhook.
What environment variables are needed for MessageBird?
Set `MESSAGEBIRD_API_KEY`, `MESSAGEBIRD_WHATSAPP_CHANNEL_ID`, `MESSAGEBIRD_WEBHOOK_SIGNING_SECRET`, and `PORT` as environment variables. Store these in a `.env` file and load them with `dotenv`.
How to handle WhatsApp message status updates?
Use the "message.updated" webhook event, which MessageBird sends when a message status changes (e.g., delivered, read). Update your database with the new status by matching it to the unique message identifier.
Why is E.164 format important for WhatsApp numbers?
The E.164 format (+[country code][number]) ensures consistent and internationally recognized phone number representation for WhatsApp. This is crucial to deliver messages correctly.