sms compliance

Sent logo
Sent TeamMar 8, 2026 / sms compliance / nodejs

Build a Node.js Fastify SMS Marketing Campaign Service with Infobip

Complete guide to building a production-ready Node.js application using Fastify to send marketing SMS messages via the Infobip API with error handling, security, and best practices.

Build a Node.js Fastify SMS Marketing Campaign Service with Infobip

Build a production-ready Node.js application using Fastify to send marketing SMS messages via the Infobip API. This guide covers project setup, core implementation, API definition, Infobip integration, error handling, security, testing, and deployment.

Project Overview and Goals

This guide shows you how to build a backend service that sends targeted SMS messages for marketing campaigns. While Infobip offers robust features for managing campaigns (like 10DLC registration), this guide focuses on the sending mechanism via their API, integrated into a scalable Fastify application.

Problem Solved: Build a structured, performant, and maintainable way to programmatically send SMS messages through Infobip from a Node.js backend for promotional alerts, notifications, or other campaign-related communications.

Technologies Used:

  • Node.js: JavaScript runtime environment chosen for its asynchronous nature, large ecosystem (npm), and suitability for I/O-bound tasks like API interactions.
  • Fastify: High-performance, low-overhead web framework for Node.js chosen for its speed, extensive plugin architecture, developer experience, and built-in schema validation.
  • Infobip Node.js SDK: Official library for interacting with the Infobip API, simplifying authentication, request formatting, and response handling compared to raw HTTP calls.
  • dotenv: Module to load environment variables from a .env file into process.env, essential for managing configuration and secrets securely.
  • pino-pretty: Development dependency that makes Fastify's default JSON logs human-readable during development.

System Architecture:

text
+-----------------+      +-----------------------+      +----------------+
|   Client (e.g., |----->|  Fastify API Server   |----->|  Infobip API   |
|  Web App, CLI)  |      |  (Node.js)            |      |  (SMS Sending) |
+-----------------+      |                       |      +----------------+
                         | - Routes (/send-sms)  |
                         | - Controllers         |
                         | - Services (Infobip)  |
                         | - Config (.env)       |
                         | - Logging (Pino)      |
                         +-----------------------+

Expected Outcome: A functional Fastify API endpoint (/send-sms) that accepts a destination phone number and message content, sends the SMS via Infobip, and returns the status.

Prerequisites:

  • An Infobip account (free or paid) – https://www.infobip.com/
  • Your Infobip API Key and Base URL
  • Node.js (LTS version recommended) and npm installed – https://nodejs.org/
  • Basic understanding of JavaScript, Node.js, REST APIs, and terminal commands
  • (Optional but Recommended for US Traffic) An approved Brand and Campaign registered within Infobip for 10DLC compliance. This guide focuses on sending; register campaigns separately via the Infobip portal or Number Registration API. See Infobip 10DLC Campaigns Documentation.

1. Setting up the Project

Initialize your Node.js project using Fastify.

  1. Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.

    bash
    mkdir fastify-infobip-sms
    cd fastify-infobip-sms
  2. Initialize Node.js Project: Create a package.json file.

    bash
    npm init -y
  3. Install Dependencies: Install Fastify, the Infobip SDK, and dotenv. Add pino-pretty as a development dependency for readable logs.

    bash
    npm install fastify @infobip-api/sdk dotenv
    npm install --save-dev pino-pretty
  4. Create Project Structure: Organize the project for clarity and maintainability.

    bash
    mkdir src
    mkdir src/routes
    mkdir src/controllers
    mkdir src/services
    mkdir src/config
    • src/: Contains all source code.
    • src/routes/: Defines API routes.
    • src/controllers/: Handles request logic and calls services.
    • src/services/: Encapsulates business logic, like interacting with Infobip.
    • src/config/: Holds configuration-related files.
  5. Configure Environment Variables: Create a .env file in the project root to store sensitive information like API keys. Never commit this file to version control. Create a .env.example file to show necessary variables (commit this one).

    .env.example:

    ini
    # Infobip API Credentials
    INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
    INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL
    
    # Application Settings
    PORT=3000
    LOG_LEVEL=info
    # Optional: Sender ID for SMS (e.g., 'InfoSMS', requires setup in Infobip)
    # INFOBIP_SENDER_ID=YourSenderID

    .env:

    ini
    # Infobip API Credentials
    INFOBIP_API_KEY=paste_your_actual_api_key_here
    INFOBIP_BASE_URL=paste_your_actual_base_url_here
    
    # Application Settings
    PORT=3000
    LOG_LEVEL=info
    # INFOBIP_SENDER_ID=OptionalSender

    Why: Using .env separates configuration from code, enhancing security and making it easy to manage different environments (development, staging, production).

  6. Create .gitignore: Ensure sensitive files and generated directories are not tracked by Git.

    .gitignore:

    text
    node_modules/
    .env
    npm-debug.log
    *.log
    coverage/
    dist/
  7. Basic Server Setup: Create the main server file.

    server.js (in project root):

    javascript
    // Load environment variables early
    require('dotenv').config();
    
    // Require the framework and instantiate it
    const fastify = require('fastify')({
      logger: {
        level: process.env.LOG_LEVEL || 'info',
        // Use pino-pretty only in development for readability
        transport: process.env.NODE_ENV !== 'production'
          ? { target: 'pino-pretty' }
          : undefined,
      },
    });
    
    // Register routes
    fastify.register(require('./src/routes/smsRoutes'));
    
    // Health check route
    fastify.get('/health', async (request, reply) => {
      return { status: 'ok', timestamp: new Date().toISOString() };
    });
    
    // Run the server!
    const start = async () => {
      try {
        const port = process.env.PORT || 3000;
        await fastify.listen({ port: parseInt(port, 10), host: '0.0.0.0' });
        fastify.log.info(`Server listening on port ${port}`);
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    start();

    Why: This sets up a Fastify server, configures logging (using pino-pretty in development), registers routes, adds a health check endpoint, and starts listening for requests.

  8. Add Run Scripts to package.json:

    json
    {
      "scripts": {
        "start": "node server.js",
        "dev": "node --watch server.js | pino-pretty",
        "test": "echo \"Error: no test specified yet\" && exit 1"
      }
    }
    • npm start: Runs the server normally.
    • npm run dev: Runs the server using Node's built-in watch mode (requires Node.js 18.11+) for auto-restarts on file changes and pipes logs through pino-pretty.

Run npm run dev and access http://localhost:3000/health in your browser to verify the server is running.

2. Implementing Core Functionality (SMS Service)

Create a service to handle the interaction with the Infobip SDK.

  1. Create Infobip Service: This service encapsulates the logic for sending SMS messages.

    src/services/infobipService.js:

    javascript
    const { Infobip, AuthType } = require('@infobip-api/sdk');
    
    // Ensure required environment variables are present
    if (!process.env.INFOBIP_API_KEY || !process.env.INFOBIP_BASE_URL) {
      // Log this critical error at the application start or use a dedicated config validation step
      console.error('CRITICAL: Missing Infobip API Key or Base URL in environment variables.');
      throw new Error('Infobip API Key and Base URL are required.');
    }
    
    // Initialize Infobip client instance
    const infobipClient = new Infobip({
      baseUrl: process.env.INFOBIP_BASE_URL,
      apiKey: process.env.INFOBIP_API_KEY,
      authType: AuthType.ApiKey, // Using API Key for authentication
    });
    
    /**
     * Sends a single SMS message using the Infobip API.
     * @param {string} destinationNumber - The recipient's phone number in international format (e.g., 447123456789).
     * @param {string} messageText - The text content of the SMS.
     * @param {string} [senderId] - Optional sender ID (must be pre-configured in Infobip). Defaults to process.env.INFOBIP_SENDER_ID.
     * @returns {Promise<object>} - The response object from the Infobip API.
     * @throws {Error} - Throws an error if the API call fails, potentially including details from Infobip.
     */
    const sendSingleSms = async (destinationNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => {
      // Logging for this action will be handled by the controller calling this service
    
      const payload = {
        messages: [
          {
            destinations: [{ to: destinationNumber }],
            text: messageText,
            // Only include 'from' if senderId is provided
            ...(senderId && { from: senderId }),
          },
        ],
      };
    
      try {
        // Use the Infobip SDK to send the SMS
        const response = await infobipClient.channels.sms.send(payload);
        // Success logging handled by the controller
        return response.data; // Return the data part of the response
      } catch (error) {
        // Error logging handled by the controller, re-throw to propagate
        // Consider wrapping the error for more context if needed
        // e.g., throw new Error(`Infobip API Error: ${error.message}`);
        throw error; // Re-throw the original error (or a wrapped one)
      }
    };
    
    module.exports = {
      sendSingleSms,
      // Add other Infobip-related functions here (e.g., sendBulkSms, checkStatus)
    };

    Why: Encapsulating external API interactions within a dedicated service makes the code modular, easier to test (by mocking the service), and isolates Infobip-specific logic from the controllers. Logging related to specific requests is handled in the controller, which has request context.

3. Building the API Layer

Create the Fastify route and controller to expose the SMS sending functionality.

  1. Create SMS Controller: This handles the incoming HTTP request, validates the input, calls the service, and sends the response.

    src/controllers/smsController.js:

    javascript
    const infobipService = require('../services/infobipService');
    
    /**
     * Handles the request to send an SMS message.
     * @param {import('fastify').FastifyRequest} request - The Fastify request object, includes request.log.
     * @param {import('fastify').FastifyReply} reply - The Fastify reply object.
     */
    const sendSmsHandler = async (request, reply) => {
      const { destinationNumber, message } = request.body;
    
      request.log.info(`Received request to send SMS to ${destinationNumber}`);
    
      try {
        const result = await infobipService.sendSingleSms(destinationNumber, message);
    
        // Check Infobip response structure for success indication
        // NOTE: Check Infobip docs for the exact fields/values indicating successful acceptance vs. immediate rejection.
        // The structure and 'groupName' might vary.
        const messageStatus = result.messages?.[0]?.status;
        if (messageStatus && messageStatus.groupName === 'PENDING') {
           request.log.info(`SMS for ${destinationNumber} accepted by Infobip (status: PENDING): Message ID ${result.messages?.[0]?.messageId}`);
           reply.code(202).send({
             message: 'SMS accepted for delivery.',
             infobipResponse: result,
           });
        } else {
           // Handle cases where the message might be processed but not in a clear 'PENDING' state, or rejected immediately.
           request.log.warn({infobipResponse: result}, `SMS for ${destinationNumber} processed by Infobip, but status is not PENDING (or status missing). Check response.`);
           reply.code(200).send({
             message: 'SMS processed by Infobip, check response status details.',
             infobipResponse: result,
           });
        }
    
      } catch (error) {
        request.log.error({ err: error, destination: destinationNumber }, `Failed to process send SMS request: ${error.message}`);
        // Log Infobip specific details if available
        if (error.response?.data) {
           request.log.error({ infobipError: error.response.data }, 'Infobip API error details');
        }
        // Send a generic error response to the client
        reply.code(500).send({
          error: 'Internal Server Error',
          message: 'Failed to send SMS due to an internal issue.',
          // Optionally include more detail in non-production environments
          details: process.env.NODE_ENV !== 'production' ? error.message : undefined,
        });
      }
    };
    
    module.exports = {
      sendSmsHandler,
    };

    Why: The controller bridges the HTTP layer and the service layer. It extracts data from the request, uses the request-specific logger (request.log), invokes the service function, and formats the HTTP response based on the outcome (success or error).

  2. Define API Route and Schema: Configure the Fastify route, including input validation using JSON Schema.

    src/routes/smsRoutes.js:

    javascript
    const smsController = require('../controllers/smsController');
    
    // Define the schema for the request body validation
    const sendSmsSchema = {
      body: {
        type: 'object',
        required: ['destinationNumber', 'message'],
        properties: {
          destinationNumber: {
            type: 'string',
            description: 'Recipient phone number in international E.164 format (e.g., +447123456789)',
            // Example pattern - ensures starts with optional +, then digits, covering international format.
            pattern: '^\\+?[1-9]\\d{1,14}$'
          },
          message: {
            type: 'string',
            description: 'The text content of the SMS message',
            minLength: 1,
            maxLength: 1600 // Example max length, check Infobip limits
          },
        },
        additionalProperties: false, // Disallow extra properties in the request body
      },
      response: {
        // Define expected success response schemas
        200: { // Processed, check status
          description: 'SMS processed by Infobip, check response for detailed status.',
          type: 'object',
          properties: {
            message: { type: 'string' },
            infobipResponse: { type: 'object' }, // Be more specific based on actual API response if needed
          },
        },
        202: { // Accepted for delivery
          description: 'SMS accepted by Infobip for delivery.',
          type: 'object',
          properties: {
            message: { type: 'string' },
            infobipResponse: { type: 'object' },
          },
        },
        // Define error response schemas (Fastify handles 500 generically, but you can customize)
        500: {
           description: 'Internal server error occurred.',
           type: 'object',
           properties: {
               error: { type: 'string', example: 'Internal Server Error' },
               message: { type: 'string' },
               details: { type: 'string', description: 'Error details (only in non-production)' }
           }
        }
        // Add 4xx schemas here too (e.g., 400 for validation errors - Fastify generates this, but you can document it)
      },
    };
    
    /**
     * Encapsulates the routes for SMS functionality.
     * @param {import('fastify').FastifyInstance} fastify - The Fastify instance.
     * @param {object} options - Plugin options.
     * @param {function} done - Callback function to signal plugin readiness.
     */
    async function smsRoutes(fastify, options) {
      fastify.post('/send-sms', { schema: sendSmsSchema }, smsController.sendSmsHandler);
    
      // Add other SMS-related routes here (e.g., /sms/status/:messageId)
    }
    
    module.exports = smsRoutes;

    Why: Fastify's built-in schema validation (sendSmsSchema) automatically checks if the incoming request body matches the defined structure and format. This prevents invalid data from reaching the controller and provides automatic 400 Bad Request responses for invalid inputs.

  3. Testing the Endpoint: Run the server (npm run dev). Test the endpoint using curl or a tool like Postman.

    curl Example:

    bash
    curl -X POST http://localhost:3000/send-sms \
    -H "Content-Type: application/json" \
    -d '{
      "destinationNumber": "+12345678900",
      "message": "Hello from Fastify and Infobip!"
    }'

    Replace +12345678900 with a valid number (your own if using Infobip free trial).

    Expected Successful Response (Status Code 202 Accepted - if Infobip returns PENDING):

    json
    {
      "message": "SMS accepted for delivery.",
      "infobipResponse": {
        "bulkId": "some-bulk-id",
        "messages": [
          {
            "to": "+12345678900",
            "status": {
              "groupId": 1,
              "groupName": "PENDING",
              "id": 26,
              "name": "PENDING_ACCEPTED",
              "description": "Message accepted"
            },
            "messageId": "some-unique-message-id"
          }
        ]
      }
    }

    Example Error Response (e.g., Invalid Phone Number Format - Status Code 400 Bad Request):

    json
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "body/destinationNumber must match pattern \"^\\\\+?[1-9]\\\\d{1,14}$\""
    }

4. Integrating with Infobip (Deep Dive)

We've already initialized the SDK, but let's detail the configuration steps.

  1. Obtain Infobip Credentials:

    • Log in to your Infobip account (https://portal.infobip.com/).
    • API Key: Navigate to Apps > API Keys. Create a new API key if you don't have one. Copy the key value securely. Treat this like a password.
    • Base URL: Your Base URL is specific to your account. You can usually find it on the Infobip portal homepage after logging in, or within the API documentation examples tailored to your account. It looks something like xxxxx.api.infobip.com.
    • (Optional) Sender ID: To use a custom sender name (like your brand name instead of a number), you need to configure and potentially register it within Infobip under Channels and Numbers > Sender Names. Regulations vary by country.
  2. Secure Storage:

    • As done in Step 1.5, store INFOBIP_API_KEY and INFOBIP_BASE_URL in your .env file for local development.
    • Ensure .env is listed in your .gitignore file.
    • In production environments, use secure secret management solutions provided by your cloud provider (e.g., AWS Secrets Manager, Azure Key Vault, GCP Secret Manager) or tools like HashiCorp Vault instead of .env files. Load these secrets into environment variables during deployment.
  3. SDK Initialization (Recap): The code in src/services/infobipService.js handles this:

    javascript
    const { Infobip, AuthType } = require('@infobip-api/sdk');
    // ... check for env vars ...
    const infobipClient = new Infobip({
      baseUrl: process.env.INFOBIP_BASE_URL,
      apiKey: process.env.INFOBIP_API_KEY,
      authType: AuthType.ApiKey,
    });
    • baseUrl: Tells the SDK which Infobip API endpoint to communicate with.
    • apiKey: The credential used for authentication.
    • authType: AuthType.ApiKey: Specifies the authentication method.
  4. Fallback Mechanisms (Conceptual): While the SDK handles basic retries for network issues, robust applications might need more:

    • Retry Logic: For transient errors (like temporary network issues or Infobip rate limits), implement application-level retries with exponential backoff using libraries like async-retry. Wrap the infobipService.sendSingleSms call within the controller or add retry logic inside the service method itself (see Section 5).
    • Circuit Breaker: Use a library like opossum to implement a circuit breaker pattern. If Infobip API calls consistently fail, the circuit breaker ""opens,"" preventing further calls for a period, reducing load on both systems.
    • Alternative Provider (Advanced): For critical notifications, you might configure a secondary SMS provider and switch to it if Infobip experiences a prolonged outage (requires significant architectural planning).

5. Error Handling, Logging, and Retry Mechanisms

Building on the basics.

  1. Consistent Error Handling:

    • Service Layer: Catches errors from the SDK in the service (infobipService.js). Re-throws the error (potentially wrapped) to be handled by the controller.
    • Controller Layer: Catches errors propagated from the service in the controller (smsController.js). Uses request.log to log detailed errors, including context like the request body (sanitized if necessary) and any error details from Infobip. Sends an appropriate HTTP status code (e.g., 500 for server errors, 503 for temporary unavailability if using retries/circuit breaker) and a generic error message to the client, avoiding leaking internal details in production.
    • Fastify Hooks: Use Fastify's setErrorHandler hook for centralized formatting of error responses before they are sent to the client, ensuring consistency.
  2. Logging:

    • Levels: Use different log levels (info, warn, error, debug). Set LOG_LEVEL via environment variable (info for production, debug for development).
    • Context: Fastify's logger automatically includes request details when using request.log. Log key events: incoming requests (Fastify does this), interactions with external services (Infobip calls - logged in controller), successful operations (SMS accepted - logged in controller), and errors (with stack traces and context - logged in controller).
    • Format: Use JSON format for logs in production (Fastify's default) for easier parsing by log aggregation tools (like Datadog, Splunk, ELK stack). Use pino-pretty only during development.
    • Correlation IDs: Implement request/correlation IDs (e.g., using fastify-request-id or relying on Fastify's built-in reqId) to trace a single request through logs across different services or operations. request.log automatically includes this.
  3. Retry Strategy (Example using async-retry in the Service):

    • Install: npm install async-retry
    • Modify src/services/infobipService.js:
      javascript
      const retry = require('async-retry');
      const { Infobip, AuthType } = require('@infobip-api/sdk');
      // ... env var checks and client init ...
      
      const sendSingleSms = async (destinationNumber, messageText, senderId = process.env.INFOBIP_SENDER_ID) => {
        const payload = { /* ... payload definition ... */ };
      
        // Wrap the SDK call in retry logic
        return retry(async (bail, attempt) => {
          // Note: Logging within the retry loop might be verbose.
          // Consider logging only on failure or final success.
          // The controller will log the overall attempt and final outcome.
          try {
            console.log(`[Attempt ${attempt}] Calling Infobip API for ${destinationNumber}`); // Use console or a passed-in logger
            const response = await infobipClient.channels.sms.send(payload);
            return response.data; // Return data on success
          } catch (error) {
            console.warn(`[Attempt ${attempt}] Error sending SMS via Infobip: ${error.message}`);
      
            // Decide if the error is retryable
            // Example: Don't retry on 4xx client errors (bad request, auth failed)
            // Check Infobip's specific error codes for retryable conditions (e.g., rate limits 429, server errors 5xx)
            if (error.response && error.response.status >= 400 && error.response.status < 500 && error.response.status !== 429) {
               console.error(`Non-retryable client error (${error.response.status}) from Infobip. Bailing out.`);
               bail(new Error(`Infobip client error (${error.response.status}): ${error.message}`)); // Stop retrying
               return; // Required after bail
            }
      
            // For potentially retryable errors (5xx, 429, network issues), throw to trigger retry
            console.warn(`Retryable error encountered (status: ${error.response?.status}). Retrying...`);
            throw error;
          }
        }, {
          retries: 3, // Number of retries
          factor: 2, // Exponential backoff factor
          minTimeout: 1000, // Initial delay ms
          maxTimeout: 5000, // Max delay ms
          onRetry: (error, attempt) => {
            console.warn(`Retrying Infobip request (Attempt ${attempt}) due to error: ${error.message}`);
          }
        });
      };
      
      module.exports = { sendSingleSms };
    • Note on Logging: Using console.log inside the service is shown for simplicity here. In a real application, you'd ideally pass the request.log instance (or a generic logger instance) into the service function if you need detailed logging within the service or retry loop itself.
    • Testing Errors: Mock the infobipClient.channels.sms.send method in tests to throw specific errors (network errors, 4xx errors, 5xx errors, 429 errors) and verify the retry logic and error handling paths work as expected.

6. Database Schema and Data Layer (Conceptual)

For a real marketing campaign system, you'd need a database. While this guide focuses on the sending mechanism, here's a conceptual overview:

  • Purpose: Store user lists/segments, campaign details, message templates, individual message statuses (mapping your internal ID to Infobip's messageId), opt-out information, etc.
  • Technology: MongoDB, PostgreSQL, MySQL, or similar. Choose based on your data structure needs and team familiarity.
  • Schema (Example - MongoDB Collections):
    • users: { _id, phoneNumber, firstName, lastName, tags: [], subscribed: true, createdAt, updatedAt }
    • campaigns: { _id, name, description, targetAudience: { tags: [] }, messageTemplate: """", status: ""draft|active|archived"", createdAt, updatedAt }
    • messages: { _id, userId, campaignId, infobipMessageId, infobipBulkId, status: ""pending|accepted|sent|delivered|failed|rejected|undeliverable"", sentAt, statusUpdatedAt, cost } (Status values aligned with Infobip Delivery Reports)
    • opt_outs: { _id, phoneNumber, reason, optedOutAt }
  • Data Layer: Implement repositories or data access objects (DAOs) to interact with the database (e.g., using Mongoose for MongoDB or an ORM like Prisma/Sequelize for SQL). Keep database logic separate from services. Your Fastify application would interact with this layer.
  • Migrations: Use tools like migrate-mongo (MongoDB) or the ORM's built-in migration tools (Prisma Migrate, Sequelize CLI) to manage schema changes over time.

Note: Implementing the database layer is beyond the scope of this specific sending guide but crucial for a complete campaign system.

7. Security Features

Security is paramount, especially when handling user data and API keys.

  1. Input Validation:

    • Fastify Schemas: Already implemented (sendSmsSchema). Ensures only expected data types and formats are accepted. Crucial against injection attacks and basic data corruption.
    • Sanitization: While validation helps, consider sanitizing message content if it includes user-generated input to prevent potential issues, although direct XSS is less likely via SMS itself. Basic checks or escaping might be relevant depending on how messages are constructed.
  2. Secrets Management:

    • Use environment variables (.env locally, secure secret stores in production).
    • Never hardcode API keys or sensitive data in source code. Ensure .gitignore prevents committing .env.
  3. Rate Limiting:

    • Protect your API from abuse and ensure fair usage. Use @fastify/rate-limit.
    • Install: npm install @fastify/rate-limit
    • Register in server.js (before your routes):
      javascript
      // In server.js, after fastify instantiation
      fastify.register(require('@fastify/rate-limit'), {
        max: 100, // Max requests per window per IP (default)
        timeWindow: '1 minute'
        // Consider more sophisticated keying (e.g., by API key if authenticated)
      });
    • Adjust max and timeWindow based on expected load and Infobip's own rate limits.
  4. Authentication/Authorization (If applicable):

    • If this API endpoint should not be public, protect it. Use strategies like:
      • API Keys: Implement a check for a custom X-API-Key header using a Fastify preHandler hook or @fastify/auth.
      • JWT: If users log in elsewhere, validate JWTs using @fastify/jwt.
      • OAuth: For third-party integrations, use @fastify/oauth2.
  5. Other Considerations:

    • Helmet: Use @fastify/helmet to set various security-related HTTP headers. npm install @fastify/helmet, then fastify.register(require('@fastify/helmet')) in server.js.
    • Dependency Updates: Regularly update dependencies (npm audit fix, use tools like Dependabot/Snyk) to patch known vulnerabilities.
    • HTTPS: Ensure your service runs over HTTPS in production (usually handled by a load balancer, reverse proxy like Nginx/Caddy, or PaaS).

8. Handling Special Cases

Real-world SMS sending involves nuances:

  • Sender ID (from field): Regulations vary globally. Some countries require pre-registration (Alphanumeric Sender ID), others override it with a local number (e.g., shared short code, long code). Use the optional senderId parameter in sendSingleSms and configure it correctly in Infobip. Ensure compliance with local laws (check Infobip docs for country specifics).
  • Character Sets and Encoding: Standard SMS (GSM 03.38) supports 160 characters. Using non-standard characters (like emojis or certain accented letters) switches to UCS-2 encoding, reducing the limit to 70 characters per SMS segment. Long messages are split into multiple segments. Be mindful of message length and character usage to manage costs and ensure readability. The Infobip API/SDK typically handles encoding automatically, but awareness is important.
  • Concatenated Messages: Long messages are automatically split and reassembled by the receiving device. Infobip handles this segmentation. Be aware of the per-segment cost.
  • Delivery Reports (DLRs): Infobip can send status updates (delivered, failed, etc.) back to your application via webhooks. Set up a separate endpoint in your Fastify app to receive these DLRs, parse them, and update the status of the corresponding message in your database (requires storing the messageId from the initial send response). This is crucial for tracking campaign effectiveness and handling failures. Implementing DLR handling is a common next step after basic sending.
  • Opt-Out Handling (STOP Keywords): Regulations (like TCPA in the US) require handling opt-out requests (e.g., replying STOP). Infobip can often manage standard keyword responses automatically if configured. You also need to ensure your application respects opt-outs stored in your database and doesn't send messages to unsubscribed numbers.
  • 10DLC Compliance (US): Sending Application-to-Person (A2P) SMS to US numbers using standard 10-digit long codes requires registering your Brand and specific Campaigns with The Campaign Registry (TCR) via providers like Infobip. This involves defining the message use case (e.g., marketing, notifications). Sending without registration risks filtering or blocking. This guide assumes registration is handled separately via Infobip's portal or APIs.
  • Rate Limits (Infobip Side): Infobip imposes rate limits on API calls and message sending throughput (messages per second), which can vary based on your account, destination country, and sender type (e.g., Toll-Free, Short Code, 10DLC). Implement rate limiting (@fastify/rate-limit) and potentially retry logic (Section 5) in your application to avoid exceeding these limits and handle 429 Too Many Requests errors gracefully.