messaging channels

Sent logo
Sent TeamMar 8, 2026 / messaging channels / MessageBird

MessageBird Fastify Two-Way SMS: Build Inbound Messaging with Node.js Webhooks

Step-by-step guide to implementing two-way SMS messaging with MessageBird webhooks in Fastify. Learn JWT signature verification, automated SMS replies, rate limiting, and production deployment for interactive SMS applications.

Implementing Inbound Two-Way SMS Messaging with MessageBird, Node.js, and Fastify

Introduction

Build a two-way SMS system with MessageBird (now Bird), Node.js, and Fastify. Learn to receive SMS messages via webhooks, verify their authenticity, and send automated replies—essential for interactive SMS applications like customer support bots, survey systems, and appointment reminders.

Two-way SMS delivers substantially higher engagement than one-way messaging: industry data from 2024-2025 shows SMS achieves 45% response rates compared to email's 6%, with 98% open rates and 90% of messages read within 3 minutes. Two-way SMS excels for time-sensitive interactions requiring customer participation.

What you'll build: A production-ready Fastify server that receives inbound SMS messages through MessageBird webhooks, validates message authenticity using JWT signature verification, and sends automated responses.

Note: MessageBird rebranded to Bird in early 2024. The SMS API and Node.js SDK functionality remain unchanged, though you may see both names in documentation and interfaces.

Prerequisites

Ensure you have:

  • Node.js v20 LTS or v22 LTS installed on your development machine. (These are the current LTS versions as of October 2025. While older versions like v16 may work, using current LTS versions ensures the best security updates and performance.)
  • MessageBird Account: Sign up at messagebird.com to obtain your API credentials.
  • API Key: Generate a live API key from your MessageBird dashboard (found under Developers → API Access).
  • Virtual Mobile Number: Purchase a phone number with SMS capabilities from MessageBird to receive inbound messages. Virtual mobile numbers vary by country; US numbers typically cost $1-2/month with instant activation. Setup typically takes 5-15 minutes once you've added account balance.
  • Basic Knowledge: Familiarity with JavaScript, async/await patterns, and REST API concepts.
  • Development Tools: A code editor (VS Code, Sublime Text, etc.) and terminal access.

For related guides on building SMS applications, see our tutorials on MessageBird bulk SMS broadcasting and SMS marketing campaigns with Node.js.

What Is Two-Way SMS Messaging and Why Use It?

Two-way SMS messaging enables bidirectional communication between your application and mobile users. Unlike one-way SMS broadcasts, two-way messaging allows customers to respond, creating interactive conversations that drive engagement and automate customer service workflows.

Common use cases for two-way SMS:

  • Customer support automation — Answer frequently asked questions instantly without human intervention
  • Appointment confirmations — Allow customers to confirm, reschedule, or cancel appointments via SMS
  • Survey and feedback collection — Gather customer opinions through conversational SMS interactions
  • Order status updates — Enable customers to check order status by texting keywords to your number
  • Opt-in and consent management — Handle subscription preferences and marketing consent requests
  • Interactive marketing campaigns — Run contests, polls, and engagement campaigns through SMS

Why choose Fastify for SMS webhooks?

Fastify excels at webhook handling with high performance (up to 30,000 requests per second), low overhead, and built-in features like schema validation, logging, and plugin architecture. Compared to Express.js, Fastify offers approximately 2-3x better throughput and lower latency for webhook-heavy workloads, with built-in JSON schema validation and TypeScript support.

Understanding Two-Way SMS Webhook Architecture

Two-way SMS messaging involves both sending and receiving messages. Here's how the system works:

  1. Customer sends SMS to your MessageBird virtual number
  2. MessageBird receives the message and triggers a webhook to your server
  3. Your Fastify server receives the webhook, verifies its authenticity, and processes the message
  4. Your application sends an automated response back through MessageBird's API
  5. Customer receives your reply on their mobile device

Webhook retry behavior: MessageBird retries failed webhook deliveries over an 8-hour period with exponential backoff. Respond with HTTP 200 within 10 seconds to prevent retries. If your endpoint consistently fails, MessageBird may disable the webhook. Implement idempotency checks using the message id field to handle duplicate deliveries safely.

How to Set Up Your MessageBird Fastify Project

Create a new directory for your project and initialize a Node.js application:

bash
mkdir messagebird-fastify-inbound-sms
cd messagebird-fastify-inbound-sms
npm init -y

Install the required dependencies:

bash
npm install fastify messagebird dotenv pino-pretty qs async-retry @fastify/rate-limit

Package purposes:

  • fastify — Fast and low-overhead web framework for Node.js (Note: This tutorial uses Fastify v5.x. If you're using an older version, some APIs may differ.)
  • messagebird — Official MessageBird Node.js SDK for sending SMS and accessing the API
  • dotenv — Loads environment variables from .env file for secure credential management
  • pino-pretty — Pretty-prints Fastify's JSON logs for easier development debugging
  • qs — Parses URL-encoded webhook payloads from MessageBird
  • async-retry — Implements retry logic for failed API calls
  • @fastify/rate-limit — Protects your webhook endpoint from abuse

How to Configure Environment Variables for MessageBird

Create a .env file in your project root to store sensitive credentials securely:

bash
# .env
MESSAGEBIRD_API_KEY=your_live_api_key_here
MESSAGEBIRD_WEBHOOK_SIGNING_KEY=your_webhook_signing_key_here
MESSAGEBIRD_ORIGINATOR=+1234567890
PORT=3000
NODE_ENV=development

Configuration details:

  • MESSAGEBIRD_API_KEY — Your live API key from MessageBird dashboard
  • MESSAGEBIRD_WEBHOOK_SIGNING_KEY — Signing key for webhook signature verification (found in webhook settings)
  • MESSAGEBIRD_ORIGINATOR — Your MessageBird virtual number in E.164 format (e.g., +14155552671)
  • PORT — Port number for your Fastify server (default: 3000)
  • NODE_ENV — Environment indicator (development or production)

How to obtain your webhook signing key: Log into your MessageBird Dashboard → Settings → Developers → Webhooks. When creating or editing a webhook, you'll see a "Signing Key" section. Click "Generate" to create a new signing key, then copy this key to your .env file as MESSAGEBIRD_WEBHOOK_SIGNING_KEY. Store this key securely—you cannot retrieve it again after leaving the page.

Security note: Add .env to your .gitignore file to prevent accidentally committing sensitive credentials to version control.

How to Create a Fastify Server for Inbound SMS Webhooks

Create a file named server.js and set up your basic Fastify server:

javascript
// server.js
'use strict';

require('dotenv').config();
const fastify = require('fastify');
const messagebird = require('messagebird');
const qs = require('qs');
const retry = require('async-retry');

// Initialize Fastify with logging
const app = fastify({
  logger: {
    transport: {
      target: 'pino-pretty',
      options: {
        translateTime: 'HH:MM:ss Z',
        ignore: 'pid,hostname',
        colorize: true
      }
    }
  },
  // Enable raw body for signature verification
  disableRequestLogging: false,
  bodyLimit: 1048576 // 1MB limit
});

// Initialize MessageBird client
const mbClient = messagebird.initClient(process.env.MESSAGEBIRD_API_KEY);

// ... webhook handlers will go here ...

// Start the server
const start = async () => {
  try {
    const port = process.env.PORT || 3000;
    await app.listen({ port, host: '0.0.0.0' });
    app.log.info(`Server listening on port ${port}`);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};

start();

Code explanation:

  • Initializes Fastify with pretty logging for development
  • Creates a MessageBird client using messagebird.initClient() with your API key
  • Configures the server to listen on all network interfaces (0.0.0.0) for webhook access
  • Implements graceful error handling in the startup function
  • The 1 MB bodyLimit accommodates typical webhook payloads (usually 1-5 KB) while preventing memory exhaustion from malformed requests

How to Verify MessageBird Webhook Signatures with JWT

Verify incoming webhooks to ensure they originate from MessageBird. Add this verification function to your server.js:

javascript
/**
 * Verifies MessageBird webhook JWT signature
 * @param {FastifyRequest} request - The Fastify request object
 * @param {FastifyReply} reply - The Fastify reply object
 */
async function verifyMessageBirdSignature (request, reply) {
  const signatureJWT = request.headers['messagebird-signature-jwt'];
  const webhookSigningKey = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_KEY;
  const rawBody = request.rawBodyString;

  if (!signatureJWT || !webhookSigningKey || typeof rawBody !== 'string') {
    request.log.warn({
        signatureJWT: !!signatureJWT,
        key: !!webhookSigningKey,
        rawBodyType: typeof rawBody
    }, 'Missing JWT signature header, signing key, or raw body.');
    reply.code(400).send({ error: 'Missing MessageBird signature JWT, key, or raw body' });
    return;
  }

  try {
    const isValid = messagebird.webhookSignatureJwt.verifySignature(
      signatureJWT,
      webhookSigningKey,
      request.raw.url,
      rawBody
    );

    if (!isValid) {
      request.log.error('Invalid webhook JWT signature received.');
      reply.code(401).send({ error: 'Invalid MessageBird signature' });
      return;
    }

    request.log.info('Webhook JWT signature verified successfully.');
  } catch (error) {
    request.log.error({ err: error }, 'Error during JWT signature verification.');
    reply.code(500).send({ error: 'Webhook signature verification failed internally' });
  }
}

Note: As of October 2025, MessageBird uses the MessageBird-Signature-JWT header for webhook authentication. This replaces older HMAC-based verification methods. Always use the SDK's built-in verification for security.

Security considerations:

  • Validates the JWT signature using MessageBird's SDK verification method
  • Rejects requests with missing or invalid signatures to prevent spoofed webhooks
  • Logs all verification attempts for security auditing
  • Uses the raw request body (not parsed) for signature verification

What JWT verification protects against: JWT signature verification with timestamps prevents replay attacks, man-in-the-middle tampering, and forged requests. The JWT includes nbf (not before) and exp (expiration) claims that ensure the webhook was sent recently (typically valid for 5 minutes), preventing attackers from capturing and replaying old webhook requests. The signature also validates that the URL and payload haven't been altered in transit.

How to Create a Webhook Endpoint for Inbound SMS Messages

Add a custom content type parser for raw body access, then create your webhook endpoint:

javascript
// Add raw body parser before webhook route
app.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, function (req, body, done) {
  req.rawBodyString = body;
  try {
    const parsed = qs.parse(body);
    done(null, parsed);
  } catch (err) {
    err.statusCode = 400;
    done(err, undefined);
  }
});

// Webhook endpoint for inbound SMS
app.post('/webhooks/inbound-sms', {
  preHandler: verifyMessageBirdSignature
}, async (request, reply) => {
  try {
    const payload = request.body;

    request.log.info({ payload }, 'Received inbound SMS webhook');

    // Extract message details
    const {
      id: messageId,
      originator: senderNumber,
      recipient: yourNumber,
      body: messageText,
      createdDatetime
    } = payload;

    // Validate required fields
    if (!senderNumber || !messageText) {
      request.log.error({ payload }, 'Webhook payload missing required fields');
      return reply.code(400).send({ error: 'Invalid payload' });
    }

    request.log.info({
      messageId,
      from: senderNumber,
      to: yourNumber,
      text: messageText,
      timestamp: createdDatetime
    }, 'Processing inbound message');

    // Process the message (send auto-reply)
    await sendAutoReply(senderNumber, messageText, request.log);

    // Acknowledge receipt to MessageBird
    reply.code(200).send({ status: 'received' });

  } catch (error) {
    request.log.error({ err: error }, 'Error processing webhook');
    reply.code(500).send({ error: 'Internal server error' });
  }
});

Webhook handling flow:

  1. Parse the URL-encoded webhook payload from MessageBird
  2. Extract essential message details (sender, recipient, content, timestamp)
  3. Validate required fields
  4. Process the message and send an automated response
  5. Return a 200 OK status to acknowledge successful receipt

Complete webhook payload structure: MessageBird inbound SMS webhooks include these fields: id (unique message ID), originator (sender's phone number), recipient (your virtual number), body (message text), createdDatetime (RFC3339 timestamp), direction (always "mo" for mobile-originated), type (usually "sms"), datacoding ("plain" or "unicode"), and messageLength (character count). See the full payload specification in MessageBird's API documentation.

Response handling: If your server returns any non-2xx status code (including 500 errors or timeouts), MessageBird will retry webhook delivery with exponential backoff over 8 hours. Return 200 OK only after successfully processing and storing the message. If you need async processing, queue the message first, then return 200 to prevent retries.

How to Send Automated SMS Replies with MessageBird API

Add a function to send automated SMS responses with retry logic:

javascript
/**
 * Sends an automated SMS reply
 * @param {string} recipient - Phone number to send reply to (E.164 format)
 * @param {string} originalMessage - Original message text received
 * @param {Object} logger - Pino logger instance
 */
async function sendAutoReply(recipient, originalMessage, logger) {
  // Generate dynamic reply based on message content
  const replyText = generateReplyMessage(originalMessage);

  const params = {
    originator: process.env.MESSAGEBIRD_ORIGINATOR,
    recipients: [recipient],
    body: replyText
  };

  try {
    // Use retry logic for resilient API calls
    const response = await retry(
      async (bail) => {
        return new Promise((resolve, reject) => {
          mbClient.messages.create(params, (err, response) => {
            if (err) {
              // Don't retry client errors (4xx)
              if (err.statusCode && err.statusCode < 500) {
                logger.error({ err, recipient }, 'Client error sending SMS - not retrying');
                bail(err);
                return;
              }
              reject(err);
            } else {
              resolve(response);
            }
          });
        });
      },
      {
        retries: 3,
        minTimeout: 1000,
        maxTimeout: 5000,
        onRetry: (err, attempt) => {
          logger.warn({ err, attempt, recipient }, 'Retrying SMS send');
        }
      }
    );

    logger.info({
      messageId: response.id,
      recipient,
      status: response.recipients.items[0].status
    }, 'Auto-reply sent successfully');

  } catch (error) {
    logger.error({ err: error, recipient }, 'Failed to send auto-reply after retries');
    throw error;
  }
}

/**
 * Generates contextual reply message
 * @param {string} incomingMessage - Original message from customer
 * @returns {string} Reply message text
 */
function generateReplyMessage(incomingMessage) {
  const message = incomingMessage.toLowerCase().trim();

  // Simple keyword-based responses
  if (message.includes('help') || message.includes('support')) {
    return 'Thanks for reaching out! Our support team will respond within 24 hours. For urgent issues, call 1-800-555-0123.';
  }

  if (message.includes('hours') || message.includes('open')) {
    return 'We\'re open Monday-Friday 9 AM-6 PM EST. Visit our website for more information: example.com/hours';
  }

  if (message.includes('price') || message.includes('cost')) {
    return 'For pricing information, visit example.com/pricing or reply with your email address for a detailed quote.';
  }

  // Default response
  return `Thanks for your message: "${incomingMessage}". We'll get back to you shortly!`;
}

Implementation highlights:

  • Uses retry logic with exponential backoff for API reliability
  • Distinguishes between client errors (don't retry) and server errors (retry up to 3 times)
  • Generates contextual responses based on message keywords
  • Logs all send attempts for debugging and monitoring
  • Handles failures gracefully with detailed error logging

SMS character limits and concatenation: Standard SMS messages support 160 characters using GSM-7 encoding or 70 characters using Unicode (UCS-2). Messages exceeding these limits are automatically split into segments of 153 characters (GSM-7) or 67 characters (Unicode), with each segment billed separately. MessageBird supports up to 9 concatenated segments (1,377 GSM-7 or 603 Unicode characters maximum). The recipient's device automatically reassembles segments. Monitor your reply message lengths to control costs—a 161-character message costs 2× a 160-character message.

How to Add Rate Limiting to Your SMS Webhook

Protect your webhook endpoint from abuse by adding rate limiting:

javascript
// Register rate limiting plugin (add near top of file, after Fastify initialization)
app.register(require('@fastify/rate-limit'), {
  max: 100, // Maximum requests per timeWindow
  timeWindow: '1 minute',
  cache: 10000,
  allowList: [], // Add trusted IPs here if available
  redis: null, // Use Redis for distributed rate limiting in production
  skipOnError: true,
  keyGenerator: (request) => {
    // Rate limit by IP address
    return request.ip;
  },
  errorResponseBuilder: (request, context) => {
    return {
      code: 429,
      error: 'Too Many Requests',
      message: `Rate limit exceeded, retry in ${context.after}`,
      expiresIn: context.after
    };
  }
});

Rate limiting configuration:

  • Allows 100 requests per minute per IP address
  • Caches rate limit data for 10,000 unique IPs
  • Can whitelist MessageBird's webhook IPs for production
  • Returns clear error messages when limits are exceeded
  • Can scale to distributed systems using Redis in production

MessageBird webhook IP ranges: MessageBird's REST API uses dynamic IP addresses from globally distributed infrastructure and cannot be whitelisted. Instead of IP whitelisting, use the JWT signature verification (shown above) to authenticate webhooks. For additional security, you can rate-limit by sender phone number (extracted from webhook payload) in addition to IP-based limits to prevent spam from specific numbers.

How to Configure MessageBird Webhook Settings

Connect your Fastify server to MessageBird:

  1. Deploy your server to a publicly accessible URL (use ngrok for local testing)
  2. Navigate to MessageBird dashboard → Settings → Webhooks
  3. Create new webhook with these settings:
    • Webhook URL: https://your-domain.com/webhooks/inbound-sms
    • Webhook Trigger: "Incoming SMS"
    • Signing Key: Copy this key to your .env file as MESSAGEBIRD_WEBHOOK_SIGNING_KEY
  4. Test webhook using MessageBird's test feature
  5. Activate webhook after successful testing

Local development with ngrok:

bash
# Install ngrok
npm install -g ngrok

# Start your Fastify server
node server.js

# In another terminal, expose your local server
ngrok http 3000

# Use the ngrok HTTPS URL (e.g., https://abc123.ngrok.io) in MessageBird webhook settings

How to Test Your Two-Way SMS Implementation

Test your two-way SMS system:

  1. Start your server:

    bash
    node server.js
  2. Send a test SMS to your MessageBird virtual number from your mobile phone

  3. Monitor server logs to verify:

    • Webhook received and signature verified
    • Message details extracted correctly
    • Auto-reply sent successfully
  4. Check your phone for the automated response

Expected log output:

json
{
  "level": 30,
  "time": "14:23:45 EST",
  "msg": "Webhook JWT signature verified successfully."
}
{
  "level": 30,
  "time": "14:23:45 EST",
  "messageId": "abc123def456",
  "from": "+15551234567",
  "to": "+14155552671",
  "text": "help",
  "msg": "Processing inbound message"
}
{
  "level": 30,
  "time": "14:23:46 EST",
  "messageId": "xyz789ghi012",
  "recipient": "+15551234567",
  "status": "sent",
  "msg": "Auto-reply sent successfully"
}

Troubleshooting checklist when testing fails:

  • No webhook received: Verify webhook URL is publicly accessible (test with curl https://your-domain.com/webhooks/inbound-sms), check MessageBird dashboard webhook status is "Active", confirm virtual number has webhook configured
  • 401/400 signature errors: Verify MESSAGEBIRD_WEBHOOK_SIGNING_KEY matches the key shown in MessageBird dashboard, ensure raw body parser is registered before route handler, check header name is lowercase messagebird-signature-jwt
  • 500 server errors: Check server logs for stack traces, verify MESSAGEBIRD_API_KEY is valid, ensure all dependencies are installed (npm install)
  • Reply not received: Verify MESSAGEBIRD_ORIGINATOR is in E.164 format with country code, check MessageBird account balance is sufficient, review MessageBird error codes in logs

Complete Code Example

Here's the full implementation with all components:

javascript
// server.js - Complete implementation
'use strict';

require('dotenv').config();
const fastify = require('fastify');
const messagebird = require('messagebird');
const qs = require('qs');
const retry = require('async-retry');

// Initialize Fastify with logging
const app = fastify({
  logger: {
    transport: {
      target: 'pino-pretty',
      options: {
        translateTime: 'HH:MM:ss Z',
        ignore: 'pid,hostname',
        colorize: true
      }
    }
  },
  disableRequestLogging: false,
  bodyLimit: 1048576
});

// Initialize MessageBird client
const mbClient = messagebird.initClient(process.env.MESSAGEBIRD_API_KEY);

// Register rate limiting
app.register(require('@fastify/rate-limit'), {
  max: 100,
  timeWindow: '1 minute',
  cache: 10000,
  skipOnError: true,
  keyGenerator: (request) => request.ip,
  errorResponseBuilder: (request, context) => ({
    code: 429,
    error: 'Too Many Requests',
    message: `Rate limit exceeded, retry in ${context.after}`,
    expiresIn: context.after
  })
});

// Parse raw body for signature verification
app.addContentTypeParser('application/x-www-form-urlencoded', { parseAs: 'string' }, function (req, body, done) {
  req.rawBodyString = body;
  try {
    const parsed = qs.parse(body);
    done(null, parsed);
  } catch (err) {
    err.statusCode = 400;
    done(err, undefined);
  }
});

/**
 * Verifies MessageBird webhook JWT signature
 */
async function verifyMessageBirdSignature (request, reply) {
  const signatureJWT = request.headers['messagebird-signature-jwt'];
  const webhookSigningKey = process.env.MESSAGEBIRD_WEBHOOK_SIGNING_KEY;
  const rawBody = request.rawBodyString;

  if (!signatureJWT || !webhookSigningKey || typeof rawBody !== 'string') {
    request.log.warn({
        signatureJWT: !!signatureJWT,
        key: !!webhookSigningKey,
        rawBodyType: typeof rawBody
    }, 'Missing JWT signature header, signing key, or raw body.');
    reply.code(400).send({ error: 'Missing MessageBird signature JWT, key, or raw body' });
    return;
  }

  try {
    const isValid = messagebird.webhookSignatureJwt.verifySignature(
      signatureJWT,
      webhookSigningKey,
      request.raw.url,
      rawBody
    );

    if (!isValid) {
      request.log.error('Invalid webhook JWT signature received.');
      reply.code(401).send({ error: 'Invalid MessageBird signature' });
      return;
    }

    request.log.info('Webhook JWT signature verified successfully.');
  } catch (error) {
    request.log.error({ err: error }, 'Error during JWT signature verification.');
    reply.code(500).send({ error: 'Webhook signature verification failed internally' });
  }
}

/**
 * Generates contextual reply message
 */
function generateReplyMessage(incomingMessage) {
  const message = incomingMessage.toLowerCase().trim();

  if (message.includes('help') || message.includes('support')) {
    return 'Thanks for reaching out! Our support team will respond within 24 hours. For urgent issues, call 1-800-555-0123.';
  }

  if (message.includes('hours') || message.includes('open')) {
    return 'We\'re open Monday-Friday 9 AM-6 PM EST. Visit our website for more information: example.com/hours';
  }

  if (message.includes('price') || message.includes('cost')) {
    return 'For pricing information, visit example.com/pricing or reply with your email address for a detailed quote.';
  }

  return `Thanks for your message: "${incomingMessage}". We'll get back to you shortly!`;
}

/**
 * Sends an automated SMS reply with retry logic
 */
async function sendAutoReply(recipient, originalMessage, logger) {
  const replyText = generateReplyMessage(originalMessage);

  const params = {
    originator: process.env.MESSAGEBIRD_ORIGINATOR,
    recipients: [recipient],
    body: replyText
  };

  try {
    const response = await retry(
      async (bail) => {
        return new Promise((resolve, reject) => {
          mbClient.messages.create(params, (err, response) => {
            if (err) {
              if (err.statusCode && err.statusCode < 500) {
                logger.error({ err, recipient }, 'Client error sending SMS - not retrying');
                bail(err);
                return;
              }
              reject(err);
            } else {
              resolve(response);
            }
          });
        });
      },
      {
        retries: 3,
        minTimeout: 1000,
        maxTimeout: 5000,
        onRetry: (err, attempt) => {
          logger.warn({ err, attempt, recipient }, 'Retrying SMS send');
        }
      }
    );

    logger.info({
      messageId: response.id,
      recipient,
      status: response.recipients.items[0].status
    }, 'Auto-reply sent successfully');

  } catch (error) {
    logger.error({ err: error, recipient }, 'Failed to send auto-reply after retries');
    throw error;
  }
}

// Webhook endpoint for inbound SMS
app.post('/webhooks/inbound-sms', {
  preHandler: verifyMessageBirdSignature
}, async (request, reply) => {
  try {
    const payload = request.body;

    request.log.info({ payload }, 'Received inbound SMS webhook');

    const {
      id: messageId,
      originator: senderNumber,
      recipient: yourNumber,
      body: messageText,
      createdDatetime
    } = payload;

    if (!senderNumber || !messageText) {
      request.log.error({ payload }, 'Webhook payload missing required fields');
      return reply.code(400).send({ error: 'Invalid payload' });
    }

    request.log.info({
      messageId,
      from: senderNumber,
      to: yourNumber,
      text: messageText,
      timestamp: createdDatetime
    }, 'Processing inbound message');

    await sendAutoReply(senderNumber, messageText, request.log);

    reply.code(200).send({ status: 'received' });

  } catch (error) {
    request.log.error({ err: error }, 'Error processing webhook');
    reply.code(500).send({ error: 'Internal server error' });
  }
});

// Health check endpoint
app.get('/health', async (request, reply) => {
  reply.send({ status: 'ok', timestamp: new Date().toISOString() });
});

// Start server
const start = async () => {
  try {
    const port = process.env.PORT || 3000;
    await app.listen({ port, host: '0.0.0.0' });
    app.log.info(`Server listening on port ${port}`);
  } catch (err) {
    app.log.error(err);
    process.exit(1);
  }
};

start();

Production Deployment Best Practices for SMS Webhooks

Implement these best practices before deploying to production:

Security Enhancements

  • Use HTTPS only — Never accept webhooks over unencrypted HTTP connections
  • Implement signature verification — Always validate JWT signatures (IP whitelisting is not available for MessageBird webhooks)
  • Rotate signing keys regularly — Update your MESSAGEBIRD_WEBHOOK_SIGNING_KEY periodically (regenerate via MessageBird Dashboard → Developers → Webhooks)
  • Add request validation — Validate all webhook payload fields before processing
  • Enable CORS properly — Configure cross-origin policies for your specific use case

Scalability Improvements

  • Use Redis for rate limiting — Distribute rate limits across multiple server instances
  • Implement message queues — Process webhooks asynchronously using Bull or RabbitMQ
  • Add horizontal scaling — Deploy behind a load balancer for high-traffic scenarios
  • Cache frequently used data — Reduce API calls by caching configuration and lookup data
  • Monitor performance — Track response times, error rates, and throughput metrics

Reliability Features

  • Add dead letter queues — Store failed webhook processing attempts for retry
  • Implement circuit breakers — Protect against cascade failures from external dependencies
  • Use health checks — Monitor server availability with /health endpoints
  • Set up alerts — Notify your team of webhook failures, rate limit hits, or API errors
  • Log aggregation — Centralize logs using services like LogDNA, Datadog, or ELK stack

Error Handling

javascript
// Add global error handler
app.setErrorHandler((error, request, reply) => {
  request.log.error({ err: error, url: request.url }, 'Unhandled error');

  if (error.statusCode) {
    reply.status(error.statusCode).send({
      error: error.message,
      statusCode: error.statusCode
    });
  } else {
    reply.status(500).send({
      error: 'Internal Server Error',
      statusCode: 500
    });
  }
});

// Graceful shutdown
const closeGracefully = async (signal) => {
  app.log.info(`Received ${signal}, closing server gracefully`);
  await app.close();
  process.exit(0);
};

process.on('SIGTERM', closeGracefully);
process.on('SIGINT', closeGracefully);

Troubleshooting Common MessageBird Webhook Issues

Why Is My Webhook Not Receiving Messages?

  • Verify URL accessibility — Ensure your webhook URL is publicly accessible via HTTPS
  • Check MessageBird configuration — Confirm webhook is active and pointing to correct URL
  • Review firewall settings — Allow inbound traffic on your server's webhook port
  • Test with ngrok — Use ngrok to expose localhost for initial testing
  • Check server logs — Look for incoming requests and any error messages

Testing commands:

bash
# Test webhook endpoint manually
curl -X POST https://your-domain.com/webhooks/inbound-sms \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "id=test123&originator=+15551234567&recipient=+14155552671&body=test&createdDatetime=2025-10-05T12:00:00Z"

# Check if port is accessible
nc -zv your-domain.com 443

# View real-time logs
tail -f server.log

Why Is MessageBird Signature Verification Failing?

  • Confirm signing key — Verify your .env file contains the correct MESSAGEBIRD_WEBHOOK_SIGNING_KEY
  • Check header name — Ensure you're reading messagebird-signature-jwt header (lowercase)
  • Verify raw body access — Signature verification requires the unparsed request body
  • Update SDK version — Ensure you're using a current version of the MessageBird SDK
  • Test with MessageBird tools — Use MessageBird's webhook testing feature to verify configuration

Why Aren't My Auto-Replies Sending?

  • Validate API key — Confirm your MESSAGEBIRD_API_KEY has sending permissions
  • Check originator format — Ensure your originator number is in E.164 format (e.g., +14155552671)
  • Review account balance — Verify your MessageBird account has sufficient credits
  • Monitor rate limits — Check if you're exceeding MessageBird's API rate limits (500 POST requests/second)
  • Examine error logs — Review detailed error messages in your Fastify logs

Common MessageBird API error codes:

  • 2 — Request not allowed (invalid API key)
  • 9 — Missing required parameters
  • 10 — Invalid parameter format
  • 20 — Resource not found
  • 25 — Insufficient account balance
  • 429 — Rate limit exceeded (500 POST/s, 50 GET/s limits)

How to Fix High Latency in SMS Processing

  • Optimize reply generation — Cache static responses and minimize processing time
  • Move to async processing — Use message queues for non-urgent replies
  • Reduce retry timeouts — Adjust minTimeout and maxTimeout in retry configuration
  • Check network latency — Test connectivity between your server and MessageBird's API
  • Monitor server resources — Ensure adequate CPU, memory, and network bandwidth

Advanced Features for Two-Way SMS Systems

Build upon this foundation with advanced features:

How to Store SMS Messages in a Database

Store inbound messages for analytics and compliance. For a complete implementation guide, see our tutorial on building SMS marketing campaigns with database integration:

javascript
// Example with PostgreSQL
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Create table schema
/*
CREATE TABLE inbound_sms (
  id SERIAL PRIMARY KEY,
  message_id VARCHAR(255) UNIQUE NOT NULL,
  sender VARCHAR(20) NOT NULL,
  recipient VARCHAR(20) NOT NULL,
  body TEXT NOT NULL,
  received_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_sender ON inbound_sms(sender);
CREATE INDEX idx_received_at ON inbound_sms(received_at);
*/

async function storeInboundMessage(messageData) {
  await pool.query(
    'INSERT INTO inbound_sms (message_id, sender, recipient, body, received_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (message_id) DO NOTHING',
    [messageData.id, messageData.originator, messageData.recipient, messageData.body, messageData.createdDatetime]
  );
}

How to Implement AI-Powered SMS Responses

Implement AI-powered responses or integrate with customer support systems:

javascript
const { OpenAI } = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function generateAIReply(message, sender) {
  const response = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [
      { role: 'system', content: 'You are a helpful customer service assistant. Keep responses under 160 characters.' },
      { role: 'user', content: message }
    ],
    max_tokens: 50,
    temperature: 0.7
  });

  return response.choices[0].message.content.substring(0, 160);
}

How to Track Multi-Step SMS Conversations

Track multi-step conversations using Redis:

javascript
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function getConversationState(phoneNumber) {
  const state = await redis.get(`conversation:${phoneNumber}`);
  return JSON.parse(state) || { step: 0, data: {} };
}

async function updateConversationState(phoneNumber, state) {
  await redis.setex(`conversation:${phoneNumber}`, 3600, JSON.stringify(state));
}

// Example: Multi-step appointment booking
async function handleConversationalReply(sender, message) {
  const state = await getConversationState(sender);

  if (state.step === 0) {
    await updateConversationState(sender, { step: 1, data: {} });
    return 'Would you like to book an appointment? Reply YES or NO';
  } else if (state.step === 1 && message.toLowerCase().includes('yes')) {
    await updateConversationState(sender, { step: 2, data: { confirmed: true } });
    return 'Great! What date works for you? (Format: MM/DD/YYYY)';
  } else if (state.step === 2) {
    await updateConversationState(sender, { step: 3, data: { ...state.data, date: message } });
    return `Appointment requested for ${message}. We'll confirm within 24 hours!`;
  }

  return 'Sorry, I didn\'t understand. Reply HELP for assistance.';
}

How to Handle SMS Opt-Out Requests Automatically

Handle unsubscribe requests automatically:

javascript
const optOutList = new Set(); // In production, use database

function generateReplyMessage(incomingMessage, senderNumber) {
  const message = incomingMessage.toLowerCase().trim();

  // Handle opt-out per TCPA requirements (effective April 11, 2025)
  // Accept any reasonable opt-out language: STOP, QUIT, END, CANCEL, UNSUBSCRIBE, REVOKE, OPT OUT
  if (/\b(stop|quit|end|cancel|unsubscribe|revoke|opt out)\b/i.test(message)) {
    optOutList.add(senderNumber);
    // Must process immediately or within 24 hours per TCPA
    return 'You have been unsubscribed. Reply START to resubscribe. Msg&Data rates may apply.';
  }

  if (message === 'start' && optOutList.has(senderNumber)) {
    optOutList.delete(senderNumber);
    return 'You have been resubscribed. Reply STOP to opt out. Msg&Data rates may apply.';
  }

  // Check opt-out status before responding
  if (optOutList.has(senderNumber)) {
    return null; // Don't send any messages to opted-out numbers
  }

  // ... rest of your reply logic ...
}

Legal compliance for opt-outs: Under the TCPA (effective April 11, 2025), businesses must honor opt-out requests made "in any reasonable manner", including keywords like STOP, QUIT, END, REVOKE, CANCEL, UNSUBSCRIBE, or OPT OUT. Process opt-outs immediately or within 24 hours maximum. Include opt-out instructions in initial message: "Reply STOP to unsubscribe." Maintain opt-out list indefinitely to prevent re-enrollment. For GDPR compliance in EU: honor right-to-erasure requests within 1 month, delete conversation data when no longer needed for the original purpose, maintain audit logs for 6-7 years for legal/tax purposes.

Frequently Asked Questions

How much does MessageBird SMS cost?

MessageBird pricing varies by destination country and message volume. Inbound SMS messages typically cost between $0.005-$0.01 per message, while outbound SMS pricing ranges from $0.01-$0.50+ depending on the destination country. Volume discounts are available for high-traffic applications. Check MessageBird's pricing page for current rates specific to your target countries.

Can I send MMS messages with this setup?

This tutorial focuses on SMS messaging. To add MMS support, modify the sendAutoReply function to include a mediaUrls parameter containing HTTPS URLs of images, videos, or audio files (up to 5 MB per file):

javascript
const params = {
  originator: process.env.MESSAGEBIRD_ORIGINATOR,
  recipients: [recipient],
  body: replyText,
  mediaUrls: ['https://example.com/image.jpg'] // Add for MMS
};

Note that MMS support and pricing vary by country—verify MMS availability for your target regions in the MessageBird dashboard.

How do I handle multiple virtual numbers in one application?

Store a mapping of phone numbers to response logic in your database or configuration file. In your webhook handler, read the recipient field from the webhook payload to identify which MessageBird number received the message, then route to the appropriate response logic. This approach allows you to manage multiple SMS campaigns, departments, or brands from a single Fastify application.

What's the maximum message throughput for this setup?

Fastify can handle 20,000-30,000 requests per second on modern hardware, far exceeding typical SMS webhook volumes. Your actual throughput will be limited by MessageBird's API rate limits (500 POST requests/second for sending messages) and your server's resources. For high-volume scenarios exceeding 100 messages per second, implement message queuing with Redis or RabbitMQ to process webhooks asynchronously.

How do I test MessageBird webhooks locally?

Expose your local development server to the internet with ngrok: ngrok http 3000. Copy the generated HTTPS URL (e.g., https://abc123.ngrok.io) and add your webhook path (/webhooks/inbound-sms) to create the full webhook URL for MessageBird's dashboard. Ngrok provides request inspection for debugging webhook payloads.

Can I use TypeScript with MessageBird and Fastify?

Yes! Both Fastify and MessageBird provide official TypeScript type definitions. Install @types/node and create a tsconfig.json file:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

The MessageBird SDK includes built-in TypeScript types, while Fastify offers comprehensive type support for routes, plugins, and request/reply objects.

How do I implement GDPR compliance for SMS messaging?

Store explicit opt-in consent records with timestamps and IP addresses, honor opt-out requests immediately (within minutes, not hours), provide clear unsubscribe instructions in every marketing message (e.g., "Reply STOP to unsubscribe"), maintain audit logs of all message activity, and implement data retention policies to delete conversation data after your legally required retention period.

Specific requirements: Under GDPR Article 17 (Right to Erasure), respond to deletion requests within 1 month. Delete conversation data when no longer needed for the original purpose (typically 30-90 days for transactional messages, longer for contractual obligations). Retain billing/invoice records for 6-7 years per tax laws, but pseudonymize personal identifiers. Implement automated deletion workflows to purge old conversation data. Document your legal basis for processing (consent, contract, legitimate interest) and retention policies in your privacy policy.

What's the difference between MessageBird and Bird?

MessageBird rebranded to Bird in early 2024 as part of a broader platform consolidation. The SMS API, Node.js SDK (npm package messagebird), and webhook functionality remain unchanged. Existing MessageBird accounts continue to work without migration. The Bird platform adds enhanced CRM and marketing automation features, but the core SMS infrastructure documented in this tutorial remains fully supported and operational.

How do I monitor webhook health and uptime?

Implement a /health endpoint (as shown in the complete code example) for external monitoring services like UptimeRobot or Pingdom. Track key metrics including webhook response time (aim for <500 ms), error rate (should be <1%), signature verification failures, and MessageBird API success rates. Use application performance monitoring (APM) tools like New Relic, Datadog, or Sentry for detailed insights into webhook processing performance. Set alerts for: response time >1 s, error rate >5%, webhook failures >10/hour, or API rate limit hits.

Can I rate limit specific phone numbers that spam my webhook?

Yes. Extend the rate limiting configuration to track both IP addresses and phone numbers:

javascript
const phoneRateLimits = new Map(); // In production, use Redis

app.addHook('preHandler', async (request, reply) => {
  if (request.url === '/webhooks/inbound-sms') {
    const sender = request.body?.originator;
    if (sender) {
      const now = Date.now();
      const limit = phoneRateLimits.get(sender) || { count: 0, resetAt: now + 60000 };

      if (now > limit.resetAt) {
        limit.count = 0;
        limit.resetAt = now + 60000;
      }

      limit.count++;
      phoneRateLimits.set(sender, limit);

      if (limit.count > 10) { // Max 10 messages per minute per number
        reply.code(429).send({ error: 'Rate limit exceeded for this phone number' });
        return;
      }
    }
  }
});

Store rate limit violations in Redis to maintain state across server restarts and enable blocking of persistent spam numbers at the application level.

Conclusion

You've built a production-ready two-way SMS messaging system with MessageBird, Node.js, and Fastify. Your implementation includes webhook signature verification, automated reply logic, rate limiting, and error handling.

Key takeaways:

  • Receive inbound SMS messages through MessageBird webhooks with JWT signature verification
  • Send automated responses based on message content using the MessageBird SDK
  • Handle errors gracefully with retry logic and comprehensive logging
  • Protect your endpoint with rate limiting and security best practices
  • Scale horizontally to handle high-volume SMS traffic

Next steps to enhance your SMS system:

  • Deploy to a production environment (AWS, Google Cloud, Azure, Heroku, or Railway)
  • Add database integration to store conversation history and enable analytics
  • Implement advanced reply logic with natural language processing or AI models
  • Set up monitoring and alerting for webhook failures and performance degradation
  • Scale horizontally with load balancers and message queues for enterprise traffic

For more advanced MessageBird features, explore their official documentation and SMS API reference. Check out related guides on bulk SMS campaigns, message scheduling, and WhatsApp Business API integration for multi-channel messaging strategies.