This guide provides a step-by-step walkthrough for integrating WhatsApp messaging capabilities into your Node.js application using Express and the MessageBird Conversations API. We'll cover everything from initial project setup to sending/receiving messages, handling webhooks securely, managing errors, and preparing for deployment.
By the end of this guide, you will have a functional Node.js service capable of:
- Sending WhatsApp messages (including text and potentially templates) via the MessageBird API.
- Receiving incoming WhatsApp messages securely through MessageBird webhooks.
- Handling common errors and implementing basic logging.
- Understanding the key configuration steps within the MessageBird dashboard.
This integration solves the problem of programmatically interacting with customers on WhatsApp, enabling automated notifications, customer support interactions, or conversational commerce directly from your backend application.
Project Overview and Goals
Goal: To build a Node.js/Express backend service that can send and receive WhatsApp messages using the MessageBird Conversations API.
Technologies:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express.js: A minimal and flexible Node.js web application framework used to create the API endpoints and webhook handler.
- MessageBird: The communications platform providing the WhatsApp Business API access and SDK. We will use their Conversations API.
messagebird
Node.js SDK: Simplifies interaction with the MessageBird API.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.ngrok
(for local development): A tool to expose your local server to the internet, necessary for receiving MessageBird webhooks during development.
System Architecture:
A typical flow involves:
- Your application sends an HTTP POST request (e.g., to
/send-whatsapp
) to your Node.js/Express API. - The Node.js API uses the MessageBird SDK to call the MessageBird API.
- MessageBird delivers the WhatsApp message to the end user.
- If the user replies, WhatsApp sends the message to MessageBird.
- MessageBird sends a webhook POST request (e.g., to
/webhook
) to your publicly accessible URL (or ngrok tunnel). - Your Node.js API receives the webhook, verifies it, and processes the incoming message (e.g., updates application logic or database).
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. (Download Node.js)
- MessageBird Account: A registered account with MessageBird. (Sign up)
- Approved WhatsApp Business Channel: You must have completed the WhatsApp onboarding process via MessageBird and have an active WhatsApp channel configured. This provides your unique
Channel ID
. (MessageBird WhatsApp Quickstart) - MessageBird API Key: An API access key generated from your MessageBird dashboard.
- Publicly Accessible URL for Webhooks: MessageBird needs to send incoming messages to a stable, public URL. For local development, we'll use
ngrok
to create a temporary public tunnel to your machine. For production, you'll need a deployed application with a permanent public HTTPS URL. - Basic JavaScript/Node.js knowledge: Familiarity with Node.js, Express, and asynchronous programming (
async
/await
).
Final Outcome: A running Node.js server with two primary endpoints: one to trigger outgoing WhatsApp messages and another to receive incoming messages via webhooks.
1. Setting Up the Project
Let's 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 -y
This 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 dotenv
express
: 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 .gitignore
Your 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/
.env
Step 5: Set Up Environment Variables
Open the .env
file and add the following variables. You'll need to 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.
# You will also 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=3000
MESSAGEBIRD_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). You will later 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.env
file. 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
/health
endpoint is included as good practice for monitoring. app.listen
starts the server. We add a check to ensure the essential environment variables are loaded.
You can now run your basic server:
node server.js
You should see Server listening on port 3000
and confirmation that the environment variables loaded.
2. Implementing Core Functionality: Sending WhatsApp Messages
Now, let's create the logic to send an outgoing WhatsApp message via MessageBird.
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
sendWhatsAppMessage
function takes therecipient
number andtext
message. - It constructs the
params
object required by the MessageBirdconversations.start
method.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 (text
for 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.start
initiates the API call. It uses a callback pattern. We wrap it in aPromise
for easier use withasync/await
in our API endpoint later.- Basic error logging is included in the callback. More detailed error handling based on
err.errors
could be implemented.
Important Note on Templates (HSMs):
- 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).
- To send a template, you would change
type
to'hsm'
and modify thecontent
object accordingly. Example:content: { 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
text
type, 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-whatsapp
path.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
recipient
andmessage
are provided and that therecipient
looks like an E.164 number. You should implement more robust validation in a production environment (e.g., using libraries likeexpress-validator
). - The
sendWhatsAppMessage
function is called usingawait
. - If successful, a
200 OK
response is sent back with the MessageBird message ID and status. - If an error occurs during the API call (caught by
catch
), a500 Internal Server Error
is 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
curl
or Postman: Send a POST request to your running server. Replace+YOUR_TEST_WHATSAPP_NUMBER
with a real WhatsApp number you can check (in E.164 format).Using
curl
:curl -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):
{ ""success"": true, ""messageId"": ""mb_message_id_string"", ""status"": ""pending"" }
Expected Response (Error - e.g., Bad Number Format):
{ ""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 thesendWhatsAppMessage
function and the MessageBird API response.
4. Integrating Third-Party Service: Receiving Messages via Webhooks
To receive incoming WhatsApp messages sent to your MessageBird number, you need to configure and handle webhooks. MessageBird will send an HTTP POST request to a URL you specify whenever a new message arrives.
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):ngrok http 3000
-
Copy the HTTPS URL:
ngrok
will display forwarding URLs. Copy thehttps
URL (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.created
andmessage.updated
(for status updates). - Signing Key: This is critical for security. Paste the exact same secret you defined as
MESSAGEBIRD_WEBHOOK_SIGNING_SECRET
in your.env
file. - 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.
// 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()
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');
}
// 2. Prepare the Signed Payload String
// MessageBird constructs the string to sign by concatenating:
// The timestamp from the 'messagebird-request-timestamp' header
// Your webhook signing secret
// The raw request body
// separated by periods (.).
const signingSecret = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_SECRET;
const signedPayloadString = `${timestamp}.${signingSecret}.${requestBody.toString('utf8')}`;
// 3. Calculate Expected Signature
// Compute the HMAC-SHA256 hash of the signedPayloadString using your signing secret as the key.
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayloadString) // Hash the constructed string
.digest('hex');
// 4. Compare Signatures Securely
// Use crypto.timingSafeEqual to prevent timing attacks.
// Both arguments MUST be Buffers of the same length.
try {
const signatureBuffer = Buffer.from(signature, 'hex');
const expectedSignatureBuffer = Buffer.from(expectedSignature, 'hex');
if (signatureBuffer.length !== expectedSignatureBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedSignatureBuffer)) {
console.error('Webhook signature verification failed!');
return res.status(403).send('Invalid signature');
}
} catch (e) {
// Handle potential errors during buffer conversion (e.g., invalid hex string)
console.error('Error comparing signatures:', 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:
crypto
Module: Imported for cryptographic functions (HMAC-SHA256).express.raw({ type: 'application/json' })
: This middleware is applied only to the/webhook
route. 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: Get the
messagebird-signature
andmessagebird-request-timestamp
headers sent by MessageBird. - Timestamp Verification (Optional but Recommended): Check if the timestamp is recent (e.g., within 5 minutes) to prevent replay attacks, where an attacker resends an old, valid request. Rejecting old timestamps adds a layer of security. Adjust tolerance as needed based on expected clock skew.
- Prepare Signed Payload String: Construct the string exactly as MessageBird signs it:
timestamp + ""."" + signing_key + ""."" + raw_request_body_as_utf8_string
. - Calculate Expected Signature: Compute the HMAC-SHA256 hash of the
signedPayloadString
using yoursigningSecret
as the cryptographic key. Digest the result as a hexadecimal string. - Compare Signatures Securely: Use
crypto.timingSafeEqual
to compare the signature from the header with the one you calculated. This method runs in constant time, preventing attackers from inferring the correct signature based on response time differences. Crucially, both inputs must be Buffers of the same length. Handle potential errors if the provided signature is not valid hex. - 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 likedelivered
orread
for messages you sent previously. Use themessage.id
to 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 OK
response 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
ngrok
is 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.js
is running. You should see logs indicating:- Webhook signature verification success.
- The event type (
message.created
). - Details of the incoming message (sender, content).
- Check
ngrok
Logs: Thengrok
terminal (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
sendWhatsAppMessage
callback/promise rejection, inspect theerr.errors
array provided by the MessageBird SDK for detailed error codes and descriptions. Log these details. Decide whether specific errors are retryable.// 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.
// 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 likewinston
orpino
. These offer structured logging (JSON format is common), different log levels (info, warn, error, debug), and transport options (console, file, external logging services).npm install winston
// 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-retry
orp-retry
to 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 themessages
table withdirection = 'incoming'
.saveOutgoingMessage(messageData)
: Called before or after callingsendWhatsAppMessage
to insert a record withdirection = 'outgoing'
and the initialstatus
(e.g., 'pending' or 'sent'). Store themessagebird_message_id
returned by the API.updateMessageStatus(messagebirdMessageId, status, timestamp)
: Called from the webhook handler (message.updated
) to update thestatus
andstatus_updated_at
fields for an existing message based on itsmessagebird_message_id
.checkConsent(contactMsisdn)
: Check thewhatsapp_consent
table 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.