Learn how to build a robust webhook endpoint using Fastify to receive and process real-time message delivery status updates from MessageBird. This guide provides a complete walkthrough, from project setup to deployment and verification, ensuring you can reliably track the status of your sent messages.
This guide focuses specifically on handling incoming Delivery Status Reports (DLRs) via webhooks from MessageBird. We'll build a dedicated Fastify endpoint that securely receives these updates, logs them, and acknowledges receipt to MessageBird, forming a crucial part of any application that relies on message delivery confirmation.
Project Overview and Goals
What We'll Build:
A Node.js application using the Fastify framework that exposes a secure webhook endpoint. This endpoint will:
- Receive
POST
requests from MessageBird containing message delivery status updates. - Securely verify the authenticity of incoming requests using MessageBird's webhook signature.
- Parse the status update payload.
- Log the relevant status information.
- Respond to MessageBird with a
200 OK
status code to acknowledge receipt.
Problem Solved:
When sending SMS or messages via other channels through an API like MessageBird, you often need confirmation that the message was actually delivered, failed, or experienced some other status change. Relying solely on the initial API response isn't enough. MessageBird provides webhooks to push these status updates to your application in real-time, enabling you to update your internal systems, notify users, or trigger further actions based on delivery success or failure.
Technologies Used:
- Node.js: The runtime environment for our JavaScript application.
- Fastify: A high-performance, low-overhead web framework for Node.js, chosen for its speed, extensibility, and developer-friendly features like built-in logging and schema validation.
- MessageBird: The communications platform providing the messaging API and sending the delivery status webhooks. (We'll interact with their webhook system, not necessarily the SDK for sending in this specific guide, but secure handling of their API/Signing Key is essential).
- dotenv: A utility to load environment variables from a
.env
file intoprocess.env
, keeping sensitive keys out of source code. - Node.js
crypto
module: Used for verifying webhook signatures.
System Architecture:
graph LR
A[Your Application] -- 1. Send Message API Request --> B(MessageBird API);
B -- 2. Sends Message --> C(End User Device);
C -- 3. Sends Delivery Status --> B;
B -- 4. POST Delivery Status Webhook --> D(Your Fastify Webhook Endpoint);
D -- 5. Verify Signature & Process --> E{Logs / Database / Queue};
D -- 6. Send 200 OK Response --> B;
style D fill:#f9f,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js and npm (or yarn) installed.
- A MessageBird account with access to API credentials.
- A way to expose your local development server to the internet (e.g., ngrok) for testing webhooks.
- Basic understanding of JavaScript, Node.js, APIs, and webhooks.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir fastify-messagebird-webhook cd fastify-messagebird-webhook
-
Initialize Node.js Project: This creates a
package.json
file.npm init -y
-
Install Dependencies: We need Fastify for the web server and
dotenv
for environment variables.npm install fastify dotenv
-
Set Up Project Structure: Create a basic structure for clarity.
mkdir src touch src/server.js touch .env touch .gitignore
-
Configure
.gitignore
: Prevent sensitive files and unnecessary directories from being committed to version control. Add the following to your.gitignore
file:# Environment variables .env* !.env.example # Node dependencies node_modules/ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Build output dist build # OS generated files .DS_Store Thumbs.db
-
Configure
.env
: Add placeholders for your MessageBird credentials and server configuration. We'll obtain theMESSAGEBIRD_WEBHOOK_SIGNING_KEY
later from the MessageBird dashboard.# .env # Server Configuration PORT=3000 HOST=0.0.0.0 # MessageBird Configuration # Get this from the MessageBird Dashboard -> Developers -> API Settings -> Webhooks MESSAGEBIRD_WEBHOOK_SIGNING_KEY=YOUR_MESSAGEBIRD_WEBHOOK_SIGNING_KEY # Replace with your actual key from MessageBird Dashboard
PORT
: The port your Fastify server will listen on.HOST
: The network interface to bind to (0.0.0.0
makes it accessible from outside Docker containers or VMs if needed, defaults to127.0.0.1
otherwise).MESSAGEBIRD_WEBHOOK_SIGNING_KEY
: The secret key provided by MessageBird specifically for verifying webhook signatures. This is NOT your API Key. You must replace the placeholder value with the actual key obtained from MessageBird.
-
Initial Fastify Server Setup (
src/server.js
): Create a basic Fastify server instance that loads environment variables.// src/server.js // Load environment variables from .env file require('dotenv').config(); // Require the framework and instantiate it const fastify = require('fastify')({ logger: true // Enable built-in logger }); // Basic route to check if the server is running fastify.get('/', async (request, reply) => { return { hello: 'world' }; }); // Run the server const start = async () => { try { const port = process.env.PORT || 3000; const host = process.env.HOST || '0.0.0.0'; await fastify.listen({ port: parseInt(port, 10), host: host }); fastify.log.info(`Server listening on ${fastify.server.address().port}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start();
-
Add Start Script to
package.json
: Modify yourpackage.json
to include a convenient start script. Note thatnpm install
will add the specific dependency versions, so replace^x.x.x
with the actual installed versions if desired.{ ""name"": ""fastify-messagebird-webhook"", ""version"": ""1.0.0"", ""description"": ""Fastify webhook handler for MessageBird delivery status"", ""main"": ""src/server.js"", ""scripts"": { ""start"": ""node src/server.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [ ""fastify"", ""messagebird"", ""webhook"" ], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""dotenv"": ""^16.x.x"", ""fastify"": ""^4.x.x"" } }
-
Run the Server:
npm start
You should see output indicating the server is running, likely on port 3000. You can visit
http://localhost:3000
in your browser to see the{ ""hello"": ""world"" }
response. Stop the server withCtrl+C
.
2. Implementing the Core Webhook Handler
Now, let's create the endpoint that will receive MessageBird's status updates.
-
Define the Webhook Route (
src/server.js
): MessageBird sends status updates viaPOST
request. We'll define a route,/webhooks/messagebird
, to handle these. Add the following code insidesrc/server.js
, before thestart
function definition.// src/server.js (add inside the file, before the start function) fastify.post('/webhooks/messagebird', async (request, reply) => { fastify.log.info('Received MessageBird webhook'); fastify.log.info({ headers: request.headers }, 'Incoming Headers'); fastify.log.info({ body: request.body }, 'Incoming Body'); // Signature verification and payload processing will be added in later steps. // Acknowledge receipt to MessageBird immediately. // It's crucial to send a 200 OK quickly. // MessageBird may retry if it doesn't receive a timely 200 response. reply.code(200).send({ status: 'received' }); }); // ... rest of the server code (start function, etc.)
- We log the incoming request headers and body for debugging.
- We immediately send a
200 OK
response. Important: Long-running processes should be handled asynchronously (e.g., pushed to a queue) after sending the 200 OK, to avoid MessageBird timeouts and retries.
-
Understanding the Payload: MessageBird's delivery status webhook payload typically looks something like this (structure may vary slightly based on event type):
{ ""id"": ""message_id_string"", ""href"": ""https://rest.messagebird.com/messages/message_id_string"", ""recipient"": 31612345678, ""originator"": ""YourSenderID"", ""body"": ""Your message text"", ""reference"": ""YourOptionalReference"", ""status"": ""delivered"", ""statusDatetime"": ""2025-04-20T10:30:00+00:00"", ""type"": ""sms"" }
Possible statuses include
sent
,buffered
,delivered
,failed
,expired
, etc. We are primarily interested inid
,status
,statusDatetime
, and potentiallyrecipient
orreference
to correlate the update with our original message. -
Processing the Payload: Let's add basic processing logic to log the key information. Update the
/webhooks/messagebird
route handler insrc/server.js
:// src/server.js - Update the '/webhooks/messagebird' route handler fastify.post('/webhooks/messagebird', async (request, reply) => { fastify.log.info('Received MessageBird webhook'); // We'll implement signature verification before processing in a later step. // Process Payload (Basic Logging Example) try { const payload = request.body; if (payload && payload.id && payload.status) { const messageId = payload.id; const status = payload.status; const statusTimestamp = payload.statusDatetime; const recipient = payload.recipient; // --- Placeholder for Real Application Logic --- // In a production application, you would typically: // 1. Look up the message in your database using `messageId`. // 2. Update its status to the received `status`. // 3. Store the `statusTimestamp`. // 4. Potentially trigger notifications or other workflows based on the status (e.g., alert on failures). // 5. Consider pushing complex processing to a background queue. // For this guide, we'll just log the information. // --- End Placeholder --- fastify.log.info(`Message ID [${messageId}] for recipient [${recipient}] updated to status [${status}] at [${statusTimestamp}]`); } else { fastify.log.warn('Received webhook with missing id or status'); } } catch (error) { fastify.log.error(error, 'Error processing webhook payload'); // Even if processing fails, we should still try to send 200 OK // unless the failure is critical (like signature mismatch, handled later). // For now, we let the default 200 OK proceed. } // Acknowledge receipt to MessageBird reply.code(200).send({ status: 'received' }); });
This code now attempts to parse the
id
,status
,statusDatetime
, andrecipient
from the request body and logs them.
3. Building a Complete API Layer (Optional Send Endpoint)
While the core goal is handling incoming webhooks, adding an endpoint to send a message via MessageBird helps in testing the full loop. This requires the official MessageBird Node.js SDK.
-
Install MessageBird SDK:
npm install messagebird
-
Add API Key to
.env
: You'll need your Live API Key from the MessageBird Dashboard (Developers -> API access (REST)). Add it to your.env
file.# .env (add this line) MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY # Replace with your actual LIVE API key from MessageBird Dashboard
Remember to replace the placeholder with your actual key.
-
Implement Send Endpoint (
src/server.js
): Add a new route to trigger sending an SMS. Add this code insidesrc/server.js
, afterrequire('dotenv').config();
and before the webhook route.// src/server.js (add near the top, after require('dotenv').config()) const messagebird = require('messagebird')(process.env.MESSAGEBIRD_API_KEY); // ... (inside the server setup, before the webhook route and start function) // Endpoint to send a test SMS fastify.post('/send-test-sms', async (request, reply) => { const { recipient, message } = request.body; if (!recipient || !message) { reply.code(400).send({ error: 'Missing recipient or message in request body' }); return; } // Basic validation (adjust as needed) if (!/^\+?[1-9]\d{1,14}$/.test(recipient)) { reply.code(400).send({ error: 'Invalid recipient phone number format (E.164 expected)' }); return; } const params = { originator: 'TestApp', // Change to your approved originator or number recipients: [ recipient ], body: message // reportUrl: 'YOUR_PUBLIC_WEBHOOK_URL/webhooks/messagebird' // Optional: Set webhook URL per message }; fastify.log.info({ params }, 'Attempting to send SMS'); try { const result = await new Promise((resolve, reject) => { messagebird.messages.create(params, (err, response) => { if (err) { fastify.log.error(err, 'MessageBird API error'); return reject(err); } fastify.log.info({ response }, 'MessageBird API success response'); resolve(response); }); }); reply.code(201).send({ success: true, messageId: result.id, status: result.recipients.items[0].status }); } catch (error) { // Determine appropriate status code based on MessageBird error if possible const statusCode = error.statusCode || 500; const errorMessage = error.errors ? error.errors[0].description : 'Failed to send SMS via MessageBird'; reply.code(statusCode).send({ success: false, error: errorMessage }); } });
- Requires the
messagebird
SDK. - Reads
recipient
andmessage
from the POST body. - Includes basic validation for the phone number (E.164 format expected).
- Uses
messagebird.messages.create
to send the SMS. - Handles success and error responses from the MessageBird API using Promises.
- Important: Set
originator
to a valid sender ID or Virtual Mobile Number registered in your MessageBird account. Alphanumeric IDs have restrictions. - The
reportUrl
parameter inmessages.create
can override the default webhook URL set in the dashboard for specific messages, but we'll rely on the dashboard setting primarily.
- Requires the
-
Testing the Send Endpoint: Once your server is running (restart it after adding the new code and API key), you can use
curl
or a tool like Postman:curl -X POST http://localhost:3000/send-test-sms \ -H ""Content-Type: application/json"" \ -d '{ ""recipient"": ""+1XXXXXXXXXX"", ""message"": ""Hello from Fastify!"" }' # Replace +1XXXXXXXXXX with a valid E.164 test phone number
4. Integrating with MessageBird (Webhook Configuration)
This is the crucial step to connect MessageBird's status updates to your running application.
-
Expose Your Local Server: MessageBird needs a publicly accessible URL to send webhooks to. During development, tools like
ngrok
are perfect.- Install ngrok (if you haven't already): Visit https://ngrok.com/download
- Run it to expose your local port (e.g., 3000):
ngrok http 3000
ngrok
will provide a public URL (e.g.,https://<random_string>.ngrok.io
). Copy this HTTPS URL.
-
Configure Webhook in MessageBird Dashboard:
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left-hand menu.
- Click on API Settings.
- Go to the Webhooks tab.
- Click Add webhook.
- Event: Select
message.updated
. This event triggers for status changes like sent, delivered, failed, etc. - URL: Paste your public HTTPS URL from ngrok, followed by your webhook route:
https://<random_string>.ngrok.io/webhooks/messagebird
- Method: Ensure
POST
is selected. - Signing Key: MessageBird will generate a Webhook Signing Key. Copy this value immediately and securely.
- Click Add webhook.
-
Update
.env
with Signing Key: Paste the Webhook Signing Key (NOT your API key) you copied from the MessageBird dashboard into your.env
file:# .env # ... other variables MESSAGEBIRD_WEBHOOK_SIGNING_KEY=paste_your_webhook_signing_key_here # Replace with the key generated in the MessageBird Dashboard
Restart your Fastify server for the new environment variable to be loaded (
Ctrl+C
thennpm start
).
5. Implementing Error Handling and Logging
We already enabled Fastify's basic logger. Let's enhance error handling, especially around webhook processing.
- Consistent Error Handling: The current
try...catch
in the webhook handler is a good start. Ensure critical errors (like signature failure, added later) prevent further processing and potentially return a non-200 status (though MessageBird generally prefers 200 OK for acknowledgment, logging the failure is key). - Logging Levels: Fastify's logger supports levels (
info
,warn
,error
,debug
, etc.). Use them appropriately:info
: Standard operations (webhook received, status processed).warn
: Non-critical issues (payload missing optional fields, unexpected status).error
: Critical failures (signature mismatch, database errors, unhandled exceptions).
- Retry Mechanisms (Conceptual):
- MessageBird Retries: MessageBird automatically retries sending webhooks if it doesn't receive a
2xx
response within a certain timeout (usually several seconds). Your primary goal is to respond quickly with200 OK
. - Internal Retries: If processing the webhook after sending the 200 OK fails (e.g., database connection issue), implement your own retry logic. Use libraries like
async-retry
or push the job to a queue (like BullMQ, RabbitMQ) that handles retries with exponential backoff. This is beyond the scope of the basic handler but essential for production robustness.
- MessageBird Retries: MessageBird automatically retries sending webhooks if it doesn't receive a
6. Creating a Database Schema and Data Layer (Conceptual)
Storing message statuses is vital. Here's a conceptual schema and how the webhook interacts with it.
-
Conceptual ERD:
erDiagram MESSAGES ||--o{ MESSAGE_STATUS_UPDATES : has MESSAGES { string messageId PK ""MessageBird ID"" string recipient string originator string body string initialStatus datetime createdAt datetime updatedAt } MESSAGE_STATUS_UPDATES { int updateId PK string messageId FK string status datetime statusTimestamp datetime receivedAt }
-
Data Layer Interaction (Pseudocode within webhook handler): This pseudocode shows where database interaction would fit inside the webhook handler's
try
block, after signature verification (added in the next step).// Inside the webhook handler's try block, after signature verification const { id: messageId, status, statusDatetime, recipient } = request.body; try { // Assuming a db connection and model/function `updateMessageStatus` await db.updateMessageStatus({ messageId: messageId, newStatus: status, statusTimestamp: new Date(statusDatetime), // Convert to Date object receivedAt: new Date() }); fastify.log.info(`Successfully updated status for message [${messageId}] to [${status}] in DB`); // Optional: Update a main 'messages' table current status as well // await db.updateMainMessage({ messageId, currentStatus: status }); } catch (dbError) { fastify.log.error(dbError, `Database error updating status for message [${messageId}]`); // CRITICAL: Even if DB fails, we already sent 200 OK. // Implement alerting or add to a dead-letter queue for manual review/retry. }
- Use the
messageId
from the webhook payload as the primary key or foreign key to find and update your message record. - Store the
status
andstatusTimestamp
. - Consider storing when your server received the update (
receivedAt
) to track potential delays. - Use a database library/ORM (like Prisma, Sequelize, Knex.js) for actual implementation. Migrations would be handled by the chosen tool.
- Use the
7. Adding Security Features
Securing your webhook endpoint is paramount.
-
Webhook Signature Verification (CRITICAL): This confirms the request genuinely came from MessageBird.
- Get Raw Body: Signature verification needs the raw, unmodified request body. Fastify parses JSON by default. We need to configure it to give us the raw body as well. Add the following near the top of
src/server.js
, after instantiatingfastify
.
// src/server.js (near the top, after const fastify = require('fastify')({...});) const crypto = require('node:crypto'); // Add crypto require here // Add a content type parser to store the raw body fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => { try { // Store raw body on the request object req.rawBody = body; // Parse JSON as usual for request.body const json = JSON.parse(body.toString()); done(null, json); } catch (err) { err.statusCode = 400; done(err, undefined); } });
- Implement Verification Logic: Create a function to perform the check. Add this function somewhere in
src/server.js
(e.g., before the route handlers).
// src/server.js (add this function somewhere in the file) function verifyMessageBirdSignature(request, signingKey) { const signature = request.headers['messagebird-signature']; const timestamp = request.headers['messagebird-request-timestamp']; const rawBody = request.rawBody; // Get raw body stored by content type parser if (!signature || !timestamp || !rawBody) { request.log.warn('Missing signature, timestamp, or rawBody for verification'); return false; } try { const hmac = crypto.createHmac('sha256', signingKey); // IMPORTANT: MessageBird documentation specifies the payload format as: // webhook request timestamp + '.' + webhook request body const signedPayload = `${timestamp}.${rawBody.toString()}`; // Ensure rawBody is treated as a string const expectedSignature = hmac.update(signedPayload).digest('hex'); // Compare signatures using timing-safe comparison const trusted = Buffer.from(signature, 'utf8'); const untrusted = Buffer.from(expectedSignature, 'utf8'); if (trusted.length !== untrusted.length) { request.log.warn('Signature length mismatch'); return false; } const signaturesMatch = crypto.timingSafeEqual(trusted, untrusted); if (!signaturesMatch) { request.log.warn('Signature verification failed: Signatures do not match'); } return signaturesMatch; } catch (error) { request.log.error(error, 'Error during signature verification'); return false; } }
- Update Webhook Handler: Modify the
/webhooks/messagebird
route handler to call the verification function.
// src/server.js - Update the '/webhooks/messagebird' route handler again fastify.post('/webhooks/messagebird', async (request, reply) => { fastify.log.info('Received MessageBird webhook'); // --- 1. Verify Signature --- const signingKey = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_KEY; if (!signingKey || signingKey === 'YOUR_MESSAGEBIRD_WEBHOOK_SIGNING_KEY' || signingKey === 'paste_your_webhook_signing_key_here') { // Check if key is missing or still the placeholder value fastify.log.error('FATAL: MESSAGEBIRD_WEBHOOK_SIGNING_KEY is not configured or is set to a placeholder value.'); reply.code(500).send({ error: 'Webhook signing key not configured' }); // Critical config error return; } if (!verifyMessageBirdSignature(request, signingKey)) { fastify.log.error('Webhook signature verification failed!'); reply.code(401).send({ error: 'Invalid signature' }); // Use 401 Unauthorized return; // Stop processing } fastify.log.info('Webhook signature verified successfully.'); // --- End Verification --- // 2. Process Payload (Basic Logging Example) try { const payload = request.body; if (payload && payload.id && payload.status) { const messageId = payload.id; const status = payload.status; const statusTimestamp = payload.statusDatetime; const recipient = payload.recipient; // --- Placeholder for Real Application Logic --- // (Database updates, queueing, etc. - see Step 6) // --- End Placeholder --- fastify.log.info(`Message ID [${messageId}] for recipient [${recipient}] updated to status [${status}] at [${statusTimestamp}]`); } else { fastify.log.warn('Received webhook with missing id or status after verification'); } } catch (error) { fastify.log.error(error, 'Error processing webhook payload after signature verification'); // Still send 200 OK as the request was authenticated and received } // 3. Acknowledge receipt reply.code(200).send({ status: 'received' }); });
- This code adds the
verifyMessageBirdSignature
function using Node'scrypto
module. - It retrieves the necessary headers (
messagebird-signature
,messagebird-request-timestamp
) and therawBody
we stored earlier via the content type parser. - It constructs the signed payload string according to MessageBird's specification (
timestamp.rawBody
). - It calculates the expected signature using HMAC-SHA256 and the
MESSAGEBIRD_WEBHOOK_SIGNING_KEY
. - It uses
crypto.timingSafeEqual
to prevent timing attacks when comparing signatures. - The main handler now calls this function at the beginning. If verification fails, it logs an error and sends a
401 Unauthorized
response, stopping further processing. It also includes a check to ensure the signing key environment variable is actually configured and not left as a placeholder.
- Get Raw Body: Signature verification needs the raw, unmodified request body. Fastify parses JSON by default. We need to configure it to give us the raw body as well. Add the following near the top of
-
Input Validation: While signature verification is key, basic checks on the payload structure (as shown in the processing step: checking for
payload.id
andpayload.status
) help prevent errors if MessageBird's payload format changes unexpectedly. For more complex validation, consider using Fastify's built-in schema validation capabilities. -
Rate Limiting: Protect your endpoint from abuse or misconfigured retries.
- Install the plugin:
npm install @fastify/rate-limit
- Register it within the
start
function insrc/server.js
, beforefastify.listen
:
// src/server.js (inside the start function, before fastify.listen) await fastify.register(require('@fastify/rate-limit'), { max: 100, // Max requests per time window per IP (adjust as needed) timeWindow: '1 minute' });
This adds basic IP-based rate limiting. Configure
max
andtimeWindow
based on expected legitimate traffic from MessageBird. - Install the plugin:
-
HTTPS: Always use HTTPS for your webhook endpoint.
ngrok
provides this automatically for development. In production, ensure your deployment environment (e.g., behind a load balancer or reverse proxy) terminates TLS/SSL.
8. Handling Special Cases
Real-world scenarios require handling edge cases.
- Duplicate Webhooks: MessageBird might occasionally send the same update twice (e.g., during network issues or retries). Your processing logic should be idempotent. Before updating a status, check if the current status in your database is already the same as the incoming one, or if the incoming
statusTimestamp
is older than the last recorded update for that message. - Out-of-Order Updates: Network latency could cause an older status update (e.g., 'sent') to arrive after a newer one ('delivered'). Always use the
statusTimestamp
from the payload to determine the actual sequence of events, not the time your server received the webhook (receivedAt
). When updating your database, ensure you don't overwrite a newer status with an older one based on thestatusTimestamp
. - Different Status Types: Handle all possible statuses (
sent
,delivered
,buffered
,failed
,expired
,delivery_failed
, etc.) appropriately in your logic. Failures might require specific alerting or cleanup actions. Consult MessageBird documentation for a full list of statuses. - Timestamp Time Zones: The
statusDatetime
is typically in ISO 8601 format with a UTC offset (often+00:00
). Store timestamps consistently in your database, preferably as UTC (e.g., using JavaScriptDate
objects or database timestamp types that handle time zones), and handle time zone conversions only when displaying data to users.
9. Implementing Performance Optimizations
Webhook endpoints need to be fast to respond to MessageBird promptly.
- Respond Quickly: As emphasized, send the
200 OK
response before doing any potentially slow processing (like complex database updates, external API calls). Signature verification should be fast enough to happen before the response. - Asynchronous Processing: Offload heavy tasks (database writes, further API calls, notifications) to a background job queue (e.g., BullMQ with Redis, RabbitMQ, Kafka, AWS SQS). The webhook handler simply verifies the signature, parses essential data, pushes a job to the queue, and returns
200 OK
. A separate worker process consumes jobs from the queue and handles the database updates or other logic. - Database Indexing: Ensure your
messages
table (or equivalent) is indexed onmessageId
(or whatever field you use to look up messages based on the webhook payload) for fast lookups when processing updates. - Caching: Caching is less relevant for writing status updates but could be used if the webhook needed to read related data frequently during processing (though this should ideally be offloaded to the async worker).
10. Adding Monitoring, Observability, and Analytics
Know what your webhook handler is doing in production.
- Health Checks: Add a simple health check endpoint that monitoring systems can poll.
// src/server.js (add route, e.g., before start function) fastify.get('/health', async (request, reply) => { // Add checks for DB connection, queue status, etc. if needed return { status: 'ok', timestamp: new Date().toISOString() }; });
- Metrics: Track key metrics:
- Number of webhooks received (
/webhooks/messagebird
requests). - Number of successful signature verifications.
- Number of failed signature verifications (401 responses).
- Number of successfully processed updates (based on logs or custom metrics).
- Number of processing errors (logged errors after verification).
- Latency of webhook processing (time from request start to sending 200 OK).
Use monitoring tools like Prometheus/Grafana, Datadog, New Relic, or your cloud provider's monitoring services. Fastify plugins like
fastify-metrics
can help expose Prometheus metrics.
- Number of webhooks received (
- Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag, Datadog APM). Fastify's
setErrorHandler
can be used to capture unhandled errors globally and report them. - Logging: Ensure logs are structured (JSON format is good, which Pino, Fastify's default logger, provides) and aggregated in a central logging system (e.g., ELK stack, Loki, Datadog Logs, CloudWatch Logs) for analysis, searching, and alerting on specific error patterns.
11. Troubleshooting and Caveats
- Webhook Not Received:
- Firewall: Ensure your server/firewall allows incoming connections on the configured port (e.g., 3000) from MessageBird's IP ranges. While signature verification is the primary security measure, firewalls can still block initial connections. Check MessageBird documentation for their current IP ranges if strict IP whitelisting is necessary, but prefer relying on signature verification.
- URL Incorrect: Double-check the webhook URL configured in the MessageBird dashboard. Ensure it exactly matches your publicly accessible endpoint, including
https://
and the correct path (/webhooks/messagebird
). Verify that yourngrok
tunnel (or production URL) is active and pointing to the running Fastify application. - Server Not Running: Ensure your Fastify server is running and listening on the correct port and host.
- DNS Issues: In production, ensure DNS records for your webhook URL are correctly configured and propagated.
- Signature Verification Fails (401 Errors):
- Incorrect Signing Key: Verify that the
MESSAGEBIRD_WEBHOOK_SIGNING_KEY
in your.env
file exactly matches the key generated and displayed in the MessageBird dashboard for that specific webhook URL. Ensure there are no extra spaces or characters. Remember to restart your server after updating.env
. - Raw Body Modification: Ensure no middleware is modifying the raw request body before the
verifyMessageBirdSignature
function accessesrequest.rawBody
. TheaddContentTypeParser
setup should preserve it correctly. - Timestamp Skew: While less common, significant clock differences between MessageBird's servers and yours could theoretically affect timestamp-based elements, though the signature itself doesn't directly rely on comparing timestamps for validity beyond its inclusion in the signed payload. Ensure your server's clock is synchronized (e.g., using NTP).
- Payload Construction: Double-check the
signedPayload
construction inverifyMessageBirdSignature
matches MessageBird's requirement:${timestamp}.${rawBody.toString()}
.
- Incorrect Signing Key: Verify that the
- Processing Errors (Logged after 200 OK):
- Database Issues: Check database connection, permissions, schema mismatches, or constraint violations.
- Payload Format Changes: MessageBird might update their payload structure. Add robust checks and logging for unexpected or missing fields.
- Downstream Service Failures: If your processing involves calling other APIs, handle potential failures gracefully.
- MessageBird Retries: If MessageBird repeatedly sends the same webhook, it usually means your endpoint isn't consistently returning a
2xx
status code within their timeout period (typically a few seconds). Focus on responding quickly (Step 9) and ensuring signature verification is correct. Check MessageBird's webhook delivery logs in their dashboard for details on failures.