code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Building Production-Ready Two-Way SMS: Fastify & Infobip Guide

A step-by-step tutorial for receiving and replying to SMS messages using Node.js, Fastify, and the Infobip API.

Project Overview and Goals

This guide details how to build a robust Node.js application using the Fastify framework to handle inbound SMS messages via Infobip webhooks and enable automated replies, creating a functional two-way messaging system.

What We'll Build:

A Fastify server that:

  1. Listens for incoming HTTP POST requests (webhooks) from Infobip containing SMS message data.
  2. Securely validates and processes these incoming messages.
  3. Logs message details for tracking and debugging.
  4. Optionally sends an automated SMS reply back to the original sender via the Infobip API.

Problem Solved:

Many applications need to react to SMS messages sent by users – for customer support, automated information retrieval, receiving commands, or triggering workflows. This guide provides the foundation for building such systems, enabling programmatic interaction via SMS.

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Fastify: A high-performance, low-overhead web framework for Node.js, chosen for its speed, extensibility, and developer-friendly features.
  • Infobip API & SDK: Provides the communication infrastructure for sending and receiving SMS messages. We'll use their webhook system for inbound messages and the official Node.js SDK (@infobip-api/sdk) for outbound replies.
  • dotenv: A zero-dependency module to load environment variables from a .env file into process.env.

System Architecture:

Note: This diagram relies on monospace fonts. For optimal display across all platforms, consider replacing this with an actual image diagram and providing descriptive alt text.

plaintext
+-------------+       +-----------------+       +----------------------+       +---------------------+       +-------------+
| User's Phone| ----> | Infobip Network | ----> | Infobip Webhook Call | ----> | Your Fastify App    | ----> | User's Phone|
| (Sends SMS) |       | (Receives SMS)  |       | (HTTP POST to URL)   |       | (Processes & Replies)|       | (Receives Reply)|
+-------------+       +-----------------+       +----------------------+       +---------------------+       +-------------+
                                                                                    |
                                                                                    |  [Optional Reply]
                                                                                    V
                                                                              +----------------+
                                                                              | Infobip API Call |
                                                                              | (Send SMS)     |
                                                                              +----------------+

Prerequisites:

  • An active Infobip account with API access.
  • A provisioned phone number within your Infobip account capable of sending/receiving SMS.
  • Node.js (v18 or later recommended) and npm (or yarn) installed on your development machine.
  • Basic understanding of JavaScript, Node.js, REST APIs, and webhooks.
  • A tool to expose your local server to the internet for testing webhooks (e.g., ngrok).

Final Outcome:

A running Fastify application capable of receiving SMS messages sent to your Infobip number and logging them. Optionally, it can send an automated reply.


1. Setting Up the Project

Let's initialize our Node.js project and install the necessary dependencies.

1.1 Create Project Directory:

Open your terminal and create a new directory for the project.

bash
mkdir fastify-infobip-sms
cd fastify-infobip-sms

1.2 Initialize Node.js Project:

Initialize the project using npm. You can accept the defaults or customize as needed.

bash
npm init -y

This creates a package.json file.

1.3 Install Dependencies:

We need Fastify for the web server, the Infobip SDK for interacting with their API, and dotenv for managing environment variables.

bash
npm install fastify @infobip-api/sdk dotenv

1.4 Install Development Dependencies (Optional but Recommended):

For a better development experience, install nodemon to automatically restart the server on file changes.

bash
npm install --save-dev nodemon

1.5 Configure package.json Scripts:

Add scripts to your package.json for running the server easily.

json
// package.json
{
  // ... other properties
  "main": "src/server.js", // Specify entry point
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js", // Use nodemon for development
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  // ... other properties
}

Note: The "main": "src/server.js" line points to a file (src/server.js) that we create in the next step. This is standard practice but be aware the file doesn't exist immediately after this package.json edit.

1.6 Create Project Structure:

Organize your code for better maintainability.

bash
mkdir src
mkdir src/routes
mkdir src/config
touch src/server.js
touch src/routes/infobipWebhook.js
touch src/config/environment.js
touch .env
touch .env.example
touch .gitignore
  • src/: Contains all source code.
  • src/routes/: Holds route handlers.
  • src/config/: For configuration files.
  • src/server.js: The main application entry point.
  • .env: Stores sensitive credentials (API keys, etc.). Never commit this file.
  • .env.example: A template showing required environment variables. Commit this file.
  • .gitignore: Specifies intentionally untracked files that Git should ignore.

1.7 Configure .gitignore:

Add node_modules and .env to your .gitignore file to prevent committing them.

plaintext
# .gitignore
node_modules/
.env
*.log

1.8 Set Up Environment Variables:

Define the necessary environment variables in .env.example and .env.

plaintext
# .env.example
# Infobip Credentials
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL # e.g., xyz.api.infobip.com
INFOBIP_WEBHOOK_SECRET=YOUR_OPTIONAL_SHARED_SECRET # Optional, for added security if supported/used

# Server Configuration
PORT=3000
HOST=0.0.0.0 # Listen on all available network interfaces

Important: Copy .env.example to .env and fill in your actual Infobip API Key and Base URL in the .env file. You can find these in your Infobip account dashboard (usually under API Keys). The INFOBIP_WEBHOOK_SECRET is optional; use it only if you configure a corresponding mechanism in Infobip (see Section 6).

1.9 Load Environment Variables:

Create a simple module to load and export environment variables.

javascript
// src/config/environment.js
require('dotenv').config(); // Load variables from .env

const environment = {
  infobip: {
    apiKey: process.env.INFOBIP_API_KEY,
    baseUrl: process.env.INFOBIP_BASE_URL,
    webhookSecret: process.env.INFOBIP_WEBHOOK_SECRET, // Can be undefined
  },
  server: {
    port: parseInt(process.env.PORT || '3000', 10),
    host: process.env.HOST || '0.0.0.0',
  },
};

// Basic validation
if (!environment.infobip.apiKey || !environment.infobip.baseUrl) {
  console.error(
    'FATAL ERROR: INFOBIP_API_KEY and INFOBIP_BASE_URL must be defined in the .env file'
  );
  process.exit(1);
}

module.exports = environment;

1.10 Basic Fastify Server Setup:

Initialize the Fastify server in src/server.js.

javascript
// src/server.js
const fastify = require('fastify')({
  logger: true, // Enable built-in Pino logger
});
const config = require('./config/environment');
const infobipWebhookRoutes = require('./routes/infobipWebhook');

// --- Register Plugins and Routes ---
// Register the webhook routes
fastify.register(infobipWebhookRoutes, { prefix: '/webhook' });

// --- Default Root Route ---
fastify.get('/', async (request, reply) => {
  return { status: 'ok', timestamp: new Date().toISOString() };
});

// --- Health Check Route ---
fastify.get('/health', async (request, reply) => {
  return { status: 'healthy' };
});

// --- Start Server ---
const start = async () => {
  try {
    await fastify.listen({ port: config.server.port, host: config.server.host });
    fastify.log.info(`Server listening on ${fastify.server.address().port}`);
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();

// Export the app instance if needed for testing (alternative to builder function)
// module.exports = fastify;
// Or refactor into a builder function for cleaner testing (see Section 9.2 note)

Now you have a basic Fastify server structure ready. You can test it by running npm run dev. You should see log output indicating the server has started. Accessing http://localhost:3000/ in your browser should return {"status":"ok", ...}.


2. Implementing the Inbound Webhook

The core of receiving SMS messages is handling the HTTP POST request Infobip sends to your application (the webhook).

2.1 Define the Webhook Route Handler:

In src/routes/infobipWebhook.js, create the Fastify route handler that will listen for POST requests at /webhook/infobip.

javascript
// src/routes/infobipWebhook.js
const config = require('../config/environment');
// Import Infobip SDK later when needed for replies
// const { Infobip, AuthType } = require('@infobip-api/sdk');

async function infobipWebhookRoutes(fastify, options) {
  // Optional: Instantiate Infobip client here if needed within this plugin scope
  // const infobipClient = new Infobip({ /* ... */ });

  fastify.post('/infobip', async (request, reply) => {
    fastify.log.info('Received Infobip webhook request');
    fastify.log.debug({ body: request.body }, 'Webhook payload');

    // --- Security Check ---
    // **IMPORTANT:** Verify if/how Infobip supports securing webhooks.
    // Common methods include Basic Authentication or a custom signature header.
    // Consult the Infobip documentation for the *specific mechanism* they provide.
    // The example below uses a hypothetical shared secret header. Adapt as needed.
    if (config.infobip.webhookSecret) {
      const receivedSecret = request.headers['x-webhook-secret']; // Example header - CHECK INFOBIP DOCS
      if (receivedSecret !== config.infobip.webhookSecret) {
         fastify.log.warn('Invalid or missing webhook secret received');
         return reply.code(401).send({ error: 'Unauthorized' });
      }
    }
    // Add checks for Basic Auth (request.headers.authorization) if configured in Infobip.

    // --- Process Incoming Message ---
    try {
      // **IMPORTANT:** Infobip payload structure can vary.
      // Always check the *specific webhook configuration* in your Infobip portal
      // and the official Infobip documentation for the expected payload format.
      // The common structure involves a 'results' array.
      const results = request.body.results;
      if (!results || !Array.isArray(results) || results.length === 0) {
        fastify.log.warn('Webhook payload missing or invalid results array');
        return reply.code(400).send({ error: 'Invalid payload format' });
      }

      // Process each message in the payload (often just one)
      for (const message of results) {
        // **IMPORTANT:** Field names like 'receivedAt', 'text', 'from', 'to', 'messageId'
        // depend on your Infobip webhook configuration. Verify these in the Infobip docs
        // or by inspecting an actual payload received from Infobip.
        const messageId = message.messageId;
        const from = message.from;
        const to = message.to; // Your Infobip number
        const text = message.text;
        const receivedAt = message.receivedAt; // Example field name

        if (!messageId || !from || !text) {
           fastify.log.warn({ message }, 'Skipping message with missing fields');
           continue; // Skip this message, process others if any
        }

        fastify.log.info(
          `Processing message ${messageId} from ${from}: ""${text}""`
        );

        // --- TODO: Implement your business logic here ---
        // Examples:
        // 1. Store the message in a database
        // 2. Trigger a workflow based on keywords in 'text'
        // 3. Send an automated reply (see Section 4)

        // --- Add Reply Logic Here (See Section 4) ---

      } // End for loop

      // --- Acknowledge Receipt ---
      // Crucially, respond quickly to Infobip to prevent timeouts/retries
      reply.code(200).send({ status: 'received' });

    } catch (error) {
      fastify.log.error(
        { err: error },
        'Error processing Infobip webhook payload'
      );
      // Send a generic error response. Avoid leaking internal details.
      reply.code(500).send({ error: 'Internal Server Error' });
    }
  });
}

module.exports = infobipWebhookRoutes;

Explanation:

  1. Route Definition: We define a POST route at /infobip (prefixed with /webhook, making the full path /webhook/infobip).
  2. Logging: We log the incoming request body for debugging. Adjust log levels (info, debug) as needed.
  3. Security Check: Includes a placeholder for validating a secret. Crucially, it emphasizes checking Infobip documentation for their actual supported security mechanisms (Basic Auth, signatures, etc.) and adapting the code accordingly.
  4. Payload Parsing: Checks for the results array. Adds strong emphasis (multiple bolded notes) that the payload structure and field names (results, messageId, from, text, receivedAt) MUST be verified against the specific Infobip configuration and documentation.
  5. Data Extraction & Validation: Extracts fields based on the assumed common structure, with added warnings about verification. Basic checks ensure required fields are present.
  6. Business Logic Placeholder: TODO marks where application-specific logic goes.
  7. Acknowledgement: Sends 200 OK promptly.
  8. Error Handling: try...catch logs errors and sends 500.

3. Integrating with Infobip

Now, we need to tell Infobip where to send the incoming SMS messages.

3.1 Expose Your Local Server (Development Only):

Infobip needs a publicly accessible URL to send webhooks. During development, you can use ngrok.

  1. Download and install ngrok: https://ngrok.com/download

  2. Authenticate ngrok if you haven't already (follow their instructions).

  3. Start your Fastify server: npm run dev (it should be running on port 3000).

  4. In a new terminal window, run:

    bash
    ngrok http 3000
  5. ngrok will display output similar to this:

    Forwarding https://<unique-id>.ngrok-free.app -> http://localhost:3000

    Copy the https://<unique-id>.ngrok-free.app URL. This is your public URL. Remember that free ngrok URLs are temporary and change every time you restart ngrok. For stable testing or production, you need a permanent URL (see Section 8).

3.2 Configure Infobip Webhook:

  1. Log in to your Infobip account portal (https://portal.infobip.com/).
  2. Navigate to the section for managing your numbers or SMS settings (e.g., "Apps", "Numbers", "Channels -> SMS"). Look for incoming message settings or webhook configurations.
  3. Find the phone number you want to use.
  4. Locate the option to configure "Incoming Messages", "Forward to URL", or "Webhook URL".
  5. Enter your public webhook URL: https://<unique-id>.ngrok-free.app/webhook/infobip (use your actual ngrok URL).
  6. Security (Optional but Recommended): Configure webhook security in Infobip (e.g., Basic Authentication, specific headers/signatures if offered) and ensure your application code (Section 2.1) validates it correctly.
  7. Save the configuration.

3.3 Verify API Key and Base URL:

Double-check that the INFOBIP_API_KEY and INFOBIP_BASE_URL in your .env file are correct. These are needed for sending replies (Section 4).

  • API Key: Find in "API Keys" or "Developer Tools" in Infobip.
  • Base URL: Specific to your account (e.g., yoursubdomain.api.infobip.com), usually shown near the API key.

4. Adding Two-Way Reply Functionality

Let's make the application reply to incoming messages.

4.1 Instantiate Infobip SDK Client and Send Reply:

Update src/routes/infobipWebhook.js to use the SDK.

javascript
// src/routes/infobipWebhook.js
const config = require('../config/environment');
const { Infobip, AuthType } = require('@infobip-api/sdk'); // Import SDK

async function infobipWebhookRoutes(fastify, options) {
  // Instantiate Infobip client within the plugin scope
  const infobipClient = new Infobip({
    baseUrl: config.infobip.baseUrl,
    apiKey: config.infobip.apiKey,
    authType: AuthType.ApiKey, // Use API Key authentication
  });

  fastify.post('/infobip', async (request, reply) => {
    // ... (logging and security checks from Section 2.1) ...

    try {
      const results = request.body.results;
      // ... (payload validation from Section 2.1, including checking Infobip docs) ...

      for (const message of results) {
        // Verify field names based on Infobip docs/payload
        const messageId = message.messageId;
        const from = message.from; // Sender's number (recipient of our reply)
        const to = message.to;     // Your Infobip number (sender of our reply)
        const text = message.text;

        // ... (field validation) ...

        fastify.log.info(
          `Processing message ${messageId} from ${from}: ""${text}""`
        );

        // --- Send Automated Reply ---
        try {
          const replyText = `Thanks for your message: ""${text.substring(0, 50)}${text.length > 50 ? '...' : ''}""`;

          fastify.log.info(`Sending reply to ${from}`);

          // Use the SDK's send method
          const infobipResponse = await infobipClient.channels.sms.send({
            messages: [
              {
                destinations: [{ to: from }], // Send reply to the original sender
                // **IMPORTANT**: Specifying 'from' is highly recommended for consistency.
                // Use your Infobip number ('to' from the incoming message) or a registered
                // Alphanumeric Sender ID (e.g., 'MyBrand').
                // Alphanumeric Sender ID rules are country-specific. Check Infobip docs.
                from: to,
                text: replyText,
              },
            ],
          });

          // Log the result from Infobip API
          // **Note:** Verify `infobipResponse.data` is the correct path to response details
          // in the specific SDK version you are using. Log `infobipResponse` itself
          // during testing if unsure.
          fastify.log.info(
            { response: infobipResponse.data },
            'Infobip reply API response'
          );

        } catch (sendError) {
           fastify.log.error(
             { err: sendError, recipient: from },
             'Failed to send SMS reply via Infobip API'
           );
           // Decide if this failure should cause the webhook to return 500. Usually not.
        }
        // --- End Send Reply ---

      } // End for loop

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

    } catch (error) {
      // ... (general error handling from Section 2.1) ...
      reply.code(500).send({ error: 'Internal Server Error' });
    }
  });
}

module.exports = infobipWebhookRoutes;

Explanation:

  1. Import & Instantiate: Imports Infobip, AuthType and creates infobipClient.
  2. Send Reply Logic:
    • Calls infobipClient.channels.sms.send().
    • destinations: Set to the original sender (from).
    • from: Updated explanation: Emphasizes that specifying from (your Infobip number or Alphanumeric Sender ID) is highly recommended or required. Notes Alphanumeric Sender ID rules are country-specific and require checking docs.
    • text: Reply content.
  3. Response Logging: Logs infobipResponse.data. Adds a note advising verification of the data property based on the SDK version.
  4. Reply Error Handling: Specific try...catch for sending, logging errors but generally still returning 200 OK for the webhook receipt.

5. Error Handling and Logging

Robust error handling and clear logging are crucial for production systems.

Error Handling Strategy:

  • Webhook Processing: Wrap main logic in try...catch. Log errors (fastify.log.error). Return appropriate HTTP status codes (500, 400, 401).
  • Reply Sending: Wrap SDK calls in separate try...catch. Log send errors specifically. Usually return 200 to the webhook even if the reply fails, handling reply failures asynchronously if needed.
  • Fastify Errors: Utilize Fastify's built-in error handling and logging. Consider custom error handlers (fastify.setErrorHandler()) for advanced scenarios.

Logging:

  • Fastify Logger (Pino): Enabled (logger: true) for structured JSON logging.
  • Log Levels: Use info, warn, error, debug appropriately. Control level via environment variables in production.
  • Context: Include relevant data (messageId, recipient, error objects) in logs.
  • Log Analysis: Use centralized logging systems (Datadog, ELK, Splunk) in production.

Retry Mechanisms:

  • Webhook Retries (Infobip): Infobip retries on non-2xx responses. Your app should respond quickly and be idempotent (handle duplicate messages safely, e.g., by checking messageId against recent history).
  • Reply Retries (Your App): Implement custom retry logic (e.g., background job queue like BullMQ) for critical replies if the Infobip API call fails transiently.

6. Security Considerations

Securing your webhook endpoint is vital.

  • Webhook Secret / Authentication:
    • Highly Recommended: Use Infobip's security features (Basic Auth, Signature Validation - check their docs).
    • Implement validation in your route handler (Section 2.1). Store secrets securely (environment variables).
  • HTTPS: Always use HTTPS. ngrok provides this locally. Production requires TLS termination (hosting, load balancer).
  • Input Validation:
    • Validate webhook payload structure rigorously.
    • Sanitize/validate SMS text content if used in databases, commands, or displayed elsewhere to prevent injection attacks (XSS, SQLi).
  • Rate Limiting: Protect against DoS/abuse using @fastify/rate-limit.
    • Install: npm install @fastify/rate-limit
    • Register in server.js:
      javascript
      // src/server.js
      // ... other imports
      const rateLimit = require('@fastify/rate-limit');
      
      // ... inside start() or before registering routes if using async register
      await fastify.register(rateLimit, {
        max: 100, // Example: Max requests per window per IP
        timeWindow: '1 minute'
      });
      // ... register routes
    • Adjust limits appropriately.
  • Least Privilege: Use Infobip API keys with minimum required permissions.
  • Dependency Updates: Keep dependencies updated (npm audit).

7. Troubleshooting and Caveats

Common issues and things to watch out for:

  • Webhook Not Received: Check URL (Infobip vs. actual), server status, firewalls, ngrok status (temporary URLs!).
  • 401 Unauthorized: Mismatched secrets/credentials (Infobip config vs. .env/app validation).
  • 400 Bad Request: Payload structure mismatch (check Infobip docs/config vs. your parsing logic in Section 2.1). Log the received request.body.
  • 500 Internal Server Error: Check server logs (fastify.log.error) for exceptions.
  • Reply Sending Fails: Check API Key/Base URL, Infobip balance/permissions, 'from'/'to' numbers (validity, Sender ID rules), Infobip status page.
  • Infobip Free Trial Limitations: Often restricted to sending only to your verified number.
  • Idempotency: Handle potential webhook retries without causing duplicate actions.
  • Payload Variations: Ensure your handler matches the specific Infobip webhook event type/configuration.

8. Deployment Strategies

Moving from local development to production.

8.1 Obtain a Stable Public URL: Use cloud hosting (AWS, Google Cloud, Azure, Heroku, DigitalOcean, Render, Railway), load balancers, or API gateways for a permanent HTTPS endpoint.

8.2 Environment Configuration: Use secure environment variable management provided by your host (NOT .env files in production). Set NODE_ENV=production.

8.3 Process Management: Use pm2 or container orchestration (Docker, Kubernetes) for process management, restarts, and clustering (pm2 start src/server.js -i max).

8.4 Dockerization (Optional): Containerize for consistency.

  • Dockerfile:
    dockerfile
    # Dockerfile
    FROM node:18-alpine As base
    
    WORKDIR /app
    
    # Install dependencies only when package files change
    COPY package*.json ./
    RUN npm ci --only=production
    
    # Copy application code
    COPY . .
    
    # Expose the port the app runs on
    EXPOSE 3000
    
    # Define the command to run your app
    CMD [ ""node"", ""src/server.js"" ]
  • .dockerignore: Include .git, .env, node_modules/.
  • Build/Run: docker build, docker run (pass environment variables securely).

8.5 CI/CD Pipeline: Automate testing and deployment using GitHub Actions, GitLab CI, Jenkins, etc. (Lint, Test, Build, Deploy).

8.6 Rollback Procedures: Plan for reverting failed deployments (previous Docker image, Git tag).


9. Verification and Testing

Ensure your application works correctly.

9.1 Manual Verification:

  1. Run app (local ngrok or deployed).
  2. Configure Infobip webhook URL correctly.
  3. Send test SMS to Infobip number.
  4. Check app logs for receipt, processing, and reply logs.
  5. Check phone for reply SMS.
  6. Check Infobip portal logs for webhook/SMS status.

9.2 Automated Testing (Recommended):

Implement unit and integration tests.

  • Install Testing Tools: npm install --save-dev tap (or jest, supertest).

  • Unit Tests: Test functions in isolation.

  • Integration Tests (Webhook): Simulate webhook requests using Fastify's inject or supertest.

    • Note on Test Setup: The example below uses Fastify's inject. This typically works best if server.js is refactored to export an asynchronous build function that creates and returns the Fastify instance, rather than starting the server immediately. You would then import and call this build function in your test file. See Fastify's documentation on testing for details.
    javascript
    // test/webhook.test.js (Example structure)
    const { test } = require('tap');
    // Assuming server.js is refactored to export a build function:
    // const build = require('../src/app'); // Adjust path if needed
    
    test('POST /webhook/infobip should process valid message', async (t) => {
      // const app = await build(); // Build app instance for testing
      // t.teardown(() => app.close()); // Close app after test
    
      // --- If NOT refactoring server.js, testing becomes more complex ---
      // You might need to require('../src/server.js') which starts the server,
      // then find a way to make requests to it (less ideal).
      // OR mock the Fastify instance and route registration.
      // Refactoring for testability is recommended.
      t.comment('Test assumes server.js refactored to export a build function');
      t.end(); // Placeholder end for demonstration
      return; // Skip actual execution in this example
    
      /*
      // Example test logic (if app instance 'app' is available)
      const mockPayload = {
        results: [ { /* ... valid message data ... */ } ],
        // ... other payload fields
      };
    
      const response = await app.inject({
        method: 'POST',
        url: '/webhook/infobip',
        payload: mockPayload,
        // headers: { 'x-webhook-secret': 'your-test-secret' } // If testing secret
      });
    
      t.equal(response.statusCode, 200, 'should return status 200');
      t.same(JSON.parse(response.payload), { status: 'received' }, 'should return correct payload');
      // Add more assertions: check logs (requires logger injection/mocking), check mocks
      */
    });
    
    // Add more tests for invalid payloads, security, errors, etc.
  • Test Coverage: Use tools like c8 or Istanbul (nyc) to measure coverage.

Verification Checklist:

  • Dependencies installed.
  • Environment variables configured.
  • Server starts.
  • Webhook URL configured in Infobip.
  • Security mechanism validated (if used).
  • App publicly accessible.
  • SMS triggers webhook.
  • Logs show successful processing.
  • 200 OK sent to Infobip.
  • Reply received (if implemented).
  • Infobip logs confirm delivery/sending.
  • Invalid requests rejected correctly (400, 401).
  • Errors logged and handled.
  • Automated tests pass (if implemented).

Complete Code Repository

A complete, working example of the code described in this guide can be found on GitHub.

This repository includes the project structure, package.json, example .env.example, .gitignore, and the source code files.


This guide provides a solid foundation for building a production-ready two-way SMS system using Fastify and Infobip. Remember to consult the official Infobip API documentation for the most up-to-date details on payload formats, authentication methods, and API capabilities. Happy coding!

Frequently Asked Questions

How to set up two-way SMS with Fastify?

Set up a Fastify server to receive incoming SMS messages via Infobip webhooks and send replies using the Infobip API. This involves configuring routes, installing necessary dependencies like `fastify`, `@infobip-api/sdk`, and `dotenv`, and setting up environment variables for your Infobip credentials and server configuration. The server will listen for incoming messages and trigger actions or automated replies.

What is the Infobip webhook URL for?

The Infobip webhook URL is the endpoint on your Fastify server that Infobip will send incoming SMS message data to. It's crucial to configure this correctly in your Infobip account portal and ensure your server is publicly accessible. Using a tool like ngrok during development can expose your local server temporarily for testing.

How to use Infobip API with Fastify?

Integrate the Infobip Node.js SDK (`@infobip-api/sdk`) into your Fastify application to send automated replies to incoming SMS messages. Instantiate the Infobip client with your API key and base URL, then use the `channels.sms.send()` method to send replies via the Infobip API. Make sure to handle potential errors during the API call.

Why does two-way SMS need webhooks?

Webhooks provide a way for Infobip to push real-time notifications to your application whenever an SMS message is received. This eliminates the need for constant polling and enables immediate responses, making it ideal for implementing two-way communication. The webhook acts as a listener for incoming messages.

How to secure Infobip webhook endpoint?

Secure your webhook endpoint by implementing a validation mechanism, such as checking a secret header or utilizing Basic Authentication if Infobip supports it. Consult the Infobip documentation for their specific security recommendations. Additionally, always use HTTPS for all communication and implement input validation to prevent attacks.

What is Fastify used for in two-way SMS?

Fastify serves as the lightweight, high-performance web framework for building your Node.js application. It handles incoming webhook requests from Infobip containing SMS messages and enables routing, logging, and processing of the data. It also manages outgoing reply requests to the Infobip API via the SDK.

How to send automated SMS replies with Infobip?

After receiving a webhook request, use the Infobip Node.js SDK to send automated SMS replies. Extract the sender's phone number from the message data and use `infobipClient.channels.sms.send()` to send a reply. Ensure the 'from' parameter is set to your Infobip number or a registered Alphanumeric Sender ID for deliverability.

What is the role of Node.js in this system?

Node.js provides the JavaScript runtime environment for the entire two-way SMS application. It allows you to execute JavaScript code on the server-side, handling the webhook requests, interacting with the Infobip API, and managing the logic for automated replies.

When should I add rate limiting to my SMS app?

Add rate limiting using `@fastify/rate-limit` as a security measure in production to protect your application from denial-of-service (DoS) attacks or abuse. Configure limits appropriate to your expected traffic, and adjust as needed. Rate limiting helps prevent overload by restricting excessive requests.

How to troubleshoot Infobip webhook not working?

If your Infobip webhook isn't working, double-check the webhook URL configured in your Infobip account matches your server's public URL. Verify your server is running and publicly accessible, check firewall settings, and ensure `ngrok` is still active if using it for local development. Inspect logs for errors.

How to handle Infobip webhook retries?

Infobip might retry sending webhooks if your server returns non-2xx HTTP status codes. Ensure your application is idempotent, meaning it can handle duplicate webhook requests without causing unintended side effects. Check message IDs to avoid processing the same message twice.

Can I use Docker for deploying my Fastify SMS app?

Yes, you can containerize your application with Docker for consistent deployments. Create a Dockerfile that includes the necessary instructions to build and run your Fastify application. Use `docker build` to create an image, then use `docker run` to launch the container. This also simplifies scaling with Kubernetes or other container orchestrators.

How to expose local Fastify server for Infobip webhook?

Use a tool like `ngrok` to temporarily create a public URL that tunnels to your local server during development. This allows Infobip to send webhook requests to your local application. Download, install, and authenticate `ngrok`, then run `ngrok http <your_port>` to create the tunnel.

What are environment variables in this project?

Environment variables store sensitive information like your Infobip API key, base URL, and webhook secret. These are loaded from a `.env` file during development using the `dotenv` package. In production, use secure environment management provided by your hosting platform. Never commit `.env` to version control.