sms compliance
sms compliance
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
.envfile intoprocess.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:
+-----------------+ +-----------------------+ +----------------+
| 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.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir fastify-infobip-sms cd fastify-infobip-sms -
Initialize Node.js Project: Create a
package.jsonfile.bashnpm init -y -
Install Dependencies: Install Fastify, the Infobip SDK, and
dotenv. Addpino-prettyas a development dependency for readable logs.bashnpm install fastify @infobip-api/sdk dotenv npm install --save-dev pino-pretty -
Create Project Structure: Organize the project for clarity and maintainability.
bashmkdir src mkdir src/routes mkdir src/controllers mkdir src/services mkdir src/configsrc/: 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.
-
Configure Environment Variables: Create a
.envfile in the project root to store sensitive information like API keys. Never commit this file to version control. Create a.env.examplefile 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=OptionalSenderWhy: Using
.envseparates configuration from code, enhancing security and making it easy to manage different environments (development, staging, production). -
Create
.gitignore: Ensure sensitive files and generated directories are not tracked by Git..gitignore:textnode_modules/ .env npm-debug.log *.log coverage/ dist/ -
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-prettyin development), registers routes, adds a health check endpoint, and starts listening for requests. -
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 throughpino-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.
-
Create Infobip Service: This service encapsulates the logic for sending SMS messages.
src/services/infobipService.js:javascriptconst { 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.
-
Create SMS Controller: This handles the incoming HTTP request, validates the input, calls the service, and sends the response.
src/controllers/smsController.js:javascriptconst 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). -
Define API Route and Schema: Configure the Fastify route, including input validation using JSON Schema.
src/routes/smsRoutes.js:javascriptconst 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. -
Testing the Endpoint: Run the server (
npm run dev). Test the endpoint usingcurlor a tool like Postman.curlExample:bashcurl -X POST http://localhost:3000/send-sms \ -H "Content-Type: application/json" \ -d '{ "destinationNumber": "+12345678900", "message": "Hello from Fastify and Infobip!" }'Replace
+12345678900with 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.
-
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.
-
Secure Storage:
- As done in Step 1.5, store
INFOBIP_API_KEYandINFOBIP_BASE_URLin your.envfile for local development. - Ensure
.envis listed in your.gitignorefile. - 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
.envfiles. Load these secrets into environment variables during deployment.
- As done in Step 1.5, store
-
SDK Initialization (Recap): The code in
src/services/infobipService.jshandles this:javascriptconst { 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.
-
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 theinfobipService.sendSingleSmscall within the controller or add retry logic inside the service method itself (see Section 5). - Circuit Breaker: Use a library like
opossumto 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).
- Retry Logic: For transient errors (like temporary network issues or Infobip rate limits), implement application-level retries with exponential backoff using libraries like
5. Error Handling, Logging, and Retry Mechanisms
Building on the basics.
-
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). Usesrequest.logto 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
setErrorHandlerhook for centralized formatting of error responses before they are sent to the client, ensuring consistency.
- Service Layer: Catches errors from the SDK in the service (
-
Logging:
- Levels: Use different log levels (
info,warn,error,debug). SetLOG_LEVELvia environment variable (infofor production,debugfor 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-prettyonly during development. - Correlation IDs: Implement request/correlation IDs (e.g., using
fastify-request-idor relying on Fastify's built-inreqId) to trace a single request through logs across different services or operations.request.logautomatically includes this.
- Levels: Use different log levels (
-
Retry Strategy (Example using
async-retryin the Service):- Install:
npm install async-retry - Modify
src/services/infobipService.js:javascriptconst 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.loginside the service is shown for simplicity here. In a real application, you'd ideally pass therequest.loginstance (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.sendmethod 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.
- Install:
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.
-
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.
- Fastify Schemas: Already implemented (
-
Secrets Management:
- Use environment variables (
.envlocally, secure secret stores in production). - Never hardcode API keys or sensitive data in source code. Ensure
.gitignoreprevents committing.env.
- Use environment variables (
-
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
maxandtimeWindowbased on expected load and Infobip's own rate limits.
- Protect your API from abuse and ensure fair usage. Use
-
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-Keyheader using a FastifypreHandlerhook or@fastify/auth. - JWT: If users log in elsewhere, validate JWTs using
@fastify/jwt. - OAuth: For third-party integrations, use
@fastify/oauth2.
- API Keys: Implement a check for a custom
- If this API endpoint should not be public, protect it. Use strategies like:
-
Other Considerations:
- Helmet: Use
@fastify/helmetto set various security-related HTTP headers.npm install @fastify/helmet, thenfastify.register(require('@fastify/helmet'))inserver.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).
- Helmet: Use
8. Handling Special Cases
Real-world SMS sending involves nuances:
- Sender ID (
fromfield): 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 optionalsenderIdparameter insendSingleSmsand 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
messageIdfrom 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.