code examples

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

SMS Marketing Campaigns with Sinch API: Fastify & Node.js Guide

Build production-ready bulk SMS marketing campaigns using Sinch REST API, Fastify, and Node.js. Complete guide covering rate limiting, TCPA compliance, Prisma integration, opt-out handling, and deployment for scalable messaging systems.

Building a Scalable SMS Campaign Sender with Fastify, Node.js, and Sinch

Learn how to build production-ready SMS marketing campaigns using Sinch REST API, Fastify, and Node.js. This comprehensive tutorial covers bulk SMS sending, TCPA compliance, rate limiting, database integration with Prisma, and deployment strategies for scalable messaging systems.

You'll create a complete API service that manages bulk SMS campaigns, handles opt-outs, implements suppression lists, and processes delivery reports. This guide addresses real-world requirements for SMS marketing including regulatory compliance, performance optimization, and error handling.

Project Overview and Goals

  • Goal: Create a Node.js backend service that exposes an API endpoint to send SMS messages to a list of recipients using the Sinch SMS API.
  • Technology:
    • Node.js: JavaScript runtime environment.
    • Fastify: High-performance web framework chosen for speed, extensibility, and built-in logging with schema validation.
    • Sinch SMS REST API: Third-party service for sending SMS via the /batches endpoint.
    • Axios: Promise-based HTTP client for Sinch API requests.
    • Dotenv: Secure environment variable management.
    • Prisma: (Optional) ORM for database interaction to log campaign details.
    • Docker: Application containerization for deployment.
  • Outcome: REST API endpoint (POST /campaigns) that accepts JSON payload with recipients (phone number array) and message (string), sends SMS via Sinch, logs attempts, and returns confirmation.
  • Prerequisites:
    • Node.js and npm (or yarn) installed.
    • Sinch account with SMS API credentials (Service Plan ID, API Token).
    • Provisioned phone number within your Sinch account.
    • Basic familiarity with Node.js, REST APIs, and terminal commands.
    • (Optional) Docker installed for containerization.
    • (Optional) Database (PostgreSQL, MySQL) for campaign logging with Prisma.

System Architecture

mermaid
graph LR
    A[Client / API Caller] -- HTTP POST /campaigns --> B(Fastify App);
    B -- Send SMS Request --> C(Sinch SMS REST API);
    C -- SMS Delivery --> D(Recipient Phones);
    B -- Log Campaign Data --> E[(Database)];
    C -- Response --> B;
    B -- API Response --> A;

    style B fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#ccf,stroke:#333,stroke-width:2px
    style E fill:#eee,stroke:#333,stroke-width:1px,stroke-dasharray: 5 5

(Note: Verify Mermaid diagram rendering on your publishing platform)

1. Setting up the Project

Initialize your Node.js project and install necessary dependencies.

  1. Create Project Directory: Open your terminal and create a directory for the project:

    bash
    mkdir sinch-fastify-campaigns
    cd sinch-fastify-campaigns
  2. Initialize Node.js Project: Create a package.json file with default settings:

    bash
    npm init -y
  3. Install Dependencies: Install Fastify, Axios, and Dotenv:

    bash
    npm install fastify axios dotenv
  4. Install Development Dependencies (Optional – Prisma): For database logging, install Prisma:

    bash
    npm install prisma @prisma/client --save-dev
  5. Initialize Prisma (Optional): Create prisma directory with schema.prisma file:

    bash
    npx prisma init
    • Configuration: Update DATABASE_URL in .env with your database connection string.
  6. Create Project Structure: Set up directory structure:

    bash
    mkdir src
    touch src/server.js src/sinchService.js .env .gitignore
    • src/server.js: Main application file with Fastify setup and routes.
    • src/sinchService.js: Module for Sinch API interaction.
    • .env: Stores sensitive credentials. Never commit this file.
    • .gitignore: Specifies untracked files for Git.
  7. Configure .gitignore: Add Node.js ignores and .env:

    plaintext
    # .gitignore
    
    node_modules
    .env
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    dist
    coverage
    .DS_Store
  8. Configure .env: Add placeholders for Sinch credentials:

    plaintext
    # .env
    
    # Sinch Credentials
    SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
    SINCH_API_TOKEN=YOUR_API_TOKEN
    SINCH_NUMBER=YOUR_SINCH_PHONE_NUMBER # e.g., +15551234567
    SINCH_REGION_URL=https://us.sms.api.sinch.com # Or eu., ca., au., br., etc.
    
    # Server Configuration
    PORT=3000
    
    # Database (Optional – Adjust based on your provider)
    # Example for PostgreSQL
    DATABASE_URL="postgresql://user:password@host:port/database?schema=public"
    • Purpose: .env keeps sensitive data secure and out of your codebase. dotenv loads these into process.env.

2. Implementing Core Functionality (Sinch Service)

Encapsulate SMS sending logic in a dedicated service module.

  1. Edit src/sinchService.js: Create a function for Sinch's /batches endpoint:

    javascript
    // src/sinchService.js
    import axios from 'axios';
    
    // Load Sinch credentials from environment variables
    const SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID;
    const API_TOKEN = process.env.SINCH_API_TOKEN;
    const SINCH_NUMBER = process.env.SINCH_NUMBER;
    const SINCH_API_BASE_URL = process.env.SINCH_REGION_URL || 'https://us.sms.api.sinch.com';
    
    /**
     * Sends an SMS campaign batch using the Sinch REST API.
     * @param {string[]} recipients - Array of phone numbers in E.164 format (e.g., +15551234567).
     * @param {string} message - Text message body.
     * @returns {Promise<object>} - Response data from Sinch API.
     * @throws {Error} - Throws error if API call fails.
     */
    async function sendSmsBatch(recipients, message) {
      if (!SERVICE_PLAN_ID || !API_TOKEN || !SINCH_NUMBER) {
        throw new Error('Sinch API credentials are not configured in .env file.');
      }
      if (!recipients || recipients.length === 0) {
        throw new Error('Recipient list cannot be empty.');
      }
      if (!message) {
        throw new Error('Message body cannot be empty.');
      }
    
      const endpoint = `${SINCH_API_BASE_URL}/xms/v1/${SERVICE_PLAN_ID}/batches`;
    
      const payload = {
        from: SINCH_NUMBER,
        to: recipients,
        body: message,
      };
    
      const config = {
        headers: {
          'Authorization': `Bearer ${API_TOKEN}`,
          'Content-Type': 'application/json',
        },
      };
    
      try {
        console.log(`Sending SMS batch to ${recipients.length} recipients via Sinch…`);
        const response = await axios.post(endpoint, payload, config);
        console.log('Sinch API response:', response.data);
        return response.data;
      } catch (error) {
        console.error('Error sending SMS via Sinch:', error.response?.data || error.message);
        throw new Error(`Sinch API request failed: ${error.response?.data?.text || error.message}`);
      }
    }
    
    export { sendSmsBatch };
    • Why this approach?
      • Modularity: Separates Sinch logic from server code.
      • Security: Loads credentials from environment variables.
      • Error Handling: Includes validation and catches Axios errors.
      • Clarity: Uses documented Sinch /batches endpoint structure.

3. Building the API Layer with Fastify

Set up Fastify server and define the API endpoint.

  1. Edit src/server.js: Configure Fastify, load environment variables, and define routes:

    javascript
    // src/server.js
    import Fastify from 'fastify';
    import dotenv from 'dotenv';
    import { sendSmsBatch } from './sinchService.js';
    
    // Load environment variables
    dotenv.config();
    
    // Initialize Fastify with logging
    const fastify = Fastify({
      logger: true
    });
    
    // API Route Definition
    const campaignSchema = {
      body: {
        type: 'object',
        required: ['recipients', 'message'],
        properties: {
          recipients: {
            type: 'array',
            items: {
              type: 'string',
              pattern: '^\\+[1-9]\\d{1,14}$' // E.164 format validation
            },
            minItems: 1,
          },
          message: {
            type: 'string',
            minLength: 1,
            maxLength: 1600
          }
        }
      },
      response: {
        200: {
          type: 'object',
          properties: {
            message: { type: 'string' },
            batchId: { type: 'string' },
            recipientCount: { type: 'number' }
          }
        },
      }
    };
    
    fastify.post('/campaigns', { schema: campaignSchema }, async (request, reply) => {
      const { recipients, message } = request.body;
    
      // **Important:** Implement suppression list check here.
      // Filter out recipients who opted out.
    
      try {
        const sinchResponse = await sendSmsBatch(recipients, message);
    
        reply.code(200).send({
          message: 'SMS campaign batch submitted successfully.',
          batchId: sinchResponse.id,
          recipientCount: recipients.length
        });
    
      } catch (error) {
        fastify.log.error(`Campaign sending failed: ${error.message}`);
        reply.code(500).send({ error: 'Failed to send SMS campaign.', details: error.message });
      }
    });
    
    // Health Check Route
    fastify.get('/health', async (request, reply) => {
      return { status: 'ok', timestamp: new Date().toISOString() };
    });
    
    // Start Server
    const start = async () => {
      try {
        const port = process.env.PORT || 3000;
        await fastify.listen({ port: parseInt(port, 10), host: '0.0.0.0' });
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    start();
    • Why Fastify? Schema validation automatically handles request validation, improving security. Built-in Pino logger provides high-performance logging.
    • Route Logic: /campaigns validates requests, calls sinchService, handles errors, and sends appropriate responses.
    • Health Check: /health endpoint enables monitoring and container orchestration.
    • Server Start: Listens on configured port and host 0.0.0.0 (required for Docker).

4. Integrating with Sinch (Credentials Setup)

Obtain API credentials from Sinch.

  1. Navigate to Sinch Dashboard: Log in to your Sinch Customer Dashboard.

  2. Find SMS API Credentials:

    • Go to SMSAPIs.
    • Locate your Service plan ID.
    • Click your Service Plan ID name.
    • Under API Credentials, find your API token. Click "Show" or generate one.
    • In Numbers section, find your provisioned phone number (E.164 format: +15551234567).
    • Note your Region (US, EU, CA, AU, BR). Common URLs:
      • US: https://us.sms.api.sinch.com
      • EU: https://eu.sms.api.sinch.com
      • Canada: https://ca.sms.api.sinch.com
      • Australia: https://au.sms.api.sinch.com
      • Brazil: https://br.sms.api.sinch.com
  3. Update .env File: Add your credentials:

    plaintext
    # .env
    SINCH_SERVICE_PLAN_ID=YOUR_ACTUAL_SERVICE_PLAN_ID
    SINCH_API_TOKEN=YOUR_ACTUAL_API_TOKEN
    SINCH_NUMBER=+1XXXXXXXXXX
    SINCH_REGION_URL=https://<region>.sms.api.sinch.com
    • Security: Keep .env secure and ensure it's in .gitignore.

5. Error Handling, Logging, and Retry Mechanisms

Your setup includes basic error handling and logging.

  • Error Handling:
    • try...catch blocks catch exceptions in server.js and sinchService.js.
    • Fastify schema validation catches malformed requests.
    • sinchService throws errors for missing credentials or failed API calls.
    • API route returns 500 on errors with generic messages, logging details internally.
  • Logging:
    • Fastify's logger: true uses Pino for efficient JSON-based logging.
    • Logs include request details, errors, and informational messages.
  • Retry Mechanisms (Advanced):
    • For production, implement retries with exponential backoff for transient errors (5xx responses).

    • Use axios-retry or manual implementation with setTimeout.

    • Example Concept:

      javascript
      // Inside sinchService.js – Conceptual Retry Logic
      async function sendWithRetry(url, payload, config, logger = console, retries = 3, delay = 1000) {
          try {
              return await axios.post(url, payload, config);
          } catch (error) {
              if (retries > 0 && (!error.response || error.response.status >= 500)) {
                  logger.warn(`Retrying Sinch request (${retries} left) after ${delay} ms delay…`);
                  await new Promise(resolve => setTimeout(resolve, delay));
                  return sendWithRetry(url, payload, config, logger, retries - 1, delay * 2);
              } else {
                  throw error;
              }
          }
      }
    • Testing Errors: Stop your network, provide invalid credentials, or use toxiproxy to simulate network failures.

6. Creating a Database Schema and Data Layer (Optional – Prisma)

If you initialized Prisma, define a schema for campaign logging.

  1. Define Schema (prisma/schema.prisma): Add campaign model:

    prisma
    // prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    model Campaign {
      id             Int      @id @default(autoincrement())
      createdAt      DateTime @default(now())
      message        String
      recipientCount Int
      status         String   // PENDING, SENT, FAILED
      batchId        String?
      errorDetails   String?
    }
    
    // Optional: Model for Suppression List
    /*
    model SuppressionList {
        phoneNumber String   @id @unique
        reason      String?
        createdAt   DateTime @default(now())
        updatedAt   DateTime @updatedAt
    }
    */
  2. Create Database Migration: Generate and apply SQL migration:

    bash
    npx prisma migrate dev --name init_campaign_model
  3. Generate Prisma Client: Update Prisma client:

    bash
    npx prisma generate
  4. Integrate with Server Code:

    • Uncomment Prisma lines in src/server.js (import, initialization, database operations).
    • Adds database logging for campaign attempts and final status.

7. Adding Security Features

Security is critical for any API.

  1. Input Validation:

    • Fastify schema validation checks request structure, data types, and constraints (E.164 format, message length).
    • Prevents injection attacks and malformed data.
  2. Rate Limiting:

    • Protect against abuse and brute-force attacks.

    • Install @fastify/rate-limit:

      bash
      npm install @fastify/rate-limit
    • Register in src/server.js:

      javascript
      // src/server.js
      import rateLimit from '@fastify/rate-limit';
      
      await fastify.register(rateLimit, {
        max: 100,
        timeWindow: '1 minute'
      });
    • Adjust max and timeWindow based on usage patterns.

  3. Secrets Management:

    • Use .env and dotenv for local development.
    • Production: Use platform secret management (Docker Secrets, Kubernetes Secrets, PaaS environment variables). Never commit .env.
  4. HTTPS:

    • Always use HTTPS in production via reverse proxy (Nginx, Caddy) or hosting platform.
  5. Helmet (Optional but Recommended):

    • Use @fastify/helmet for security headers:
    bash
    npm install @fastify/helmet
    javascript
    // src/server.js
    import helmet from '@fastify/helmet';
    await fastify.register(helmet);

8. Handling Special Cases for SMS Marketing

Real-world SMS marketing campaigns require specific considerations for compliance, deliverability, and cost optimization.

  • Large Recipient Lists:
    • Sinch's /batches endpoint supports up to 1,000 recipients per batch (Sinch SMS API Release Notes).
    • For campaigns exceeding 1,000 recipients, split into multiple batches, respecting API rate limits.
    • Asynchronous Processing: Use background job queue (BullMQ with Redis) for large batches. Return immediate 202 Accepted response.
  • Character Limits & Encoding:
    • GSM-7 encoding: 160 characters per segment.
    • Unicode/UCS-2 (emojis, special characters): 70 characters per segment.
    • Sinch handles concatenation, but you're billed per segment.
    • Set maxLength validation (e.g., 1600 characters) to prevent excessive costs.
  • Opt-Outs & Compliance:
    • Crucial: Follow TCPA (US), GDPR (EU) regulations. Only message consenting users.
    • Provide clear opt-out mechanism (reply STOP).
    • Handle incoming SMS webhooks from Sinch to process STOP keywords.
    • Maintain suppression list (database table).
    • Before sending, check recipients against suppression list and remove opted-out numbers.
  • International Formatting:
    • Use E.164 format (+ followed by country code and number) for international deliverability.
    • Schema validation enforces this with regex pattern validation.
  • Delivery Reports (DLRs):
    • Configure webhook URL in Sinch dashboard for delivery status updates.
    • Create Fastify route (e.g., POST /sinch/dlr) to handle webhook payloads.
    • Process DLRs to update campaign logs with delivery status.
    • Requires public endpoint (use ngrok for local testing).

9. Implementing Performance Optimizations

Optimize for high-load scenarios.

  • Asynchronous Processing (Queues): Offload Sinch API calls to background job queue (BullMQ) to prevent blocking and improve response times.

  • Database Connection Pooling: Prisma manages this automatically.

  • Caching: Cache suppression lists (Redis/Memcached) for large-scale lookups with proper invalidation.

  • Node.js Clustering: Use cluster module or PM2 (pm2 start src/server.js -i max) to leverage multi-core processors.

  • Load Testing: Use k6, autocannon, or wrk to simulate traffic:

    bash
    # Example using autocannon (npm i -g autocannon)
    autocannon -m POST -H "Content-Type: application/json" -b '{"recipients":["+15551234567"],"message":"Test Load"}' http://localhost:3000/campaigns
  • Profiling: Use Node.js profiler (node --prof) or Clinic.js (npm i -g clinic; clinic doctor -- node src/server.js) to analyze performance.

10. Adding Monitoring, Observability, and Analytics

Monitor production service behavior.

  • Health Checks:

    • GET /health provides basic liveness check.
    • Enhance with database connectivity checks.
  • Structured Logging:

    • Fastify's Pino logger outputs JSON for log aggregation (ELK Stack, Splunk, Datadog, Grafana Loki).
    • Logs capture context (request IDs, batch IDs, user IDs).
  • Performance Metrics (Prometheus):

    • Integrate prom-client for application metrics:
    bash
    npm install prom-client
    • Expose /metrics endpoint and track custom metrics (campaign rate, Sinch API latency).
    • Set up Prometheus scraping and Grafana visualization.
  • Error Tracking:

    • Use Sentry, Bugsnag, or Datadog APM for real-time error capture with context.
  • Dashboards:

    • Create Grafana dashboards showing:
      • API request rate and latency
      • API error rates (4xx, 5xx)
      • Sinch API latency and errors
      • Campaign throughput
      • Database query performance
      • System resource usage
  • Alerting:

    • Configure alerts for:
      • High API error rate (> 1%)
      • High API latency (> 500 ms p95)
      • High Sinch API error rate
      • /health endpoint failures
      • High resource utilization
      • Job queue failures

11. Troubleshooting and Caveats

Common issues and solutions:

  • Sinch Errors:
    • 401 Unauthorized: Incorrect SERVICE_PLAN_ID or API_TOKEN. Verify .env and dashboard. Check Authorization: Bearer <token> format.
    • 400 Bad Request: Invalid JSON structure, incorrect E.164 format, or parameter issues. Check error.response.data.
    • 403 Forbidden/Insufficient Funds: Account lacks funds or permissions. Check balance and settings.
    • 5xx Server Error: Temporary Sinch issue. Implement retries.
  • Configuration Issues:
    • .env not loaded: Ensure dotenv.config() is called early. Verify file location.
    • Incorrect DATABASE_URL: Check format, credentials, and host.
    • Firewall Issues: Ensure outbound requests to Sinch API and database. For webhooks, ensure Sinch can reach your endpoint.
  • Code Errors:
    • Variable name typos
    • Incorrect async/await usage
    • Schema validation errors
  • Deployment Problems:
    • Environment variables not set correctly (use platform secrets management)
    • Incorrect host binding (use 0.0.0.0 for Docker)
    • Port conflicts
  • Compliance/Opt-Out Failures:
    • Critical: Forgetting suppression list check can cause legal issues. Test thoroughly. Process incoming STOP messages correctly.

Check detailed error messages in Fastify logs (fastify.log.error) and Axios response data (error.response?.data) when troubleshooting.

FAQ

How do I get Sinch SMS API credentials?

Log in to your Sinch Customer Dashboard, navigate to SMS → APIs, and locate your Service Plan ID. Click on the Service Plan ID to view your API token (generate one if needed). Find your provisioned phone number in the Numbers section – this is your sender number. Note your region (US, EU, CA, AU, BR) to construct the correct API base URL (e.g., https://us.sms.api.sinch.com).

What is the maximum number of recipients per Sinch SMS batch?

Sinch's /batches endpoint supports up to 1,000 recipients per batch request (Sinch SMS API Documentation). The to field accepts an array of 1–1000 phone numbers in E.164 format. This limit was increased from 100 to 1,000 on October 12, 2019 (Sinch Release Notes). For campaigns exceeding 1,000 recipients, split into multiple batch requests and implement rate limiting. Use background job queues (BullMQ with Redis) for asynchronous processing.

How do I implement E.164 phone number validation in Fastify?

Use Fastify's JSON schema validation with regex pattern: pattern: '^\\+[1-9]\\d{1,14}$'. This enforces E.164 format (+ followed by country code and number, 7–15 digits total). Schema validation runs automatically before route handler execution, rejecting malformed numbers with 400 Bad Request responses.

What are TCPA and GDPR requirements for SMS marketing campaigns?

TCPA (US) requires prior express written consent, clear opt-out mechanisms (STOP keyword), mandatory communication hours (8:00 AM – 9:00 PM recipient local time), and suppression lists (FCC TCPA Guidelines). GDPR (EU) requires explicit consent, right to erasure, and data processing records. For US compliance, most businesses need 10DLC registration for SMS marketing campaigns. Implement suppression list database table, check recipients before every campaign, and process STOP keywords via Sinch webhooks. Penalties: $500–$1,500 per violation for TCPA (Tratta TCPA Guide) with no upper limit, and up to 4% of annual global revenue for GDPR violations.

How do I handle SMS character limits and encoding with Sinch?

GSM-7 encoding supports 160 characters per segment. Unicode/UCS-2 (emojis, special characters) reduces this to 70 characters per segment. Sinch automatically concatenates longer messages, but you're billed per segment. Set maxLength validation (e.g., 1600 characters for ~10 segments) in Fastify schema to prevent excessive costs. Monitor segment counts in production.

What retry strategy should I use for Sinch API failures?

Implement exponential backoff with 3–5 retry attempts for transient errors (5xx responses, network timeouts). Use axios-retry or manual implementation with setTimeout. Only retry retriable errors – don't retry 4xx client errors. Example: retry after 1 s, 2 s, 4 s delays. Log all retry attempts and final failures. Consider circuit breaker patterns for sustained Sinch API outages.

How do I process opt-outs and STOP keywords with Sinch?

Configure webhook URL in Sinch dashboard (Service Plan settings) to receive inbound SMS. Create Fastify route (POST /sinch/inbound) to handle webhook payloads. Parse message body for STOP, UNSUBSCRIBE, or QUIT keywords. Add sender's phone number to suppression list database immediately. Send confirmation SMS. Check suppression list before every campaign send.

Use Prisma with Campaign model containing: id (autoincrement), createdAt (timestamp), message (text), recipientCount (integer), status (PENDING/SENT/FAILED), batchId (Sinch batch ID), and errorDetails (nullable string). Add SuppressionList model with: phoneNumber (E.164 string, unique primary key), reason (STOP/Complaint), createdAt, and updatedAt. Index phoneNumber for fast lookups.

How do I handle rate limiting for Sinch SMS API?

Install @fastify/rate-limit to protect your API endpoint (e.g., max: 100 requests per minute per IP). For Sinch API rate limits, each service plan has specific messages-per-second limits (Sinch Rate Limits Documentation). A batch with 10 recipients counts as 10 messages. Batches queue in first-in-first-out order. Check account tier limits, implement batch splitting for large campaigns, and use job queues to control velocity. Monitor for 429 Too Many Requests responses and implement backoff.

What monitoring and alerting should I implement for SMS campaigns?

Expose /metrics endpoint using prom-client for Prometheus. Track: API request rate and latency, Sinch API success/failure rates, campaign throughput, error rates by type, and queue depth. Set alerts for: error rate > 1%, API latency > 500 ms p95, Sinch API failures, health check failures, and low Sinch account balance. Use Grafana dashboards and Sentry for error tracking.

Frequently Asked Questions

How to send bulk SMS messages using Node.js?

Use a Node.js framework like Fastify with the Sinch SMS API. Create a Fastify server and an API endpoint that accepts recipient numbers and a message, then uses the Sinch API to send the SMS messages in bulk. This setup is scalable and efficient for marketing campaigns, notifications, or alerts.

What is Fastify and why use it for SMS campaigns?

Fastify is a high-performance Node.js web framework known for its speed and extensibility. It's an ideal choice for building an SMS campaign sender because of its efficiency, built-in logging, and features like schema validation, which improve security and maintainability.

Why are environment variables important for Sinch API integration?

Environment variables securely store sensitive credentials like your Sinch Service Plan ID and API token. This protects your API keys from being exposed in your codebase, enhancing security and preventing accidental leaks.

How to structure a Node.js project for sending SMS campaigns?

Create separate modules for server logic (`server.js`) and Sinch API interaction (`sinchService.js`). Use `.env` to manage environment variables, and consider a database integration (e.g., Prisma) for logging campaign details. This promotes modularity, security, and maintainability.

What is the role of Axios in this SMS campaign setup?

Axios is a promise-based HTTP client used to make requests to the Sinch SMS REST API. It simplifies the process of sending HTTP POST requests with the necessary headers and data to trigger SMS messages through Sinch.

When should I use a message queue for sending SMS messages?

A message queue (like BullMQ) is recommended for very large recipient lists or when asynchronous processing is beneficial. It offloads SMS sending to a background process, preventing blocking and improving the responsiveness of your main API.

How to handle Sinch API rate limits in Node.js?

Sinch's API has rate limits to prevent abuse. Respect these limits by breaking large recipient lists into smaller batches and submitting them sequentially or with controlled concurrency. Monitor Sinch's response codes and adjust your sending rate accordingly.

What is the character limit for SMS messages with Sinch?

Standard GSM-7 encoding allows 160 characters per SMS segment. Longer messages are split into multiple segments. Using non-GSM characters reduces the limit to 70 characters per segment. Sinch handles concatenation, but be aware of cost implications for multi-part messages.

Can I get delivery reports for SMS messages sent via Sinch?

Yes, Sinch offers delivery reports (DLRs) via webhooks. Configure a webhook URL in your Sinch dashboard, and create a corresponding route in your Fastify app to receive and process these status updates. This enables you to track message delivery success or failure.

How to handle opt-outs and comply with SMS regulations?

Implement a suppression list (e.g., using a database) to store opted-out numbers. Before sending any campaign, always check the recipient list against this suppression list. Provide a clear opt-out mechanism (e.g., 'reply STOP') and handle incoming webhooks from Sinch to process opt-outs. This ensures compliance with regulations like TCPA and GDPR.

What is E.164 format and why is it important for SMS?

E.164 is an international telephone number format that includes the '+' sign followed by the country code and number (e.g., +15551234567). Using E.164 format ensures correct number formatting for global SMS delivery, crucial for reaching international recipients.

How to add input validation to my SMS campaign API endpoint?

Leverage Fastify's schema validation to define the structure and data types of the request body. This automatically validates incoming requests, ensuring correct data format (like E.164 for phone numbers) and preventing issues related to invalid or malicious input.

What are some best practices for logging SMS campaign attempts?

Use structured logging (JSON format) with a library like Pino. Log campaign details (message, recipient count), status (pending, sent, failed), batch IDs from Sinch, and any error details. This aids debugging, monitoring, and analysis of your campaign performance.

How to troubleshoot common Sinch API integration issues?

Check for common error codes like 401 (Unauthorized – incorrect credentials), 400 (Bad Request – invalid format or parameters), and 5xx (Sinch server errors). Inspect the `error.response.data` from Axios for specific error details from Sinch and ensure credentials are correctly set in your environment variables.

How to improve the performance of my SMS campaign sender?

For high-volume campaigns, use a message queue (e.g., BullMQ) for asynchronous processing. Consider caching frequently accessed data (like suppression lists) and optimize your database interactions. Implement Node.js clustering or use a process manager like PM2 if CPU usage becomes a bottleneck.