This guide provides a step-by-step walkthrough for building a solid foundation for a production Node.js application using the Fastify framework to send and receive WhatsApp messages via the MessageBird API. We will cover project setup, core messaging functionality, webhook handling, security best practices, error management, deployment considerations, and verification.
By the end of this tutorial, you will have a functional Fastify application capable of:
- Sending text messages through WhatsApp via a simple API endpoint.
- Securely receiving incoming WhatsApp messages via MessageBird webhooks.
- Basic logging and error handling for robust operation.
This guide assumes you have a basic understanding of Node.js, APIs, and terminal commands.
Project Overview and Goals
Goal: To create a reliable backend service using Fastify that acts as a bridge between your application logic and the WhatsApp messaging platform, facilitated by MessageBird.
Problem Solved: Directly integrating with WhatsApp's infrastructure can be complex. MessageBird provides a unified messaging platform and robust API that simplifies sending and receiving messages across various channels, including WhatsApp, handling the complexities of carrier integrations and WhatsApp Business API requirements.
Technologies Used:
- Node.js: A popular JavaScript runtime for building scalable server-side applications.
- Fastify: A high-performance, low-overhead web framework for Node.js, known for its speed, extensibility, and developer experience.
- MessageBird API: A communication Platform as a Service (CPaaS) offering APIs for SMS, Voice, WhatsApp, and more. We'll use their Conversations API for WhatsApp.
@messagebird/api
: The official Node.js SDK for interacting with the MessageBird API.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.@fastify/env
: A Fastify plugin for validating and loading environment variables.- (Optional)
ngrok
: A tool to expose your local development server to the internet for webhook testing.
System Architecture:
- Your application logic calls an endpoint on the Fastify app to send a message.
- The Fastify app uses the MessageBird SDK to send the message request to the MessageBird API.
- MessageBird processes the request and sends the message via the WhatsApp Business API.
- The message is delivered to the end user's WhatsApp client.
- The end user replies to the message.
- WhatsApp delivers the incoming message to MessageBird.
- MessageBird forwards the message payload to your pre-configured webhook URL, handled by the Fastify app.
- The Fastify app verifies the webhook signature, processes the message, and potentially triggers further actions in your application logic.
Prerequisites:
- Node.js (LTS version recommended, e.g., v18 or v20) and npm/yarn.
- A MessageBird account.
- A WhatsApp Business Account (WABA) approved and linked to your MessageBird account via a WhatsApp Channel. You can set this up in the MessageBird Dashboard.
- Access to a terminal or command prompt.
- (Optional but recommended for local development)
ngrok
installed.
1. Setting up the Project
Let's initialize our Node.js project using Fastify and install the necessary dependencies.
1. Create Project Directory:
mkdir fastify-messagebird-whatsapp
cd fastify-messagebird-whatsapp
2. Initialize npm Project:
npm init -y
This creates a package.json
file.
3. Install Dependencies:
We need Fastify, the MessageBird SDK, dotenv
for managing environment variables, and @fastify/env
for schema validation of those variables.
npm install fastify @messagebird/api dotenv @fastify/env
4. (Optional) Install Development Dependencies:
pino-pretty
makes Fastify's logs more readable during development.
npm install --save-dev pino-pretty
5. Configure package.json
Scripts:
Add scripts to your package.json
for easily running the application:
{
// ... other configurations
"main": "src/server.js", // Specify entry point
"scripts": {
"start": "node src/server.js",
"dev": "node src/server.js | pino-pretty" // Use pino-pretty for development
},
// ... other configurations
}
6. Create Project Structure:
A good structure helps maintainability.
fastify-messagebird-whatsapp/
├── node_modules/
├── src/
│ ├── app.js # Fastify application setup (plugins, routes)
│ ├── server.js # Server instantiation and startup logic
│ └── routes/
│ └── whatsapp.js # Routes related to WhatsApp actions
├── .env # Local environment variables (DO NOT COMMIT)
├── .env.example # Example environment variables (Commit this)
├── .gitignore # Files/folders to ignore in Git
└── package.json
Create the src
and src/routes
directories:
mkdir src
mkdir src/routes
7. Create .gitignore
:
Ensure sensitive files and irrelevant folders are not committed to version control.
# .gitignore
# Dependencies
node_modules/
# Environment variables
.env
*.env.local
*.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Build outputs
dist/
build/
# OS generated files
.DS_Store
Thumbs.db
8. Set up Environment Variables:
Create two files: .env.example
(to track required variables) and .env
(for your actual local values).
# .env.example
# Server Configuration
PORT=3000
HOST=0.0.0.0
# MessageBird API Credentials
# Get from MessageBird Dashboard -> Developers -> API access
MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY
# MessageBird WhatsApp Channel Configuration
# Get from MessageBird Dashboard -> Channels -> WhatsApp -> Select Channel -> Copy Channel ID
MESSAGEBIRD_WHATSAPP_CHANNEL_ID=YOUR_WHATSAPP_CHANNEL_ID
# MessageBird Webhook Configuration
# Get when setting up the webhook in MessageBird Dashboard (Channels -> WhatsApp -> Edit -> Webhooks)
MESSAGEBIRD_WEBHOOK_SIGNING_KEY=YOUR_WEBHOOK_SIGNING_KEY
# (Optional) Custom secret for additional webhook verification layer if needed
# WEBHOOK_SECRET=YOUR_CUSTOM_WEBHOOK_SECRET
Now, create the .env
file and populate it with your actual credentials from the MessageBird dashboard. Remember to replace the placeholder values.
# .env - Keep this file secure and out of Git!
PORT=3000
HOST=0.0.0.0
MESSAGEBIRD_API_KEY=live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
MESSAGEBIRD_WHATSAPP_CHANNEL_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MESSAGEBIRD_WEBHOOK_SIGNING_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# WEBHOOK_SECRET=a-very-strong-secret-if-used
Why this setup?
- Separation of Concerns: Code (
src
), configuration (.env
), and dependencies (node_modules
) are kept separate. - Environment Variables: Using
.env
keeps sensitive credentials out of your codebase, which is crucial for security.@fastify/env
ensures required variables are present and correctly formatted on startup. - Clear Entry Point:
src/server.js
handles the server lifecycle, whilesrc/app.js
configures the Fastify instance itself (plugins, routes, etc.). This promotes modularity.
2. Implementing Core Functionality
Now, let's build the core Fastify application and implement the logic for sending and receiving messages.
1. Basic Server Setup (src/server.js
):
This file initializes Fastify, loads the application logic from app.js
, and starts the server.
// src/server.js
'use strict';
// Read the .env file.
require('dotenv').config();
const buildApp = require('./app');
const start = async () => {
let app;
try {
// Build the Fastify app instance (registers plugins, routes)
app = await buildApp({
// Pass logger options suitable for production/development
logger: {
level: process.env.LOG_LEVEL || 'info',
// Use pino-pretty only if explicitly enabled (e.g., not in production)
transport: process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
},
});
// Start listening
await app.listen({
port: app.config.PORT, // Use validated port from @fastify/env
host: app.config.HOST, // Use validated host from @fastify/env
});
// Optional: Log registered routes on startup (useful for debugging)
// console.log(app.printRoutes());
} catch (err) {
if (app) {
app.log.error(err);
} else {
console.error('Error during server startup:', err);
}
process.exit(1);
}
};
start();
2. Fastify Application Setup (src/app.js
):
This file sets up the Fastify instance, registers essential plugins like @fastify/env
, and registers our WhatsApp routes.
// src/app.js
'use strict';
const Fastify = require('fastify');
const fastifyEnv = require('@fastify/env');
const whatsappRoutes = require('./routes/whatsapp');
// Define the schema for environment variables
// See: https://github.com/fastify/fastify-env
const envSchema = {
type: 'object',
required: [
'PORT',
'HOST',
'MESSAGEBIRD_API_KEY',
'MESSAGEBIRD_WHATSAPP_CHANNEL_ID',
'MESSAGEBIRD_WEBHOOK_SIGNING_KEY',
],
properties: {
PORT: { type: 'number', default: 3000 },
HOST: { type: 'string', default: '0.0.0.0' },
MESSAGEBIRD_API_KEY: { type: 'string' },
MESSAGEBIRD_WHATSAPP_CHANNEL_ID: { type: 'string' },
MESSAGEBIRD_WEBHOOK_SIGNING_KEY: { type: 'string' },
// WEBHOOK_SECRET: { type: 'string' } // Uncomment if using custom secret
},
};
async function buildApp(opts = {}) {
const app = Fastify(opts);
// Register @fastify/env to validate and load .env variables
await app.register(fastifyEnv, {
confKey: 'config', // Access variables via app.config
schema: envSchema,
dotenv: true, // Load .env file
});
// Initialize MessageBird SDK - makes it available across the app
// We attach it to the Fastify instance for easy access in routes
try {
const messagebird = require('@messagebird/api')(app.config.MESSAGEBIRD_API_KEY);
app.decorate('messagebird', messagebird);
app.log.info('MessageBird SDK initialized successfully.');
} catch (err) {
app.log.error('Failed to initialize MessageBird SDK:', err);
throw new Error('MessageBird SDK initialization failed.'); // Prevent startup if SDK fails
}
// Register routes
app.register(whatsappRoutes, { prefix: '/api/whatsapp' }); // Prefix routes with /api/whatsapp
// Basic root route
app.get('/', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
// Add a health check endpoint (useful for monitoring)
app.get('/health', async (request, reply) => {
// Add more sophisticated checks if needed (e.g., DB connection)
return { status: 'ok' };
});
return app;
}
module.exports = buildApp;
Why this structure?
- Async Initialization:
buildApp
isasync
because plugin registration (like@fastify/env
) can be asynchronous. - Environment Validation:
@fastify/env
ensures the application doesn't start without critical configuration, reducing runtime errors. Variables are accessed viaapp.config
. - SDK Decoration: Attaching the initialized MessageBird SDK to the Fastify instance (
app.decorate('messagebird', ...)
) makes it easily accessible within route handlers (request.server.messagebird
) without needing to re-initialize it everywhere. - Route Prefixing: Using
prefix: '/api/whatsapp'
keeps WhatsApp-related endpoints organized under a common path.
3. Building the API Layer (WhatsApp Routes)
Now, let's define the actual endpoints for sending messages and handling incoming webhooks.
Create src/routes/whatsapp.js
:
// src/routes/whatsapp.js
'use strict';
const crypto = require('crypto');
// Schema for the /send endpoint body validation
const sendMessageSchema = {
body: {
type: 'object',
required: ['to', 'text'],
properties: {
to: {
type: 'string',
description: 'Recipient WhatsApp number in E.164 format (e.g., +14155552671)',
// Example pattern - adjust if needed for stricter validation
pattern: '^\\+[1-9]\\d{1,14}$'
},
text: {
type: 'string',
minLength: 1,
description: 'The text message content',
},
},
},
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
messageId: { type: 'string' },
details: { type: 'object' } // Include raw response for debugging if needed
}
},
// Add schemas for error responses if desired
}
};
// --- Webhook Verification Logic ---
// IMPORTANT: This function MUST use the RAW request body, BEFORE JSON parsing.
function verifyMessageBirdWebhook(request, reply, rawBody) {
const server = request.server; // Access Fastify instance (app)
const signature = request.headers['messagebird-signature-key'];
const timestamp = request.headers['messagebird-request-timestamp'];
const signingKey = server.config.MESSAGEBIRD_WEBHOOK_SIGNING_KEY;
// 1. Check if headers are present
if (!signature || !timestamp) {
server.log.warn('Webhook received without required MessageBird headers.');
reply.code(400).send({ error: 'Missing MessageBird signature headers' });
return false; // Indicate verification failed
}
// 2. Construct the string to sign
// Format: timestamp + '|' + rawBody
const signedPayload = `${timestamp}|${rawBody.toString('utf-8')}`; // Ensure body is string
// 3. Calculate the expected signature
const expectedSignature = crypto
.createHmac('sha256', signingKey)
.update(signedPayload)
.digest('hex');
// 4. Compare signatures using a timing-safe method
try {
// Ensure buffers have the same length for timingSafeEqual
const signatureBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (signatureBuffer.length !== expectedBuffer.length) {
server.log.warn('Webhook signature length mismatch.');
reply.code(401).send({ error: 'Invalid signature' });
return false;
}
const isValid = crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
if (!isValid) {
server.log.warn('Webhook signature validation failed.');
reply.code(401).send({ error: 'Invalid signature' });
return false;
}
} catch (error) {
server.log.error({ err: error }, 'Error during signature comparison.');
reply.code(500).send({ error: 'Internal server error during verification' });
return false;
}
server.log.info('Webhook signature verified successfully.');
return true; // Indicate verification succeeded
}
// --- End Webhook Verification Logic ---
async function whatsappRoutes(fastify, options) {
const { messagebird, config, log } = fastify; // Access decorated SDK, config, logger
// === Endpoint to SEND a WhatsApp message ===
fastify.post('/send', { schema: sendMessageSchema }, async (request, reply) => {
const { to, text } = request.body;
const channelId = config.MESSAGEBIRD_WHATSAPP_CHANNEL_ID;
const params = {
to: to,
from: channelId, // Use Channel ID as the 'from'
type: 'text',
content: {
text: text,
},
// Optional: Track message status via webhooks
// reportUrl: 'YOUR_STATUS_WEBHOOK_URL' // Define another webhook for status updates
};
log.info(`Attempting to send WhatsApp message to ${to} via channel ${channelId}`);
try {
// Use the MessageBird SDK's conversation API
// Ref: https://developers.messagebird.com/api/conversations/#send-message
const result = await messagebird.conversations.send(params);
log.info({ msgId: result.id, status: result.status }, `Message sent successfully to ${to}`);
return reply.code(200).send({
status: 'success',
messageId: result.id, // Return MessageBird message ID
details: result, // Optionally return full response
});
} catch (error) {
log.error({ err: error, recipient: to }, 'Failed to send WhatsApp message via MessageBird');
// Extract more specific error details if available
const errorDetails = error.response?.body?.errors || [{ description: error.message }];
// Determine appropriate status code (e.g., 400 for bad input, 500/503 for provider issues)
const statusCode = error.statusCode && error.statusCode >= 400 && error.statusCode < 500 ? error.statusCode : 500;
return reply.code(statusCode).send({
status: 'error'_
message: 'Failed to send message'_
errors: errorDetails_
});
}
});
// === Endpoint to RECEIVE WhatsApp messages (Webhook) ===
// IMPORTANT: We need the raw body for signature verification.
// We configure Fastify to NOT parse JSON for this specific route initially.
fastify.addContentTypeParser('application/json'_ { parseAs: 'buffer' }_ (req_ body_ done) => {
// We'll parse JSON manually *after* verification if needed
done(null, body);
});
fastify.post('/webhook', {
// No body schema here initially, as we need the raw buffer first
config: {
// Optional: Add specific config for this route if needed
}
}, async (request, reply) => {
const rawBody = request.body; // This is a Buffer due to the content type parser
// 1. Verify the signature FIRST
const isVerified = verifyMessageBirdWebhook(request, reply, rawBody);
if (!isVerified) {
// verifyMessageBirdWebhook already sent the reply on failure
return;
}
// 2. Signature is valid, now parse the JSON payload
let payload;
try {
payload = JSON.parse(rawBody.toString('utf-8'));
} catch (error) {
log.error({ err: error }, 'Failed to parse webhook JSON payload after verification.');
return reply.code(400).send({ error: 'Invalid JSON payload' });
}
// 3. Process the valid webhook payload
log.info({ webhookPayload: payload }, 'Received and verified MessageBird webhook');
// Example: Log message details
// Structure depends on the MessageBird webhook event type (e.g., message.created, message.updated)
// Ref: https://developers.messagebird.com/api/conversations/#receiving-messages-and-status-updates
if (payload.type === 'message.created' && payload.message) {
const message = payload.message;
log.info(`Received message type: ${message.type} from: ${message.from} content: ${JSON.stringify(message.content)}`);
// --- Add your business logic here ---
// - Store the message in a database
// - Trigger a response based on content
// - Forward to another service
// Example: Echo back the text message (for testing)
// if (message.type === 'text' && message.direction === 'received') {
// try {
// await messagebird.conversations.reply(payload.conversation.id, {
// type: 'text',
// content: { text: `You said: ${message.content.text}` }
// });
// log.info(`Sent echo reply to ${message.from}`);
// } catch (replyError) {
// log.error({ err: replyError }, 'Failed to send echo reply');
// }
// }
// ------------------------------------
} else {
log.info(`Received webhook event type: ${payload.type}`);
// Handle other event types if necessary (e.g., message status updates)
}
// 4. Acknowledge the webhook receipt quickly
// MessageBird expects a 200 OK response.
return reply.code(200).send({ status: 'received' });
});
// Remove the custom parser after registering webhook route so it doesn't affect others
fastify.restoreContentTypeParser('application/json');
}
module.exports = whatsappRoutes;
Explanation:
- Send Endpoint (
/api/whatsapp/send
):- Uses Fastify's schema validation (
sendMessageSchema
) to ensureto
(WhatsApp number) andtext
are provided in the correct format. - Constructs the parameters required by the MessageBird Conversations API (
messagebird.conversations.send
). - Calls the SDK method within a
try...catch
block for error handling. - Returns the MessageBird message ID upon success or a detailed error message on failure.
- Uses Fastify's schema validation (
- Webhook Endpoint (
/api/whatsapp/webhook
):- Raw Body Handling:
fastify.addContentTypeParser
is used to capture the raw request body as a Buffer before Fastify automatically parses it as JSON. This is essential for signature verification. - Signature Verification: Calls
verifyMessageBirdWebhook
immediately upon receiving a request. This function performs the critical security check using theMessageBird-Signature-Key
,MessageBird-Request-Timestamp
, and the raw body. It usescrypto.timingSafeEqual
to prevent timing attacks. If verification fails, it sends an error response and stops processing. - JSON Parsing: Only after successful verification is the raw buffer parsed into a JSON object (
JSON.parse
). - Payload Processing: Logs the received payload. Includes a commented-out section demonstrating where you'd add your business logic (database interaction, triggering automated replies, etc.). Crucially, keep webhook processing fast. Offload long-running tasks to background job queues if necessary.
- Acknowledgement: Sends a
200 OK
response promptly to acknowledge receipt to MessageBird. Failure to respond quickly can cause MessageBird to retry the webhook, leading to duplicate processing. - Restore Parser:
fastify.restoreContentTypeParser
ensures the custom raw body parser only applies to the webhook route.
- Raw Body Handling:
4. Integrating with MessageBird (Configuration Steps)
You've already set up the environment variables. Here's a reminder of where to get them and how to configure the webhook in MessageBird:
1. Obtain API Key (MESSAGEBIRD_API_KEY
):
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left-hand menu.
- Click on API access.
- If you don't have a live API key, create one.
- Copy the Live API Key and paste it into your
.env
file. Keep this key secure!
2. Obtain WhatsApp Channel ID (MESSAGEBIRD_WHATSAPP_CHANNEL_ID
):
- In the MessageBird Dashboard, navigate to Channels.
- Click on WhatsApp.
- Find the approved WhatsApp channel you want to use.
- Click on the channel name or the edit icon.
- On the channel details page, find the Channel ID (usually a UUID like
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
). - Copy this ID and paste it into your
.env
file.
3. Set up Webhook and Obtain Signing Key (MESSAGEBIRD_WEBHOOK_SIGNING_KEY
):
- Get Public URL: While developing locally, you need a public URL for MessageBird to reach your server. Use
ngrok
:# Expose port 3000 (or whatever your app uses) ngrok http 3000
ngrok
will provide a public HTTPS URL (e.g.,https://abcd-1234.ngrok.io
). Use this temporary URL for testing. For production, use your server's actual public domain/IP. - Configure in MessageBird:
- Go back to your WhatsApp Channel settings in the MessageBird Dashboard (Channels -> WhatsApp -> Edit your channel).
- Scroll down to the Webhooks section.
- Click Add webhook.
- Webhook URL: Enter your public URL followed by the webhook path:
https://<your-ngrok-or-production-url>/api/whatsapp/webhook
- Events: Select the events you want to receive. For incoming messages, ensure
message.created
is checked. You might also wantmessage.updated
for status changes. - Click Add webhook.
- Copy Signing Key: After adding the webhook, MessageBird will display a Signing Key. This is crucial for verification. Copy this key immediately (it might not be shown again) and paste it into your
.env
file asMESSAGEBIRD_WEBHOOK_SIGNING_KEY
.
Environment Variable Recap:
MESSAGEBIRD_API_KEY
: Authenticates your requests to MessageBird.MESSAGEBIRD_WHATSAPP_CHANNEL_ID
: Identifies which WhatsApp number (channel) you are sending from.MESSAGEBIRD_WEBHOOK_SIGNING_KEY
: Used by your application to verify that incoming webhook requests are actually from MessageBird.
5. Implementing Error Handling, Logging, and Retries
Robust applications need solid error handling and logging.
Error Handling Strategy:
- Specific Errors: Catch errors from the MessageBird SDK (
try...catch
aroundmessagebird.*
calls) and provide specific feedback (e.g., invalid recipient number, insufficient balance). Log detailed error information server-side. - Validation Errors: Fastify's schema validation handles invalid request payloads for the
/send
endpoint automatically, returning 400 errors. - Webhook Verification Errors: The
verifyMessageBirdWebhook
function handles signature failures, returning 400 or 401 errors. - General Errors: Use Fastify's default error handler or implement a custom one (
app.setErrorHandler
) for unexpected server errors (return 500). - Logging: Use Fastify's built-in Pino logger (
fastify.log
orrequest.log
). Log key events (startup, request received, message sent/failed, webhook received/verified/failed, errors). Include relevant context (like message IDs, recipient numbers) in logs.
Logging:
- Levels: Use standard levels (
info
,warn
,error
,debug
). SetLOG_LEVEL
in your environment (e.g.,info
for production,debug
for development). - Format: Pino logs in JSON format by default, which is great for log aggregation tools (Datadog, Splunk, ELK). Use
pino-pretty
only for local development readability. - Context: Include request IDs (Fastify adds these automatically) and relevant business data (message IDs, channel IDs, recipient/sender identifiers) in log messages.
Retry Mechanisms (for Outgoing Messages):
Network issues or temporary MessageBird outages can cause sending failures. Implementing retries can improve reliability.
- Strategy: Use exponential backoff (wait longer between retries). Limit the number of retries.
- Implementation: You could wrap the
messagebird.conversations.send
call with a retry library likeasync-retry
or implement a simple loop.
// Example using async-retry (install: npm install async-retry)
// Inside the /send route handler:
const retry = require('async-retry');
// ... inside the try block of /send handler
try {
const result = await retry(
async (bail, attemptNumber) => {
log.info(`Attempt ${attemptNumber} to send message to ${to}`);
const mbResult = await messagebird.conversations.send(params);
log.info({ msgId: mbResult.id, status: mbResult.status }, `Message sent successfully on attempt ${attemptNumber}`);
return mbResult; // Return result on success
},
{
retries: 3, // Number of retries (total attempts = retries + 1)
factor: 2, // Exponential backoff factor
minTimeout: 1000, // Initial delay ms
maxTimeout: 5000, // Max delay ms
onRetry: (error, attemptNumber) => {
log.warn({ err: error, attempt: attemptNumber }, `Retry attempt ${attemptNumber} failed for recipient ${to}. Retrying...`);
// Use bail() to stop retrying for non-recoverable errors (e.g., 4xx client errors)
if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
log.error(`Non-recoverable error (status ${error.statusCode}) sending to ${to}. Aborting retries.`);
bail(error); // Stop retrying
}
}
}
);
// The rest of the success handling...
log.info({ msgId: result.id_ status: result.status }_ `Message sent successfully to ${to}`);
return reply.code(200).send({
status: 'success'_
messageId: result.id_
details: result_
});
} catch (error) {
// This catch block now catches errors after all retries fail or if bail() is called.
log.error({ err: error_ recipient: to }_ 'Failed to send WhatsApp message via MessageBird after retries');
const errorDetails = error.response?.body?.errors || [{ description: error.message }];
const statusCode = error.statusCode && error.statusCode >= 400 && error.statusCode < 500 ? error.statusCode : 500;
return reply.code(statusCode).send({
status: 'error',
message: 'Failed to send message',
errors: errorDetails,
});
}
// ... rest of the route handler logic if any ...
Note: Implement retries carefully. Avoid retrying non-recoverable errors (like invalid authentication or recipient number). Only retry temporary issues (network errors, 5xx server errors from MessageBird).
6. Creating a Database Schema and Data Layer (Conceptual)
While this guide focuses on the core integration, a production application typically needs to store message history, conversation state, or user data.
Why a Database?
- Message History: Keep track of sent and received messages for auditing, analysis, or displaying conversation history to users.
- Conversation State: Manage multi-turn conversations or chatbot interactions.
- User Linking: Associate WhatsApp numbers with user accounts in your system.
- Rate Limiting/Tracking: Store usage data per user/number.
Conceptual Schema (Example using PostgreSQL syntax):
CREATE TABLE conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
messagebird_conversation_id VARCHAR(255) UNIQUE, -- MessageBird's ID
whatsapp_contact_id VARCHAR(255) NOT NULL, -- End user's WhatsApp ID (e.g., +1415...)
last_message_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
messagebird_message_id VARCHAR(255) UNIQUE, -- MessageBird's ID for this message
direction VARCHAR(10) NOT NULL CHECK (direction IN ('sent', 'received')), -- 'sent' or 'received' from app's perspective
sender_id VARCHAR(255) NOT NULL, -- Could be Channel ID or User WhatsApp ID
recipient_id VARCHAR(255) NOT NULL, -- Could be User WhatsApp ID or Channel ID
message_type VARCHAR(50) NOT NULL, -- 'text', 'image', 'audio', 'template', etc.
content JSONB, -- Store message content (text, media URLs, etc.)
status VARCHAR(50), -- 'pending', 'sent', 'delivered', 'read', 'failed' (from status webhooks)
messagebird_created_at TIMESTAMPTZ, -- Timestamp provided by MessageBird in the payload (message creation time)
status_updated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW() -- Timestamp when the record was created in *our* database
);
-- Indexes for common lookups
CREATE INDEX idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX idx_messages_messagebird_message_id ON messages(messagebird_message_id);
CREATE INDEX idx_conversations_whatsapp_contact_id ON conversations(whatsapp_contact_id);
Implementation:
- Choose a database (PostgreSQL, MongoDB, etc.).
- Select an ORM (Object-Relational Mapper) like Prisma, Sequelize, or use a database driver directly (e.g.,
pg
). - Integrate the data access logic into your webhook handler (to save incoming messages) and potentially the send endpoint (to log outgoing messages).
- Use database migrations (like Prisma Migrate or Sequelize CLI) to manage schema changes safely.
7. Adding Security Features
Security is paramount, especially when handling user communication and API keys.
- Webhook Signature Verification: Already implemented and mandatory. This prevents attackers from sending fake webhook events to your endpoint. Ensure
MESSAGEBIRD_WEBHOOK_SIGNING_KEY
is kept secret. - Environment Variables: Keep all secrets (
MESSAGEBIRD_API_KEY
,MESSAGEBIRD_WEBHOOK_SIGNING_KEY
, database credentials) in environment variables, never hardcoded. Use.gitignore
correctly. - Input Validation: Implemented for
/send
endpoint using Fastify schemas. This prevents malformed requests from causing errors or potential injection issues (though less critical for simple text sending). Sanitize any input that might be reflected back or used in database queries if not using an ORM that handles it. - HTTPS: Always run your application behind HTTPS in production (ngrok provides this for local testing). This encrypts data in transit. Use a reverse proxy like Nginx or Caddy, or ensure your hosting platform provides TLS termination.
- Rate Limiting: Protect your
/send
endpoint from abuse. Use a plugin like@fastify/rate-limit
. Consider applying rate limits based on source IP or, if authentication is added, based on user/API key.