code examples

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

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

Learn how to send SMS messages using Sinch REST API with Node.js and Express. Step-by-step tutorial covering authentication, API integration, error handling, and production best practices.

Build a production-ready Node.js application that sends SMS messages via the Sinch SMS REST API using Express. This comprehensive tutorial covers everything from initial setup and credential management to error handling and deployment strategies.

Last Updated: January 2025

What You'll Learn: Building a Sinch SMS API Integration

What You'll Build:

Create a minimal but functional Express API with a single endpoint (/send-sms). This endpoint accepts POST requests containing a recipient phone number and message body, then uses the Sinch SMS API to send the message.

Common Use Cases for SMS Integration:

This guide enables developers to quickly integrate SMS sending capabilities into their Node.js applications for various purposes, such as:

  • User notifications and alerts
  • Appointment reminders
  • Two-factor authentication (basic trigger, not full verification flow)
  • Marketing messages (ensure compliance with regulations)

Technologies Used:

  • Node.js: A JavaScript runtime environment for building server-side applications.
  • Express: A minimal and flexible Node.js web application framework used to create the API endpoint.
  • Sinch SMS REST API: The third-party service used to dispatch the SMS messages. We'll use its simple /batches endpoint.
  • axios: A promise-based HTTP client for Node.js to make requests to the Sinch API.
  • dotenv: A module to load environment variables from a .env file, keeping sensitive credentials secure.

Note on Implementation Approach: This guide demonstrates direct REST API calls using axios. Sinch also provides an official Node.js SDK (@sinch/sdk-core v1.2.1+) that simplifies authentication and API interactions. For production applications, consider using the SDK for better type safety and maintenance. This tutorial focuses on the REST API approach to provide deeper understanding of the underlying HTTP mechanics.

Architecture Diagram:

text
+-----------+       +-----------------------+       +-----------------+       +-----------------+
|  Client   | ----> | Node.js/Express API | ----> |   Sinch SMS API | ----> | SMS Recipient   |
| (e.g. curl|       |   (POST /send-sms)    |       |   (/batches)    |       | (Mobile Phone)  |
|  Postman) |       +-----------------------+       +-----------------+       +-----------------+
+-----------+
        |                 |
        | Uses            | Reads Credentials
        | HTTP Request    | from .env
        +-----------------+

Prerequisites:

  • Node.js and npm (or yarn): Installed on your system. You can download them from nodejs.org. Verify installation with node -v and npm -v.
    • Recommended: Node.js 20 LTS or later (Node.js 18 support ends May 2025)
  • Sinch Account: A registered account on the Sinch Customer Dashboard.
  • Basic Terminal/Command Line Knowledge: Familiarity with navigating directories and running commands.
  • Text Editor or IDE: Such as VS Code, Sublime Text, or WebStorm.

Final Outcome:

At the end of this guide, you'll have a running Node.js Express server that accepts API requests to send SMS messages using your Sinch account credentials.


1. Setting up the Project

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

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

    bash
    mkdir sinch-sms-sender
    cd sinch-sms-sender
  2. Initialize Node.js Project: Initialize the project using npm. The -y flag accepts the default settings.

    bash
    npm init -y

    This creates a package.json file in your directory.

  3. Install Dependencies: Install Express for the server, axios for making HTTP requests, and dotenv for environment variable management.

    bash
    npm install express axios dotenv

    (Note: We will install express-rate-limit later when it's introduced in Section 7.)

  4. Project Structure: Create the necessary files. Your basic structure should look like this:

    text
    sinch-sms-sender/
    ├── node_modules/
    ├── .env
    ├── index.js
    └── package.json
    • .env: Stores sensitive credentials (API keys, phone numbers). Crucially, add .env to your .gitignore file later to prevent committing secrets.
    • index.js: Contains our Express application logic.
    • package.json: Defines project metadata and dependencies.
  5. Create .gitignore: Create a file named .gitignore in the root of your project and add the following lines to prevent committing sensitive information and dependency folders:

    text
    # .gitignore
    
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # Optional Operating System files
    .DS_Store
    Thumbs.db

2. Sinch Account Setup and API Credentials

Before writing code, you need to retrieve necessary credentials and information from your Sinch account.

  1. Log in to Sinch: Access the Sinch Customer Dashboard.

  2. Navigate to SMS API Settings: In the left-hand menu, go to SMS -> APIs.

  3. Retrieve SERVICE_PLAN_ID and API_TOKEN:

    • Under the REST API section, you will find your Service plan ID. Copy this value.
    • You will also see an API token. Click Show to reveal it, then copy the value. Treat this token like a password – keep it secure.
  4. Retrieve Your Sinch Number:

    • You need a virtual phone number associated with your Service Plan ID to send messages from.
    • If you don't have one, you may need to acquire one through the Sinch dashboard (Numbers section, ensure it's configured for SMS).
    • To find numbers associated with your plan, click on your Service plan ID link on the SMS -> APIs page. Scroll down to find associated numbers or details on how to assign one. Copy the number in E.164 format (e.g., +12025550184).
  5. Determine Your Region:

    • Sinch operates in different regions (e.g., US, EU, AU, CA, BR). The API endpoint URL depends on your account's region.
    • Check your dashboard or account settings. Common base URLs are:
      • US (United States): https://us.sms.api.sinch.com
      • EU (Europe): https://eu.sms.api.sinch.com
      • AU (Australia): https://au.sms.api.sinch.com
      • CA (Canada): https://ca.sms.api.sinch.com
      • BR (Brazil): https://br.sms.api.sinch.com
    • Note the correct base URL for your region. The API endpoint we'll use is /xms/v1/{SERVICE_PLAN_ID}/batches.
    • Important: Using the wrong regional endpoint will result in authentication or routing errors. Verify your region in the Sinch Dashboard under SMS → APIs.
  6. Configure Environment Variables: Open the .env file you created earlier and add your Sinch credentials and configuration. Replace the placeholder values with your actual details.

    dotenv
    # .env
    # Sinch Credentials
    SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID_HERE
    SINCH_API_TOKEN=YOUR_API_TOKEN_HERE
    SINCH_NUMBER=+YOUR_SINCH_PHONE_NUMBER_HERE # Use E.164 format (e.g., +12025550184)
    
    # Sinch API Configuration
    SINCH_REGION_BASE_URL=https://us.sms.api.sinch.com # Adjust if your region is different (e.g., eu)
    
    # Application Port
    PORT=3000
    • SINCH_SERVICE_PLAN_ID: Your service plan ID from the dashboard.
    • SINCH_API_TOKEN: Your API token from the dashboard.
    • SINCH_NUMBER: The Sinch virtual number you'll send messages from.
    • SINCH_REGION_BASE_URL: The base URL for your account's region.
    • PORT: The port your Express server will listen on.

3. Building the Express Server with SMS Functionality

Now, let's set up the basic Express server structure in index.js.

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

// --- Environment Variable Validation (Basic) ---
// Ensure essential Sinch variables are loaded
if (!process.env.SINCH_SERVICE_PLAN_ID || !process.env.SINCH_API_TOKEN || !process.env.SINCH_NUMBER || !process.env.SINCH_REGION_BASE_URL) {
    // Use single quotes for the error message string for consistency
    console.error('FATAL ERROR: Missing required Sinch environment variables. Check your .env file.');
    process.exit(1); // Exit if essential config is missing
}

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_URL = `${process.env.SINCH_REGION_BASE_URL}/xms/v1/${SERVICE_PLAN_ID}/batches`;
const PORT = process.env.PORT || 3000; // Default to 3000 if not set

// --- Express App Setup ---
const app = express();
app.use(express.json()); // Middleware to parse JSON request bodies

// --- Health Check Endpoint ---
app.get('/', (req, res) => {
    res.status(200).json({ status: 'OK', message: 'Sinch SMS Sender API is running.' });
});

// --- SMS Sending Endpoint (To be implemented next) ---
// app.post('/send-sms', ...)

// --- Start Server ---
// Assign the server instance returned by app.listen to a variable
const server = app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

// --- Graceful Shutdown (Optional but Recommended) ---
process.on('SIGTERM', () => {
    console.info('SIGTERM signal received: closing HTTP server');
    // Now 'server.close()' works correctly
    server.close(() => {
        console.log('HTTP server closed');
        // Add any other cleanup logic here before exiting
        process.exit(0);
    });
});

process.on('SIGINT', () => { // Handle Ctrl+C gracefully as well
    console.info('SIGINT signal received: closing HTTP server');
    server.close(() => {
        console.log('HTTP server closed');
        process.exit(0);
    });
});

Explanation:

  1. require('dotenv').config();: Loads variables from the .env file into process.env. This must be called early.
  2. Validation: We perform a basic check to ensure critical environment variables are present. In a production app, more robust validation (e.g., using Joi or Zod) is recommended.
  3. Constants: We store the environment variables and construct the full Sinch API endpoint URL.
  4. app = express();: Creates an Express application instance.
  5. app.use(express.json());: Enables the server to automatically parse incoming JSON request bodies, making req.body available.
  6. Health Check: A simple / GET route confirms the server is running.
  7. const server = app.listen(): Starts the server, makes it listen for connections on the specified PORT, and assigns the server instance to the server variable.
  8. Graceful Shutdown: Basic handling for SIGTERM and SIGINT (Ctrl+C) signals is included, using the server variable to properly close connections before exiting.

You can run the server now to test the basic setup:

bash
node index.js

You should see Server running on http://localhost:3000. Accessing http://localhost:3000 in your browser should show the health check JSON response. Press Ctrl+C to stop the server (which should now trigger the graceful shutdown log).


4. Implementing the SMS Sending Logic

Let's create the function responsible for interacting with the Sinch API.

Add the following function before the // --- Express App Setup --- section in index.js:

javascript
// index.js
// ... (require statements and environment variable validation) ...

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_URL = `${process.env.SINCH_REGION_BASE_URL}/xms/v1/${SERVICE_PLAN_ID}/batches`;
const PORT = process.env.PORT || 3000;

// --- Sinch SMS Sending Function ---
async function sendSinchSms(recipientNumber, messageBody) {
    console.log(`Attempting to send SMS to: ${recipientNumber}`); // Basic logging

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

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

    try {
        const response = await axios.post(SINCH_API_URL, payload, config);
        console.log('Sinch API Response Status:', response.status);
        // Pretty print JSON for console readability; consider structured logging for production
        console.log('Sinch API Response Data:', JSON.stringify(response.data, null, 2));

        // Basic success check (Sinch usually returns 201 Created for successful batch submissions)
        if (response.status === 200 || response.status === 201) {
             // The batch ID can be used for tracking if needed
            const batchId = response.data?.id;
            console.log(`SMS batch submitted successfully. Batch ID: ${batchId}`);
            return { success: true, batchId: batchId, details: response.data };
        } else {
            // Handle unexpected success statuses if necessary
            console.warn(`Unexpected success status from Sinch: ${response.status}`);
            return { success: false, error: 'Unexpected status from Sinch API', details: response.data };
        }
    } catch (error) {
        console.error('Error sending SMS via Sinch:', error.message);
        // Log more detailed error info if available (e.g., response from Sinch)
        if (error.response) {
            console.error('Sinch Error Response Status:', error.response.status);
            // Pretty print JSON for console readability; consider structured logging for production
            console.error('Sinch Error Response Data:', JSON.stringify(error.response.data, null, 2));
            // Extract specific Sinch error details if possible
            const sinchError = error.response.data?.request_error?.service_exception?.text || 'Unknown Sinch error';
             return { success: false, error: `Sinch API Error: ${sinchError}`, status: error.response.status, details: error.response.data };
        } else if (error.request) {
             // Request was made but no response received
            console.error('Sinch Error Request:', error.request);
            return { success: false, error: 'No response received from Sinch API' };
        } else {
            // Something else happened in setting up the request
             return { success: false, error: `Client setup error: ${error.message}` };
        }
    }
}

// --- Express App Setup ---
// ... (rest of the code from section 3) ...

Explanation:

  1. async function sendSinchSms: Defines an asynchronous function to handle the SMS sending logic, making it easier to use await with the axios promise.
  2. payload: Constructs the JSON body required by the Sinch /batches endpoint.
    • from: Your Sinch virtual number (loaded from .env).
    • to: An array containing the recipient's phone number(s) in E.164 format.
    • body: The text message content.
  3. config: Defines the request configuration for axios.
    • headers: Sets the necessary Authorization header using the Bearer scheme with your API_TOKEN and the Content-Type header.
  4. axios.post: Sends the POST request to the SINCH_API_URL with the payload and config.
  5. Success Handling: Checks if the response status is 200 or 201 (Created), logs success, and returns a success object containing the batchId from the response.
  6. Error Handling (catch block):
    • Logs the error message.
    • Checks if the error includes a response from Sinch (error.response). If so, logs the status and data from Sinch's error response, providing more specific details. It attempts to extract a meaningful error message from the nested Sinch response structure.
    • Checks for request errors where no response was received (error.request).
    • Handles other setup errors.
    • Returns a standardized error object { success: false, error: '...' }.
    • (Note on Logging: JSON.stringify(..., null, 2) is used for readable console output during development. For production, consider structured logging, e.g., plain JSON objects, for better machine parsing.)

5. Creating the /send-sms API Endpoint

Now, let's create the Express endpoint that will use the sendSinchSms function.

Add the following route definition inside index.js, replacing the placeholder comment // --- SMS Sending Endpoint (To be implemented next) ---:

javascript
// index.js
// ... (require statements, validation, constants, sendSinchSms function) ...

// --- Express App Setup ---
const app = express();
app.use(express.json());

// --- Health Check Endpoint ---
app.get('/', (req, res) => {
    res.status(200).json({ status: 'OK', message: 'Sinch SMS Sender API is running.' });
});

// --- SMS Sending Endpoint ---
app.post('/send-sms', async (req, res) => {
    const { recipient, message } = req.body; // Destructure from request body

    // --- Basic Input Validation ---
    if (!recipient || !message) {
        console.warn('Validation Error: Missing recipient or message in request body.');
        return res.status(400).json({ success: false, error: 'Missing required fields: recipient, message' });
    }

    // Basic E.164 format check (very simple, consider a library for production)
    if (!/^\+\d{1,15}$/.test(recipient)) {
         console.warn(`Validation Error: Invalid recipient format: ${recipient}`);
        return res.status(400).json({ success: false, error: 'Invalid recipient phone number format. Use E.164 (e.g., +12025550184).' });
    }

    // --- Call the Sending Logic ---
    try {
        const result = await sendSinchSms(recipient, message);

        if (result.success) {
            res.status(202).json({ // 202 Accepted: Request accepted, processing underway
                success: true,
                message: 'SMS batch submitted successfully to Sinch.',
                batchId: result.batchId,
                // Avoid sending back full Sinch details unless needed by client
                // sinchDetails: result.details
            });
        } else {
            // Determine appropriate status code based on Sinch error if possible
            const statusCode = result.status || 500; // Default to 500 if specific status unknown
            console.error(`Failed to send SMS: ${result.error}`);
             res.status(statusCode).json({
                success: false,
                error: result.error,
                 // Optionally include Sinch error details during development/debugging
                 // sinchDetails: result.details
            });
        }
    } catch (internalError) {
        // Catch unexpected errors within the endpoint handler itself
        console.error('Internal Server Error in /send-sms handler:', internalError);
        res.status(500).json({ success: false, error: 'Internal Server Error' });
    }
});


// --- Start Server ---
// ... (server assignment, app.listen and graceful shutdown) ...

Explanation:

  1. app.post('/send-sms', ...): Defines a route that listens for POST requests on the /send-sms path.
  2. const { recipient, message } = req.body;: Extracts the recipient phone number and the message text from the JSON payload sent in the request body.
  3. Input Validation:
    • Checks if both recipient and message are provided. Returns a 400 Bad Request error if not.
    • Performs a very basic regular expression check to see if the recipient looks like E.164 format (starts with +, followed by digits). For production, use a dedicated phone number parsing/validation library (like libphonenumber-js).
  4. Call sendSinchSms: Calls the asynchronous function we created earlier, passing the validated recipient and message. Uses await to wait for the result.
  5. Handle Result:
    • If result.success is true, sends a 202 Accepted response back to the client, indicating the request was received and passed to Sinch. Includes the batchId.
    • If result.success is false, logs the error and sends an appropriate error response (e.g., 500 Internal Server Error, or potentially a more specific code like 400 if the error from Sinch indicated a client-side problem like an invalid number format not caught by basic validation).
  6. Internal Error Catch: A top-level try...catch within the route handler catches any unexpected errors during request processing.

Testing the Endpoint with curl:

  1. Start the server: node index.js

  2. Open another terminal and run the following curl command. Important: Replace +1xxxxxxxxxx with a valid recipient phone number in E.164 format (your own mobile is fine for testing). Also, ensure the SINCH_NUMBER in your .env file is correctly set to your provisioned Sinch number (e.g., +YOUR_SINCH_NUMBER_HERE).

    bash
    curl -X POST http://localhost:3000/send-sms \
    -H "Content-Type: application/json" \
    -d '{
        "recipient": "+1xxxxxxxxxx",
        "message": "Hello from Node.js and Sinch!"
    }'
    • -X POST: Specifies the HTTP method.
    • http://localhost:3000/send-sms: The URL of your endpoint.
    • -H "Content-Type: application/json": Sets the request header. Note the correct quoting for the header value.
    • -d '{...}': Provides the JSON data payload. Note the correct quoting for JSON keys and string values. (Note: The multi-line format with \ works in Unix-like shells like bash or zsh. For Windows Command Prompt, you might need to put the entire command on one line or handle line breaks differently.)

Expected Success Response (Terminal running curl):

json
{
  "success": true,
  "message": "SMS batch submitted successfully to Sinch.",
  "batchId": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
}

(Example Batch ID shown)

Expected Server Logs (Terminal running node index.js):

text
Server running on http://localhost:3000
Attempting to send SMS to: +1xxxxxxxxxx
Sinch API Response Status: 201
Sinch API Response Data: {
  "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "to": [
    "+1xxxxxxxxxx"
  ],
  "from": "+YOUR_SINCH_NUMBER_HERE",
  "canceled": false,
  "body": "Hello from Node.js and Sinch!",
  "type": "mt_batch",
  "created_at": "2023-10-26T10:00:00.123Z",
  "modified_at": "2023-10-26T10:00:00.123Z",
  "delivery_report": "none",
  "send_at": null,
  "expire_at": null,
  "flash_message": false,
  "feedback_enabled": false
}
SMS batch submitted successfully. Batch ID: 01ARZ3NDEKTSV4RRFFQ69G5FAV

(Example timestamps and Batch ID shown; from number should match your .env)

You should also receive the SMS on the recipient phone shortly after.


6. Error Handling and Logging Best Practices

We've implemented basic error handling, but let's refine it slightly.

  • Consistent Error Format: Our API endpoint and the sendSinchSms function return consistent { success: false, error: '...' } objects. This is good practice.
  • Logging: We are using console.log and console.error. For production, consider using a more robust logging library like winston or pino. These enable:
    • Different log levels (debug, info, warn, error).
    • Structured logging (JSON format for easier parsing by log aggregators).
    • Outputting logs to files or external services.
  • Sinch API Errors: The sendSinchSms function attempts to parse specific errors from the Sinch response (error.response.data). You can enhance this by checking specific error.response.status codes (e.g., 400 for bad request, 401/403 for auth issues, 429 for rate limiting) and returning more specific errors or potentially implementing retries for transient issues (like 5xx errors from Sinch, although retries are complex and not implemented here).

Example: Testing an Error Scenario

  1. Stop the server (Ctrl+C).
  2. Temporarily change the SINCH_API_TOKEN in your .env file to an invalid value (e.g., add INVALID at the end).
  3. Restart the server: node index.js
  4. Run the curl command again (using a valid recipient number).

Expected curl Response:

json
{
  "success": false,
  "error": "Sinch API Error: Invalid credentials or service plan ID"
}

(Or similar message based on Sinch's exact response)

Expected Server Logs:

text
Server running on http://localhost:3000
Attempting to send SMS to: +1xxxxxxxxxx
Error sending SMS via Sinch: Request failed with status code 401
Sinch Error Response Status: 401
Sinch Error Response Data: {
  "request_error": {
    "service_exception": {
      "message_id": "AUTH1001",
      "text": "Invalid credentials or service plan ID",
      "variables": []
    }
  }
}
Failed to send SMS: Sinch API Error: Invalid credentials or service plan ID

(Example Sinch 401 Error shown)

Reminder: Don't forget to change the SINCH_API_TOKEN back to its correct value in your .env file after completing this test!


7. Security Best Practices for SMS APIs

While this is a basic guide, keep these security points in mind for real applications:

  1. Credential Management: Never hardcode API keys, tokens, or phone numbers directly in your source code. Always use environment variables (.env locally, secure configuration management in deployment). Ensure .env is in your .gitignore.

  2. Input Validation: We added basic validation for recipient and message. Robust validation is crucial to prevent injection attacks or malformed requests causing errors. Use libraries like joi, zod, or express-validator. Sanitize message content if necessary, depending on how it's used.

  3. Rate Limiting: Protect your API endpoint from abuse (accidental or malicious) by implementing rate limiting. Libraries like express-rate-limit make this straightforward.

    First, install the dependency:

    bash
    npm install express-rate-limit

    Then, add the rate limiting middleware in index.js:

    javascript
    // index.js
    // ... require statements ...
    const rateLimit = require('express-rate-limit'); // Require the package
    
    // ... environment variable validation, constants, sendSinchSms function ...
    
    const app = express();
    app.use(express.json());
    
    // 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 windowMs
        message: 'Too many requests sent from this IP, please 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 specifically to the /send-sms route
    
    // ... Health Check Endpoint, SMS Sending Endpoint ...
    // ... Start Server, Graceful Shutdown ...
  4. Authentication/Authorization: This example API is open. In a real application, you would secure the /send-sms endpoint so only authorized clients or users can trigger SMS sends. This typically involves API keys, JWT tokens, or session management.

  5. HTTPS: Always use HTTPS in production to encrypt traffic between the client and your server. Deployment platforms often handle this via load balancers or reverse proxies.


8. Special Cases and International SMS

Consider these points for expansion or specific scenarios:

  • Character Encoding/Limits: Standard SMS messages have character limits (160 for GSM-7 encoding, 70 for UCS-2). Longer messages might be split into multiple segments or require concatenation support (Sinch handles some of this automatically). Be mindful of message length to manage costs and user experience. Check Sinch documentation for details on encoding and concatenation.
  • Internationalization: Ensure recipient numbers are always provided in E.164 format (+ followed by country code and number). Be aware of varying regulations, sender ID requirements, and potential filtering regarding SMS sending in different countries.

9. Performance Optimization Strategies

For applications sending a higher volume of messages:

  • Asynchronous Operations: Node.js is inherently non-blocking due to its event loop. Using async/await correctly, as shown in the sendSinchSms function and the endpoint handler, ensures the server remains responsive while waiting for the network request to Sinch to complete.
  • Queuing: For high-throughput scenarios, avoid calling the Sinch API directly within the HTTP request handler. Instead, push the message details (recipient, body) onto a message queue (e.g., RabbitMQ, Redis Queue, AWS SQS, Google Cloud Pub/Sub). Have separate worker processes consume messages from the queue and make the calls to the Sinch API. This decouples the API response time from the actual SMS sending process, improves fault tolerance (e.g., if Sinch is temporarily unavailable), and helps manage Sinch API rate limits more gracefully.

10. Monitoring & Observability

For production applications, implement monitoring:

  • Logging: As mentioned, use a structured logging library (winston, pino) and configure appropriate log levels and destinations (files, log aggregation services). Log key events like request received, validation success/failure, Sinch API call attempt, Sinch API response (success/error), and final response sent to the client. Include correlation IDs (e.g., the Sinch batchId) to trace requests.
  • Health Checks: Our / route is a basic liveness check. Consider adding a readiness check that verifies essential dependencies are available (e.g., perhaps by making a lightweight, non-sending call to Sinch if available, or checking database connectivity if used).
  • Metrics: Track key performance indicators (KPIs) for your service and the /send-sms endpoint:
    • Request count
    • Request latency (average, percentiles)
    • Error rates (total, per error type like 4xx, 5xx)
    • Sinch API call latency and error rates. Use tools like Prometheus/Grafana or Datadog APM.
  • Error Tracking: Integrate an error tracking service (e.g., Sentry, Datadog Error Tracking, Rollbar) to capture, aggregate, and alert on runtime exceptions and unhandled errors in your Node.js application.
  • Sinch Dashboard: Regularly monitor your SMS delivery status, usage statistics, and billing information within the Sinch Customer Dashboard. Use the batchId logged by the application to correlate specific requests with Sinch's delivery reports.

11. Common Issues and Troubleshooting

  • Error 401/403 (Unauthorized/Forbidden):
    • Double-check SINCH_SERVICE_PLAN_ID and SINCH_API_TOKEN in your environment variables are correct and exactly match your dashboard values.
    • Ensure the Authorization: Bearer YOUR_API_TOKEN header is being correctly constructed and sent by axios.
    • Verify your Sinch account/plan is active and explicitly enabled for sending SMS messages via the REST API.
    • Confirm you are using the correct regional base URL (SINCH_REGION_BASE_URL) for your account.
  • Error 400 (Bad Request):
    • Check the recipient number format. It must be in E.164 format (+ followed by country code and number, with no spaces, dashes, or other characters).
    • Ensure the from number (SINCH_NUMBER in .env) is a valid Sinch virtual number associated with your service plan and is also correctly formatted in E.164.
    • Verify the request body structure sent to Sinch exactly matches their API specification: { "from": "...", "to": ["..."], "body": "..." }. Pay close attention to to being an array. Check that the Content-Type: application/json header is correctly set on the request to Sinch.
    • The message body might be empty, invalid (e.g., containing unsupported characters if not using appropriate encoding), or exceed length limits.
    • Examine the detailed error message provided within the Sinch error response (error.response.data in the server logs) for specific clues (e.g., INVALID_PARAMETER_VALUE, MISSING_PARAMETER).
  • SMS Not Received:
    • Verify the recipient number is absolutely correct and the device is active and capable of receiving SMS.
    • Check for potential carrier filtering or spam blocks on the recipient's network or device. This is often outside your direct control.
    • Use the batchId returned by the API and logged by your server to look up the specific message status and delivery reports in the Sinch Customer Dashboard. This is the most reliable way to diagnose delivery issues.
    • Ensure your Sinch account has sufficient balance or credit if using a paid plan. Trial accounts may have restrictions.
  • Rate Limits (Error 429): Sinch enforces API rate limits. If you send too many requests in a short period, you might receive 429 Too Many Requests errors from their API. Your own express-rate-limit middleware can also return 429 if the client exceeds the limits you set. Implement appropriate delays between requests or use a queuing system (see Section 9) for high-volume sending.
  • Costs: Sending SMS messages typically incurs costs based on destination country, message volume, and potentially message segments (for long messages). Monitor your usage and billing carefully in the Sinch dashboard.

12. Deployment and CI/CD (Conceptual)

Deploying this Node.js application typically involves these steps:

  1. Choose a Hosting Provider: Select a platform suitable for Node.js applications, such as Heroku, Render, AWS (EC2, Elastic Beanstalk, Lambda, Fargate), Google Cloud (App Engine, Cloud Run, GKE), Azure (App Service, AKS), or others (DigitalOcean App Platform, Fly.io).
  2. Configure Environment Variables: Securely configure the production environment variables (SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_NUMBER, SINCH_REGION_BASE_URL, PORT, NODE_ENV=production) within your chosen hosting platform's configuration management system. Crucially, do not commit your .env file or hardcode secrets in your repository.
  3. Build Step (If Necessary): For this simple JavaScript application, a dedicated build step is likely not required. If using TypeScript or a bundler, you would add a build process (e.g., npm run build) to compile/bundle your code before deployment.
  4. Run the Application: Configure the hosting platform to start your application using a command like npm start (if defined in package.json) or node index.js. Use a process manager like pm2 (if managing your own server/VM) or rely on the platform's built-in mechanisms (like Heroku dynos or container orchestration) to run the application reliably, monitor its health, and restart it if it crashes.
  5. CI/CD Pipeline (Recommended): Automate the testing and deployment process using CI/CD tools like GitHub Actions, GitLab CI, Jenkins, CircleCI, etc. A typical pipeline would:
    • Trigger on code pushes/merges.
    • Install dependencies (npm ci).
    • Lint the code (npm run lint, if configured).
    • Run automated tests (unit, integration - npm test, if configured).
    • Build the application (if necessary).
    • Deploy the application artifact to a staging environment for further testing.
    • Deploy to the production environment (potentially after manual approval).
  6. HTTPS: Ensure your deployment setup provides HTTPS termination. Most PaaS providers (Heroku, Render) and load balancers/reverse proxies (like Nginx or those provided by cloud platforms) handle SSL certificate management and terminate HTTPS connections, forwarding plain HTTP traffic to your Node.js application listening on its configured PORT.

13. Testing Your SMS Integration

  • Manual Verification Checklist:
    • Start the server locally (node index.js).
    • Send a valid POST request using curl or a tool like Postman to http://localhost:3000/send-sms with a correct recipient E.164 number and message body.
    • Verify a 202 Accepted success response (with success: true and a batchId) is received by the client.
    • Check the server console logs for messages indicating successful submission to Sinch, including the batchId.
    • Confirm the actual SMS message arrives on the recipient's mobile device.
    • Send requests with missing fields (e.g., no recipient or no message) and verify appropriate 400 Bad Request error responses are received.
    • Send a request with an invalid recipient number format (e.g., 12345) and verify a 400 Bad Request error response related to the format is received.
    • Temporarily invalidate credentials in the .env file (e.g., modify SINCH_API_TOKEN), restart the server, send a valid request, and verify an appropriate error response (likely 401 Unauthorized or similar, based on the logged Sinch error) is returned by the API. Remember to restore the correct credentials afterwards.
  • Automated Testing (Conceptual):
    • Unit Tests: Use a testing framework like Jest or Mocha/Chai to test individual functions in isolation. For sendSinchSms, mock the axios.post call to simulate successful and various error responses from the Sinch API without making actual network requests. Verify that the function handles responses correctly and returns the expected { success: ..., ... } object.
    • Integration Tests: Use a library like supertest to make HTTP requests to your running Express application (potentially in a test environment with mocked dependencies). Test the /send-sms endpoint:
      • Send valid requests and assert the correct status code (202) and response body structure.
      • Send invalid requests (missing fields, bad format) and assert the correct error status codes (400) and error messages.
      • Test the rate limiting middleware (if implemented) by sending multiple requests quickly.
      • Mock the sendSinchSms function within these tests to control its behavior and prevent actual calls to Sinch during testing.
    • End-to-End Tests (Use Sparingly): Tests that actually send an SMS via a dedicated test Sinch account and verify reception (e.g., using a Sinch test number or a dedicated test phone). These are more complex, potentially costly, and slower, so use them judiciously for critical paths.

Frequently Asked Questions About Sinch SMS API

How do I get my Sinch Service Plan ID and API Token?

Log in to the Sinch Dashboard and navigate to SMS → APIs. Your Service Plan ID appears in the SERVICE PLAN ID column. Click "Show" in the API TOKEN column to reveal your API token. Treat this token as a password and never commit it to version control.

Which Sinch regional endpoint should I use?

The regional endpoint depends on where your Sinch account is provisioned. Check your Sinch Dashboard under SMS → APIs to verify your region. Common endpoints include https://us.sms.api.sinch.com (United States), https://eu.sms.api.sinch.com (Europe), https://au.sms.api.sinch.com (Australia), https://ca.sms.api.sinch.com (Canada), and https://br.sms.api.sinch.com (Brazil). Using the wrong endpoint will cause authentication errors.

Should I use the Sinch Node.js SDK or the REST API directly?

The official Sinch Node.js SDK (@sinch/sdk-core v1.2.1+) provides better type safety, simplified authentication, and easier maintenance for production applications. This tutorial uses direct REST API calls with axios to demonstrate the underlying HTTP mechanics, which helps you understand how the API works. For production, consider the SDK for reduced boilerplate and built-in error handling.

What phone number format does Sinch require?

Sinch requires all phone numbers in E.164 format: a plus sign (+) followed by the country code and number with no spaces, dashes, or parentheses. For example: +12025550184 (US) or +442071234567 (UK). The basic regex /^\+\d{1,15}$/ validates this format, but use a library like libphonenumber-js for production validation.

How do I handle Sinch API errors in Node.js?

Sinch returns detailed error information in the response body. Check error.response.status for HTTP status codes (401 for auth issues, 400 for bad requests, 429 for rate limits) and error.response.data.request_error.service_exception.text for specific error messages. Log these details for debugging and return appropriate error responses to your API clients.

What is the Sinch SMS API rate limit?

Sinch enforces rate limits that vary by account type and region. Implement client-side rate limiting using express-rate-limit to protect your API. For high-volume applications, use a message queue (RabbitMQ, Redis, AWS SQS) to decouple API requests from Sinch API calls and manage throughput more effectively.

How much does sending SMS with Sinch cost?

SMS costs vary by destination country, message segments (160 characters for GSM-7 encoding, 70 for Unicode), and your Sinch plan. Monitor usage and billing in the Sinch Dashboard. Longer messages may be split into multiple segments, increasing costs. Check Sinch's pricing page for current rates by country.

Can I send SMS to international numbers with Sinch?

Yes, Sinch supports international SMS delivery to most countries. Ensure recipient numbers are in E.164 format with the correct country code. Be aware of country-specific regulations, potential carrier filtering, and varying delivery rates. Some countries require sender ID registration or have restrictions on commercial messaging.

Frequently Asked Questions

How to send SMS with Node.js and Express?

This guide provides a step-by-step process to send SMS messages using Node.js with the Express framework and the Sinch SMS REST API. It covers setting up a project, acquiring Sinch credentials, building an API endpoint, handling errors, and implementing security best practices like rate limiting.

What is the Sinch SMS REST API?

The Sinch SMS REST API is a third-party service used to send SMS messages programmatically. This guide uses Sinch's /batches endpoint to send messages and provides instructions on retrieving the necessary API credentials from the Sinch dashboard.

Why use dotenv in a Node.js project?

Dotenv is used to load environment variables from a .env file. This helps securely manage sensitive credentials like API keys and tokens, keeping them separate from your codebase and preventing accidental exposure in version control.

When should I implement rate limiting?

Rate limiting is crucial for protecting your API from abuse. It prevents excessive requests from a single IP address, which could lead to service disruptions or increased costs. This guide recommends using the express-rate-limit library for this purpose.

Can I send SMS messages internationally using this setup?

Yes, you can send international SMS messages. Ensure recipient numbers are in E.164 format (+[country code][number]). Be aware of international SMS regulations and potential variations in cost and deliverability.

How to set up Sinch account credentials for Node.js SMS?

Log into your Sinch dashboard, navigate to SMS -> APIs to find your Service plan ID and API token. Retrieve or provision a Sinch virtual number, note your Sinch region base URL, and store these values securely in a .env file.

What is the purpose of a health check endpoint?

The health check endpoint, typically at the root path ('/'), allows you to quickly confirm if the server is running and responding. This is helpful for monitoring and ensuring basic functionality is available.

Why use axios for sending SMS messages?

Axios is a promise-based HTTP client for Node.js. It simplifies making HTTP requests to external APIs like the Sinch SMS API, providing clear error handling and support for asynchronous operations using async/await.

How to handle errors when sending SMS with Sinch?

The provided example code includes detailed error handling using try-catch blocks. It catches errors during the API call, logs relevant information, including any response from Sinch, and returns consistent error objects to the client.

When to use a message queue for SMS sending?

For high-throughput scenarios where sending many SMS messages is necessary, a message queue is recommended. This decouples message processing from the API request/response cycle, improving performance, scalability, and fault tolerance.

How to validate recipient phone numbers?

While the example uses a basic regex, it's strongly recommended to use a dedicated phone number validation library like libphonenumber-js for production to ensure accurate and robust validation according to international standards.

What are some security best practices for sending SMS?

Key security practices include never hardcoding API keys, using environment variables and .gitignore, implementing robust input validation, using rate limiting, and securing the endpoint with proper authentication/authorization in production.

How do I troubleshoot Sinch SMS delivery failures?

Check Sinch's dashboard for detailed delivery reports correlated with the batchId returned by the API. Also, verify the recipient number is correct and consider potential carrier filtering or spam blocks at the recipient's end.

How can I improve SMS sending performance with Node.js?

Leverage Node.js's asynchronous nature using async/await, and for high volumes, consider using a message queue to handle sending asynchronously with worker processes to maximize throughput and reliability.