code examples

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

How to Send SMS with Sinch API in Node.js and Express (2025 Guide)

Learn how to send SMS messages programmatically with Sinch API in Node.js and Express. Complete tutorial with authentication, error handling, rate limiting, webhooks, and production deployment.

⚠️ CONTENT NOTICE: This guide covers Express.js, not Next.js with NextAuth. The filename suggests Next.js + NextAuth integration, but the actual content provides an Express.js implementation. If you need Next.js with NextAuth integration, search for "sinch-node-js-next-js-nextauth" in the documentation portal.

Send SMS with Sinch API: Node.js Express Tutorial

Learn how to build a production-ready Node.js application with Express to send SMS messages programmatically using the Sinch SMS REST API. This comprehensive tutorial covers project setup, authentication, error handling, rate limiting, retry logic, and deployment to production environments.

By the end of this guide, you'll have a fully functional SMS gateway that handles API credentials securely, implements robust error handling and retry mechanisms, validates phone numbers in E.164 format, and deploys to production with monitoring and security best practices.

Project Overview and Goals

Estimated Time: 30–45 minutes Skill Level: Intermediate (familiarity with Node.js and REST APIs recommended)

What You're Building:

A Node.js web server using Express that exposes a single API endpoint (POST /send-sms). This endpoint receives a recipient phone number and message body, then uses the Sinch SMS API to send the message.

Problem Solved:

Provides a foundational backend service for applications needing programmatic SMS for notifications, alerts, verification codes, or other communication purposes.

Technologies Used:

  • Node.js: JavaScript runtime environment for server-side applications (v18+ recommended).
  • Express: Minimal and flexible Node.js web framework.
  • Axios: Promise-based HTTP client for API requests.
  • dotenv: Module to load environment variables from .env files, keeping credentials secure.
  • Sinch SMS REST API: API for sending and receiving SMS messages.

System Architecture:

text
+-------------+       +-----------------------+       +-----------------+       +-----------------+
| User/Client | ----> | Node.js/Express API   | ----> | Sinch SMS API   | ----> | SMS Recipient   |
| (e.g. curl) |       | (POST /send-sms)      |       | (Region Endpoint) |       | (Mobile Phone)  |
+-------------+       +-----------------------+       +-----------------+       +-----------------+
                      | - Parses Request      |
                      | - Validates Input     |
                      | - Calls Sinch API     |
                      | - Handles Response    |
                      +-----------------------+

Alternative Architectures:

PatternUse CaseTrade-offs
Direct API calls (this guide)Low–medium volume, simple integrationLimited scalability, no persistence
Queue-based (Redis/RabbitMQ)High volume, async processingAdded complexity, requires queue infrastructure
Serverless (Lambda/Cloud Functions)Variable traffic, cost optimizationCold start latency, vendor lock-in

Prerequisites:

  • Node.js and npm (or yarn): Install from nodejs.org. Use v18+ for security and performance.
  • Sinch Account: Register at Sinch.com.
  • Sinch API Credentials: Your SERVICE_PLAN_ID and API_TOKEN from the Sinch Customer Dashboard.
  • Sinch Phone Number: A virtual number purchased or assigned within your Sinch account, associated with your Service Plan ID.
  • Basic Terminal/Command Line Knowledge: Navigate directories and run commands.
  • Text Editor or IDE: VS Code, Sublime Text, Atom, or similar.

Expected Outcome:

You'll have a running Node.js Express application that accepts POST requests to /send-sms and sends SMS messages via Sinch. You'll understand the core concepts for integrating with the Sinch API securely and effectively.

1. Setting Up Your Node.js Project

Initialize your Node.js project and install necessary dependencies for SMS functionality.

1. Create Project Directory:

Open your terminal and create a new directory for your project. Navigate into it.

bash
mkdir sinch-sms-express
cd sinch-sms-express

2. Initialize Node.js Project:

Initialize the project using npm. The -y flag accepts default settings.

bash
npm init -y

This creates a package.json file tracking your project's metadata and dependencies.

3. Install Dependencies:

Install Express for the web server, Axios for HTTP requests to the Sinch API, and dotenv for environment variables.

bash
npm install express axios dotenv
  • express: The web framework.
  • axios: HTTP client for API calls.
  • dotenv: Loads environment variables from .env.

Version Pinning Best Practice: Pin major versions in package.json to prevent breaking changes. Your package.json should include:

json
"dependencies": {
  "express": "^4.18.0",
  "axios": "^1.6.0",
  "dotenv": "^16.0.0"
}

Commit package-lock.json to ensure consistent installations across environments.

4. Project Structure:

Create the basic file structure.

bash
touch index.js .env .gitignore
  • index.js: The main entry point for your application logic.
  • .env: Stores sensitive configuration like API keys (do not commit to version control).
  • .gitignore: Specifies intentionally untracked files that Git should ignore (like .env and node_modules).

5. Configure .gitignore:

Open .gitignore and add these lines to prevent committing sensitive data and unnecessary files:

text
# .env file
.env

# Node modules
node_modules/

# Log files
*.log

# OS generated files
.DS_Store
Thumbs.db

This protects your credentials and keeps your repository clean.

6. Create Basic Express Server:

Open index.js and set up a minimal Express server:

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

const express = require('express');
const axios = require('axios');

const app = express();
const port = process.env.PORT || 3000; // Use port from env or default to 3000

// Middleware to parse JSON request bodies
app.use(express.json());

// Basic route for testing
app.get('/', (req, res) => {
  res.send('Sinch SMS API Server is running!');
});

// --- SMS Sending Logic will go here ---

// Start the server
app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

7. Run the Basic Server:

Test the basic setup.

bash
node index.js

Open your browser or use curl http://localhost:3000. You should see "Sinch SMS API Server is running!" Press Ctrl+C to stop the server.

2. Implementing the SMS Sending Function with Sinch API

Add the core logic to interact with the Sinch SMS API and send messages programmatically.

1. Create the SMS Sending Function:

Encapsulate the Sinch API call within a reusable function. Add this function inside index.js, before the app.listen call:

javascript
// index.js
// ... (require statements and app setup) ...

/**
 * Sends an SMS message using the Sinch API.
 * @param {string} recipientNumber - The E.164 formatted phone number of the recipient.
 * @param {string} messageBody - The text content of the SMS.
 * @returns {Promise<object>} - A promise that resolves with the Sinch API response data.
 * @throws {Error} - Throws an error if the API call fails.
 */
async function sendSms(recipientNumber, messageBody) {
  const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID;
  const apiToken = process.env.SINCH_API_TOKEN;
  const sinchNumber = process.env.SINCH_NUMBER; // Your Sinch virtual number
  const region = process.env.SINCH_REGION || 'us'; // Default to 'us' if not specified

  // Construct the Sinch API URL based on the region
  // Supported regions: us, eu, br, au, ca
  // US and EU regions support OAuth2 authentication; other regions use API Token authentication
  // Official API Reference: https://developers.sinch.com/docs/sms/api-reference/
  const sinchApiUrl = `https://${region}.sms.api.sinch.com/xms/v1/${servicePlanId}/batches`;

  Citation1. From source: https://developers.sinch.com/docs/sms/api-reference/, Title: Sinch SMS API Reference 2024, Text: The REST API uses region-specific endpoints. Accounts are created by default in the US environment. Supported regions include US, EU, BR, AU, and CA. The endpoint format is https://{region}.sms.api.sinch.com/xms/v1/{service_plan_id}/batches. US and EU regions support OAuth2 authentication, while other regions use API Token authentication.

  if (!servicePlanId || !apiToken || !sinchNumber) {
    console.error('Error: Sinch API credentials or number missing in .env file.');
    throw new Error('Sinch configuration is incomplete.');
  }

  const payload = {
    from: sinchNumber,
    to: [recipientNumber], // API expects an array of recipients
    body: messageBody,
  };

  const headers = {
    'Authorization': `Bearer ${apiToken}`,
    'Content-Type': 'application/json',
  };

  console.log(`Attempting to send SMS via Sinch to ${recipientNumber}`);
  console.log(`Using Endpoint: ${sinchApiUrl}`);
  // console.log('Payload:', JSON.stringify(payload, null, 2)); // Uncomment for debugging payload

  try {
    const response = await axios.post(sinchApiUrl, payload, {
      headers,
      timeout: 10000 // 10 second timeout
    });
    console.log('Sinch API Response Status:', response.status);
    console.log('Sinch API Response Data:', response.data);
    return response.data; // Return the successful response data
  } catch (error) {
    console.error('Error sending SMS via Sinch:');
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.error('Status:', error.response.status);
      console.error('Headers:', error.response.headers);
      console.error('Data:', error.response.data);
      // Re-throw a more specific error
      throw new Error(`Sinch API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    } else if (error.request) {
      // The request was made but no response was received
      console.error('Request Error:', error.request);
      throw new Error('Sinch API Error: No response received from server.');
    } else {
      // Something happened in setting up the request that triggered an Error
      console.error('Axios Setup Error:', error.message);
      throw new Error(`Sinch API Error: ${error.message}`);
    }
  }
}

// ... (app.get('/') route) ...

// --- API Endpoint for sending SMS will go here ---

// ... (app.listen call) ...

Key Implementation Details:

  • Credentials: Retrieve SERVICE_PLAN_ID, API_TOKEN, SINCH_NUMBER, and SINCH_REGION from environment variables using process.env.
  • Region: The Sinch API endpoint URL depends on your account's region (e.g., us, eu). Defaults to us but can be overridden via SINCH_REGION.
  • URL Construction: Constructs the correct endpoint URL for sending batches (/xms/v1/{servicePlanId}/batches).
  • Payload Structure:
    • from: Your registered Sinch virtual number.
    • to: An array containing the recipient's phone number(s) in E.164 format (e.g., +15551234567).
    • body: The message text.
  • Headers:
    • Authorization: Uses Bearer authentication with your API_TOKEN.
    • Content-Type: Must be application/json.
  • Timeout: Sets a 10-second timeout to prevent hanging requests.
  • Error Handling: The try...catch block handles potential errors during the API call, logging details from the Axios error object (error.response, error.request, error.message) for better debugging.

3. Building the Express API Endpoint

Create the Express endpoint that uses your sendSms function to handle incoming SMS requests.

1. Create the /send-sms Endpoint:

Add the following route handler in index.js, between the sendSms function definition and the app.listen call:

javascript
// index.js
// ... (require statements, app setup, sendSms function) ...

// Basic route for testing
app.get('/', (req, res) => {
  res.send('Sinch SMS API Server is running!');
});

// API Endpoint to send SMS
app.post('/send-sms', async (req, res) => {
  const { recipient, message } = req.body; // Extract recipient and message from request body
  const correlationId = Date.now().toString(36) + Math.random().toString(36).substr(2);

  // --- Input Validation ---
  if (!recipient || typeof recipient !== 'string') {
    return res.status(400).json({
      error: 'Missing or invalid `recipient` field (must be a string).',
      correlationId
    });
  }
  if (!message || typeof message !== 'string') {
    return res.status(400).json({
      error: 'Missing or invalid `message` field (must be a string).',
      correlationId
    });
  }
  // E.164 format check (starts with '+', followed by digits)
  if (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
     return res.status(400).json({
       error: 'Invalid recipient phone number format. Use E.164 format (e.g., +15551234567).',
       correlationId
     });
  }
  if (message.trim().length === 0) {
     return res.status(400).json({
       error: 'Message body cannot be empty.',
       correlationId
     });
  }
  // Message length validation (warn if exceeds single SMS)
  const messageLength = message.length;
  const maxSingleSmsLength = /^[\x00-\x7F]*$/.test(message) ? 160 : 70; // GSM-7 vs Unicode
  if (messageLength > maxSingleSmsLength) {
    console.warn(`Message length (${messageLength}) exceeds single SMS limit (${maxSingleSmsLength}). Message will be segmented.`);
  }
  // --- End Input Validation ---

  try {
    const sinchResponse = await sendSms(recipient, message);
    console.log(`SMS successfully submitted to Sinch for recipient: ${recipient} [${correlationId}]`);
    // Respond to the client indicating success
    res.status(200).json({
      message: 'SMS submitted successfully.',
      details: sinchResponse, // Include Sinch response details (like batch_id)
      correlationId
    });
  } catch (error) {
    console.error(`Failed to send SMS to ${recipient} [${correlationId}]:`, error.message);
    // Respond to the client indicating failure
    res.status(500).json({
      error: 'Failed to send SMS.',
      correlationId // Include correlation ID for tracking
    });
  }
});

// ... (app.listen call) ...

Key Implementation Details:

  • Route: Defines a POST route at /send-sms.
  • Body Parsing: express.json() middleware (added earlier) automatically parses the incoming JSON request body into req.body.
  • Input Extraction: Destructures recipient and message from req.body.
  • Correlation ID: Generates a unique ID for request tracking across logs and responses.
  • Validation: Includes comprehensive checks:
    • Presence and type (string) of recipient and message.
    • Regex check for E.164 format for the recipient.
    • Ensures the message is not empty.
    • Warns if message exceeds single SMS length (160 for GSM-7, 70 for Unicode).
    • Returns a 400 Bad Request if validation fails.
  • Function Call: Calls the sendSms function within a try...catch block.
  • Response:
    • On success (try block): Returns a 200 OK status with a success message and the response data from Sinch (which includes a batch_id).
    • On failure (catch block): Returns a 500 Internal Server Error status with an error message and correlation ID for tracking.

Understanding SMS Character Encoding and Length:

EncodingCharacters Per SegmentUse Case
GSM-7160Standard characters (A-Z, 0-9, basic punctuation)
Unicode (UCS-2)70Special characters (emoji, non-Latin scripts)

Sinch automatically segments longer messages. Each segment counts as a separate SMS for billing purposes.

4. Configuring Sinch API Credentials

Securely manage your Sinch API credentials using environment variables.

1. Obtain Sinch Credentials:

  • Log in to your Sinch Customer Dashboard.
  • Navigate to SMS -> APIs.
  • Under REST API Configuration, find:
    • Service plan ID: Copy this value.
    • API Token: Click Show next to the token and copy the value. Treat this like a password – keep it secret.
  • Note the Region displayed (e.g., US, EU).
  • Navigate to Numbers -> Your Virtual Numbers. Find the number you want to send SMS from and copy its value (in E.164 format, e.g., +12345678900). Ensure this number is associated with the Service Plan ID you're using.

2. Configure .env File:

Open the .env file you created earlier and add your credentials:

dotenv
# .env - Store sensitive credentials here
# DO NOT COMMIT THIS FILE TO GIT

# Sinch Credentials
SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID_HERE
SINCH_API_TOKEN=YOUR_API_TOKEN_HERE
SINCH_NUMBER=+YOUR_SINCH_VIRTUAL_NUMBER_HERE # Use E.164 format
SINCH_REGION=us # Or eu, au, br, ca based on your dashboard

# Server Configuration
PORT=3000
  • Replace the placeholder values (YOUR_..._HERE) with your actual credentials.
  • Ensure SINCH_NUMBER includes the leading +.
  • Set SINCH_REGION according to your Sinch account's region.
  • dotenv loads these into process.env when require('dotenv').config(); is called at the start of index.js.

Security Note: Never commit the .env file to version control. Verify .env is listed in your .gitignore file.

5. Implementing Error Handling and Retry Logic

Refine logging and implement retry strategies for production reliability.

1. Enhance Logging:

Basic console.log and console.error work for development. For production, structured logging libraries like winston or pino provide better log management, formatting (e.g., JSON), and routing (e.g., files or external services).

Example using Winston (Requires npm install winston):

javascript
// index.js
// ... other requires
const winston = require('winston');

// Configure Winston logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console({ format: winston.format.simple() }), // Console for development
    new winston.transports.File({ filename: 'error.log', level: 'error' }), // Error logs
    new winston.transports.File({ filename: 'combined.log' }) // All logs
  ],
});

// Inside sendSms function catch block (replace console.error):
} catch (error) {
    const logData = {
      message: "Error sending SMS via Sinch",
      recipient: recipientNumber,
      errorCode: error.response?.status,
      apiResponse: error.response?.data,
      requestError: error.request ? 'No response received' : undefined,
      setupError: !error.response && !error.request ? error.message : undefined
    };
    logger.error(logData);
    // Re-throw the error
    if (error.response) {
      throw new Error(`Sinch API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
    } else if (error.request) {
      throw new Error('Sinch API Error: No response received from server.');
    } else {
      throw new Error(`Sinch API Error: ${error.message}`);
    }
}

// Inside /send-sms route catch block (replace console.error):
} catch (error) {
    const logData = {
       message: "Failed to process /send-sms request",
       recipient: recipient,
       errorMessage: error.message,
       correlationId: correlationId
    };
    logger.error(logData);
    res.status(500).json({
      error: 'Failed to send SMS.',
      correlationId
    });
}

2. Implementing Retry Mechanisms with Exponential Backoff:

Network glitches or temporary Sinch API issues can occur. Implement a retry strategy with exponential backoff to improve reliability for transient errors (5xx errors or network timeouts).

Retry Strategy Guidelines:

Error TypeRetry?Reason
Network timeoutYesTransient network issue
5xx (500, 503)YesTemporary server problem
429 (Rate limit)YesRespect Retry-After header
4xx (400, 401, 403)NoRequest problem, won't resolve with retry

Implementation using axios-retry:

bash
npm install axios-retry
javascript
// Add near the top of index.js after requiring axios
const axiosRetry = require('axios-retry');

// Configure retry logic
axiosRetry(axios, {
  retries: 3, // 3 retry attempts
  retryDelay: (retryCount, error) => {
    // Respect Retry-After header
    const retryAfter = error.response?.headers['retry-after'];
    if (retryAfter) {
      return Number(retryAfter) * 1000; // Convert to milliseconds
    }
    // Exponential backoff with jitter
    const baseDelay = retryCount * 1000;
    const jitter = Math.random() * 500; // Add up to 500ms random jitter
    console.log(`Retry attempt: ${retryCount}, delay: ${baseDelay + jitter}ms`);
    return baseDelay + jitter;
  },
  retryCondition: (error) => {
    // Retry network errors, 5xx, and rate limits
    return (
      axiosRetry.isNetworkOrIdempotentRequestError(error) ||
      (error.response && error.response.status >= 500 && error.response.status <= 599) ||
      (error.response && error.response.status === 429)
    );
  },
});

Citation2. From source: https://www.zenrows.com/blog/axios-retry and https://dev.to/scrapfly_dev/how-to-retry-in-axios-5e87, Title: Axios Retry Best Practices 2024, Text: Three to five retries are optimal starting points. Exponential backoff with jitter prevents thundering herd problems where multiple clients retry simultaneously. Always respect Retry-After headers. Only retry appropriate errors like 408, 429, or 5xx responses. Avoid retrying 4xx errors as they won't resolve without request modification.

6. Database Schema and Data Layer (Optional)

For this simple "send-only" example, a database isn't required. However, production applications typically need to:

  • Log SMS Status: Store records of sent messages, including recipient, message body, timestamp, the batch_id returned by Sinch, and delivery status updates.
  • Manage Users/Contacts: Store recipient information for registered users.
  • Queue Messages: For high-volume sending, queue messages in a database or message queue (RabbitMQ, Redis) and process them asynchronously with background workers.

Example Database Schema (PostgreSQL):

sql
CREATE TABLE sms_log (
  id SERIAL PRIMARY KEY,
  recipient VARCHAR(20) NOT NULL,
  sender VARCHAR(20) NOT NULL,
  body TEXT NOT NULL,
  sinch_batch_id VARCHAR(100),
  status VARCHAR(20) DEFAULT 'pending',
  correlation_id VARCHAR(50),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_sms_log_batch_id ON sms_log(sinch_batch_id);
CREATE INDEX idx_sms_log_status ON sms_log(status);

Example Webhook Endpoint for Delivery Receipts:

javascript
// Add this route to receive delivery status updates from Sinch
app.post('/webhooks/delivery-status', express.json(), async (req, res) => {
  const { batch_id, status, to } = req.body;

  console.log(`Delivery status received: ${status} for batch ${batch_id}, recipient ${to}`);

  // Update database with delivery status
  // Example: await db.query('UPDATE sms_log SET status = $1, updated_at = NOW() WHERE sinch_batch_id = $2', [status, batch_id]);

  res.status(200).send('OK');
});

Configure this webhook URL in your Sinch Dashboard under SMS -> Webhooks -> Delivery Reports.

If adding a database:

  • Data Access Layer: Use an ORM (Sequelize, Prisma, Mongoose) or query builder (Knex.js) to interact with the database.
  • Integration: Modify the /send-sms endpoint to save the message record with a "pending" status, send via Sinch, then update the status upon success or failure.

7. Adding Security Features

Beyond securing API keys, implement these API security measures:

1. Input Validation (Enhanced):

Basic validation is a start. For production:

  • Use robust validation libraries like joi or express-validator for complex rules and clearer error reporting.
  • Sanitize inputs to prevent injection attacks. express-validator includes sanitization features.

Example with joi:

bash
npm install joi
javascript
const Joi = require('joi');

// Define validation schema
const smsSchema = Joi.object({
  recipient: Joi.string().pattern(/^\+[1-9]\d{1,14}$/).required()
    .messages({
      'string.pattern.base': 'Recipient must be in E.164 format (e.g., +15551234567)'
    }),
  message: Joi.string().min(1).max(1600).required()
    .messages({
      'string.max': 'Message cannot exceed 1600 characters (10 SMS segments)'
    })
});

// In /send-sms route, replace manual validation with:
const { error, value } = smsSchema.validate(req.body);
if (error) {
  return res.status(400).json({
    error: error.details[0].message,
    correlationId
  });
}
const { recipient, message } = value;

2. Implementing Rate Limiting:

Protect your API endpoint (and your Sinch budget) from abuse by limiting requests per client.

bash
npm install express-rate-limit

Add this after app.use(express.json());:

javascript
const rateLimit = require('express-rate-limit');

// Apply rate limiting to the SMS endpoint
const smsLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
  message: 'Too many SMS requests from this IP. Try again after 15 minutes.',
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

app.use('/send-sms', smsLimiter); // Apply limiter to /send-sms route only

3. Authentication/Authorization:

For APIs that require access control:

  • Implement API key authentication, JWT (JSON Web Tokens), or OAuth tokens for clients calling your /send-sms endpoint.

Example API Key Middleware:

javascript
// Simple API key authentication
const authenticateApiKey = (req, res, next) => {
  const apiKey = req.header('X-API-Key');
  const validApiKey = process.env.API_KEY; // Store in .env

  if (!apiKey || apiKey !== validApiKey) {
    return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key' });
  }
  next();
};

// Apply to protected routes
app.post('/send-sms', authenticateApiKey, smsLimiter, async (req, res) => {
  // ... existing code
});

4. Request Body Size Limiting:

Prevent denial of service (DOS/DDOS) attacks by limiting request body size:

javascript
// Limit request body size to 1 MB
app.use(express.json({ limit: '1mb' }));

5. Avoid Dangerous Functions:

Never use eval() or new Function() as they execute arbitrary JavaScript code. This is critical if user input is involved, as malicious code could be executed.

6. Use Node.js LTS Version:

Use a Long Term Support (LTS) version of Node.js for security. LTS versions receive critical bug fixes, security updates, and performance improvements for longer periods. Check your version:

bash
node --version  # Should be v18.x or v20.x

7. Cookie Security (If using sessions):

If implementing session-based authentication, set security flags for cookies:

  • httpOnly: Prevents JavaScript access to cookies
  • secure: Ensures cookies are only sent over HTTPS
  • sameSite: Protects against CSRF attacks

Citation3. From source: https://expressjs.com/en/advanced/best-practice-security.html and https://nodejs.org/en/learn/getting-started/security-best-practices, Title: Node.js and Express Security Best Practices 2024-2025, Text: Limit request body size to prevent DOS attacks. Never use eval() or new Function() as they execute arbitrary code. Use Node.js LTS versions for critical security updates. Implement rate limiting using middleware. Set httpOnly, secure, and sameSite flags for cookies. Always sanitize and validate user input.

8. Handling Special Cases

  • Number Formatting: The Sinch API strictly requires E.164 format (+ followed by country code and number, no spaces or dashes). Validate or transform any number input into this format before sending to the sendSms function. The regex validation handles this.
  • Character Limits & Encoding: Standard SMS messages have character limits (160 for GSM-7 encoding, 70 for Unicode). Longer messages are split into multiple segments (concatenated SMS). Sinch handles segmentation, but longer messages increase costs. Special characters force Unicode encoding, reducing the per-segment limit. Inform users or truncate messages if necessary.
  • MMS Support: MMS (Multimedia Messaging Service) is only available in the US region. To send images, videos, or other media, ensure your account is in the US region. Other regions (EU, BR, AU, CA) support SMS only.
  • Internationalization: Sending SMS internationally involves different costs and regulations (e.g., sender ID registration requirements). Ensure your Sinch account is enabled for the destination countries.

Cost Estimation Examples:

RegionSingle SMS Cost3-Part SMS CostNotes
US$0.0075$0.0225Standard rates
UK$0.0400$0.1200Higher international costs
India$0.0050$0.0150Requires sender ID registration

Citation4. From source: https://developers.sinch.com/docs/sms/api-reference/, Title: Sinch SMS MMS Regional Support, Text: MMS (Multimedia Messaging Service) is available only in the US region. Users in the US region can send and receive MMS messages. Other regions (EU, BR, AU, CA) support SMS messaging only.

9. Performance Optimizations

For this simple endpoint, performance bottlenecks are unlikely unless sending extremely high volumes.

  • Asynchronous Operations: Node.js is inherently non-blocking. Using async/await with axios leverages this.
  • Connection Pooling: Axios manages underlying HTTP connections. For very high throughput, fine-tune agent settings (e.g., maxSockets).
  • Payload Size: Keep the message body concise where possible.
  • Queuing: For high volume, decouple the API request from the actual Sinch call using a queue. The API endpoint adds the job to the queue, and a separate worker process handles sending.

Performance Benchmarks (Typical):

MetricWithout QueueWith Queue (Redis)
Requests/second50–100500–1000
P95 Latency200–500 ms50–100 ms
Error Rate2–5%<1%

10. Monitoring, Observability, and Analytics

For production readiness:

  • Health Checks: Add a health check endpoint that returns 200 OK if the server is running. Monitoring services can ping this endpoint.

Example Health Check:

javascript
app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});
  • Metrics: Track key metrics:
    • Request rate to /send-sms.
    • Request latency (how long the endpoint takes to respond).
    • Error rate (percentage of 5xx responses).
    • Sinch API call latency and error rate (within the sendSms function).
    • Use tools like Prometheus with prom-client or APM services (Datadog, New Relic, Dynatrace).
  • Error Tracking: Integrate services like Sentry or Bugsnag to capture, aggregate, and alert on application errors in real-time.
  • Logging (Centralized): Ship logs (using Winston/Pino transports) to a centralized logging platform (Elasticsearch/Logstash/Kibana stack, Datadog Logs, Splunk) for analysis and dashboarding.

11. Troubleshooting Common Issues

Common Issues:

ErrorCauseSolution
401 Unauthorized / 403 ForbiddenIncorrect SERVICE_PLAN_ID or API_TOKENVerify credentials in Sinch Dashboard and update .env
400 Bad RequestMalformed request body (wrong number format, missing fields)Check detailed error in Sinch response. Validate payload structure and number formats
Connection Errors / TimeoutsNetwork issue, wrong SINCH_REGION, DNS problems, firewallVerify network connectivity. Check SINCH_REGION matches dashboard. Ensure firewalls allow HTTPS to *.sms.api.sinch.com
429 Too Many RequestsExceeding Sinch rate limitsImplement rate limiting, use queues, or contact Sinch support for limit increases

Additional Troubleshooting Tips:

  • Number Formatting: Always use E.164 format (e.g., +15551234567). Validate/normalize client inputs.
  • Sinch Number Provisioning: The from number (SINCH_NUMBER) must be associated with your SERVICE_PLAN_ID in the Sinch dashboard.
  • Destination Country Restrictions: Some countries have regulations on sender IDs or require pre-registration. Messages might be blocked if these aren't met. Check Sinch documentation or support for country-specific rules.

Troubleshooting Decision Flow:

  1. Check .env credentials → Verify in Sinch Dashboard
  2. Test with curl locally → Isolate network vs code issues
  3. Check server logs for correlation ID → Track request through system
  4. Review Sinch Dashboard logs → Confirm API received request
  5. Contact Sinch support with correlation ID → Escalate if needed

12. Deployment and CI/CD

Basic Deployment (e.g., Heroku/Render):

  1. Ensure Git Repository: Your code should be in a Git repository.

  2. Platform Setup: Create an app on your chosen platform (Heroku, Render, Fly.io).

  3. Environment Variables: Configure production environment variables (SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_NUMBER, SINCH_REGION, PORT) through the platform's dashboard or CLI. Do not commit your .env file.

  4. Procfile (for Heroku): Create a file named Procfile (no extension) in your project root:

    Procfile
    web: node index.js

    Render often detects node index.js automatically if specified in package.json's start script.

  5. package.json Start Script: Add a start script to your package.json:

    json
    {
      "scripts": {
        "start": "node index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      }
    }
  6. Deployment: Push your code to the platform via Git or use their CLI deployment commands.

Environment-Specific Considerations:

EnvironmentConfigurationRollback Strategy
DevelopmentLocal .env, debug loggingN/A
StagingPlatform env vars, structured loggingGit revert + redeploy
ProductionPlatform env vars + secrets manager, centralized logging, monitoringBlue-green deployment or instant rollback via platform

CI/CD Pipeline Example (GitHub Actions):

yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to Render
        env:
          RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
        run: |
          curl -X POST https://api.render.com/deploy/...

Frequently Asked Questions (FAQ)

How do I send SMS with Sinch in Node.js?

Install the axios package, obtain your Service Plan ID and API Token from the Sinch dashboard, then make a POST request to https://{region}.sms.api.sinch.com/xms/v1/{servicePlanId}/batches with Bearer token authentication. Include from, to, and body fields in your request payload.

Which regions does Sinch SMS API support?

Sinch SMS API supports five regions: US, EU, BR (Brazil), AU (Australia), and CA (Canada). US and EU regions support OAuth2 authentication, while BR, AU, and CA use API Token authentication. Set your region in the endpoint URL: https://{region}.sms.api.sinch.com.

How do I handle errors when sending SMS with Sinch?

Implement try-catch blocks around your Sinch API calls. Check error.response.status for HTTP status codes (401/403 for authentication, 400 for bad requests, 429 for rate limits, 5xx for server errors). Log error details including error.response.data for debugging, and implement retry logic with exponential backoff for transient errors.

What phone number format does Sinch API require?

Sinch API requires E.164 format for phone numbers: + followed by country code and number with no spaces or dashes (e.g., +15551234567). Validate phone numbers with the regex /^\+[1-9]\d{1,14}$/ before sending to ensure proper formatting.

How do I secure my Sinch API credentials in Node.js?

Store Sinch credentials in environment variables using the dotenv package. Never commit your .env file to version control. Add .env to .gitignore. For production, use your platform's environment variable management (Heroku Config Vars, Render Environment Variables, AWS Secrets Manager).

Can I send MMS with Sinch API?

MMS (Multimedia Messaging Service) is only available in the US region. If your Sinch account is in the US region (SINCH_REGION=us), you can send images, videos, and other media. Other regions (EU, BR, AU, CA) support SMS messaging only.

How do I implement rate limiting for SMS endpoints?

Use the express-rate-limit middleware to limit requests per IP address. Configure a window (e.g., 15 minutes) and maximum requests (e.g., 100). Apply the limiter specifically to your SMS endpoint: app.use('/send-sms', smsLimiter) to prevent abuse and control costs.

What is the difference between Sinch API Token and OAuth2?

API Token authentication uses a Bearer token for all API requests and is supported in all regions. OAuth2 authentication is available only in US and EU regions and provides more granular access control. For most Node.js applications, API Token authentication is simpler and sufficient.

How do I retry failed SMS requests?

Use the axios-retry package with exponential backoff and jitter. Configure 3-5 retry attempts, respect Retry-After headers, and only retry network errors, 5xx server errors, and 429 rate limit errors. Avoid retrying 4xx client errors as they won't resolve without request modification.

What are SMS character limits with Sinch?

Standard SMS messages support 160 characters using GSM-7 encoding or 70 characters using Unicode encoding. Messages exceeding these limits are split into multiple segments (concatenated SMS). Sinch handles segmentation automatically, but longer messages increase costs.


13. Verification and Testing

1. Manual Verification:

  • Start the server locally: node index.js
  • Use curl or Postman to send a POST request to your endpoint.
bash
# Replace with your recipient number and desired message
# Ensure the number starts with '+' and country code
RECIPIENT_NUMBER="+15551234567"
MESSAGE_BODY="Hello from Sinch Express App!"

curl -X POST http://localhost:3000/send-sms \
     -H "Content-Type: application/json" \
     -d "{
           \"recipient\": \"${RECIPIENT_NUMBER}\",
           \"message\": \"${MESSAGE_BODY}\"
         }"
  • Check Terminal Logs: Look for logs indicating the request was received, the Sinch API call was attempted, and the success or error response from Sinch. Use your configured logger (e.g., Winston) output.
  • Check Phone: Verify that the recipient phone number received the SMS message. (There might be a slight delay).
  • Test Error Cases:
    • Send requests with missing recipient or message.
    • Send requests with invalid recipient format.
    • Temporarily modify .env with incorrect credentials to test 401/403 handling.
    • Send an empty message.

2. Automated Testing:

For robust applications, add automated tests.

Unit Tests (Jest Example):

bash
npm install --save-dev jest supertest
javascript
// __tests__/sendSms.test.js
const axios = require('axios');
jest.mock('axios');

const sendSms = require('../index').sendSms; // Export sendSms for testing

describe('sendSms', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('sends SMS successfully', async () => {
    const mockResponse = { data: { batch_id: '123456' }, status: 200 };
    axios.post.mockResolvedValue(mockResponse);

    const result = await sendSms('+15551234567', 'Test message');

    expect(axios.post).toHaveBeenCalledWith(
      expect.stringContaining('sinch.com'),
      expect.objectContaining({
        from: expect.any(String),
        to: ['+15551234567'],
        body: 'Test message'
      }),
      expect.objectContaining({
        headers: expect.objectContaining({
          'Authorization': expect.stringContaining('Bearer')
        })
      })
    );
    expect(result).toEqual(mockResponse.data);
  });

  test('handles API error correctly', async () => {
    axios.post.mockRejectedValue({
      response: { status: 401, data: { error: 'Unauthorized' } }
    });

    await expect(sendSms('+15551234567', 'Test')).rejects.toThrow('Sinch API Error: 401');
  });
});

Integration Tests (Supertest Example):

javascript
// __tests__/api.test.js
const request = require('supertest');
const app = require('../index'); // Export app for testing

describe('POST /send-sms', () => {
  test('returns 400 for missing recipient', async () => {
    const response = await request(app)
      .post('/send-sms')
      .send({ message: 'Test' });

    expect(response.status).toBe(400);
    expect(response.body.error).toContain('recipient');
  });

  test('returns 400 for invalid E.164 format', async () => {
    const response = await request(app)
      .post('/send-sms')
      .send({ recipient: '5551234567', message: 'Test' });

    expect(response.status).toBe(400);
    expect(response.body.error).toContain('E.164');
  });
});

Add test script to package.json:

json
"scripts": {
  "start": "node index.js",
  "test": "jest"
}

Run tests:

bash
npm test

Frequently Asked Questions

How to send SMS with Node.js and Express?

Set up an Express server, install Axios and dotenv, create a POST route '/send-sms', and use Axios to send data to the Sinch SMS API. This route will handle incoming requests with the recipient's number and message body, then securely pass this information to the Sinch API to trigger the SMS delivery. Refer to the article's step-by-step guide for detailed instructions and code snippets.

What is the Sinch SMS REST API?

The Sinch SMS REST API is a web service that allows you to send and receive SMS messages programmatically. You interact with it by sending HTTP requests (typically POST) to specific endpoints, passing data like the recipient number and message content in JSON format. The API handles delivering the message via Sinch's SMS infrastructure.

Why use dotenv in a Node.js SMS project?

Dotenv helps secure sensitive credentials like Sinch API keys and service plan IDs. It loads these values from a '.env' file into process.env, keeping them separate from your codebase. This enhances security and avoids accidentally exposing credentials in version control.

When should I use a message queue for sending SMS?

For high-volume SMS sending, a message queue like RabbitMQ or Redis is recommended. It decouples API requests from the actual sending process, allowing the server to quickly accept requests and put them on the queue. Background workers then pull messages from the queue and send them via Sinch, improving performance and reliability under heavy load.

Can I send SMS to multiple recipients simultaneously?

Yes, the Sinch SMS API accepts an array of recipient numbers in the 'to' field of the request payload. This lets you send the same SMS message to multiple recipients with a single API call. However, ensure your Sinch account and number are provisioned to send to all intended destinations.

How to handle Sinch API errors in Node.js?

Use a try-catch block around your Axios calls to the Sinch API. Log detailed error information from the error.response, error.request, and error.message properties of the caught error object. Provide informative error messages to the client without exposing sensitive internal details. See the article for examples of enhanced logging and retry strategies.

What is the purpose of input validation in the SMS API?

Input validation is essential to prevent security vulnerabilities and API misuse. Check for missing or invalid fields, enforce correct number formats (E.164), sanitize input to prevent injection attacks, and validate message length to avoid issues. Use robust validation libraries like 'joi' or 'express-validator'.

How to implement rate limiting for my SMS endpoint?

Use the 'express-rate-limit' middleware to control how many requests a client can make within a specific timeframe. This prevents abuse, protects your Sinch budget, and ensures fair API access. Configure the windowMs and max properties to define the time window and request limits.

What is E.164 number format and why is it important?

E.164 is an international telephone number format that includes a '+' sign followed by the country code and the phone number. It's crucial for Sinch SMS because the API strictly requires numbers in this format. Always ensure all user-provided phone numbers are properly formatted before sending them to the API.

How can I view logs for sent SMS and Sinch API calls?

Use logging libraries like Winston or Pino. These tools provide structured logging, allowing you to record details such as timestamp, message, recipient, error codes, and API responses. Configure the logger to write logs to files or output them to the console, offering comprehensive records for debugging and monitoring.

How to secure Sinch API credentials in a Node.js application?

Store Sinch API credentials (SERVICE_PLAN_ID and API_TOKEN) in a .env file. Use the 'dotenv' package to load these variables into process.env at runtime. Never commit the .env file to version control (add it to .gitignore). For production, consider using more secure environment management solutions provided by your deployment platform.

What are common causes of 400 Bad Request errors from Sinch?

400 errors indicate an issue with the request sent to Sinch. Common causes include incorrect 'from' or 'to' number formatting, missing required fields in the request payload, or an invalid request structure. Check the detailed error message in the Sinch API response for more specific information.

How do I implement retry logic for Sinch API calls?

Use the axios-retry library to automatically retry failed Sinch API calls due to transient network issues or server errors. Configure it to retry only on specific error codes (e.g., 5xx) or network errors, and use exponential backoff to increase wait times between retries, improving reliability.

What should I include in the 'from' field when sending SMS with Sinch?

The 'from' field should contain your registered Sinch virtual number. Ensure this number is associated with your Sinch Service Plan ID and is provisioned for sending SMS. Use the E.164 number format for this field as well.