code examples
code examples
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
.envfiles, keeping credentials secure. - Sinch SMS REST API: API for sending and receiving SMS messages.
System Architecture:
+-------------+ +-----------------------+ +-----------------+ +-----------------+
| 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:
| Pattern | Use Case | Trade-offs |
|---|---|---|
| Direct API calls (this guide) | Low–medium volume, simple integration | Limited scalability, no persistence |
| Queue-based (Redis/RabbitMQ) | High volume, async processing | Added complexity, requires queue infrastructure |
| Serverless (Lambda/Cloud Functions) | Variable traffic, cost optimization | Cold 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_IDandAPI_TOKENfrom 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.
mkdir sinch-sms-express
cd sinch-sms-express2. Initialize Node.js Project:
Initialize the project using npm. The -y flag accepts default settings.
npm init -yThis 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.
npm install express axios dotenvexpress: 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:
"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.
touch index.js .env .gitignoreindex.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.envandnode_modules).
5. Configure .gitignore:
Open .gitignore and add these lines to prevent committing sensitive data and unnecessary files:
# .env file
.env
# Node modules
node_modules/
# Log files
*.log
# OS generated files
.DS_Store
Thumbs.dbThis protects your credentials and keeps your repository clean.
6. Create Basic Express Server:
Open index.js and set up a minimal Express server:
// 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.
node index.jsOpen 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:
// 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, andSINCH_REGIONfrom environment variables usingprocess.env. - Region: The Sinch API endpoint URL depends on your account's region (e.g.,
us,eu). Defaults tousbut can be overridden viaSINCH_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: UsesBearerauthentication with yourAPI_TOKEN.Content-Type: Must beapplication/json.
- Timeout: Sets a 10-second timeout to prevent hanging requests.
- Error Handling: The
try...catchblock 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:
// 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
POSTroute at/send-sms. - Body Parsing:
express.json()middleware (added earlier) automatically parses the incoming JSON request body intoreq.body. - Input Extraction: Destructures
recipientandmessagefromreq.body. - Correlation ID: Generates a unique ID for request tracking across logs and responses.
- Validation: Includes comprehensive checks:
- Presence and type (
string) ofrecipientandmessage. - Regex check for E.164 format for the
recipient. - Ensures the
messageis not empty. - Warns if message exceeds single SMS length (160 for GSM-7, 70 for Unicode).
- Returns a
400 Bad Requestif validation fails.
- Presence and type (
- Function Call: Calls the
sendSmsfunction within atry...catchblock. - Response:
- On success (
tryblock): Returns a200 OKstatus with a success message and the response data from Sinch (which includes abatch_id). - On failure (
catchblock): Returns a500 Internal Server Errorstatus with an error message and correlation ID for tracking.
- On success (
Understanding SMS Character Encoding and Length:
| Encoding | Characters Per Segment | Use Case |
|---|---|---|
| GSM-7 | 160 | Standard characters (A-Z, 0-9, basic punctuation) |
| Unicode (UCS-2) | 70 | Special 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:
# .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_NUMBERincludes the leading+. - Set
SINCH_REGIONaccording to your Sinch account's region. dotenvloads these intoprocess.envwhenrequire('dotenv').config();is called at the start ofindex.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):
// 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 Type | Retry? | Reason |
|---|---|---|
| Network timeout | Yes | Transient network issue |
| 5xx (500, 503) | Yes | Temporary server problem |
| 429 (Rate limit) | Yes | Respect Retry-After header |
| 4xx (400, 401, 403) | No | Request problem, won't resolve with retry |
Implementation using axios-retry:
npm install axios-retry// 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_idreturned 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):
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:
// 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-smsendpoint 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
joiorexpress-validatorfor complex rules and clearer error reporting. - Sanitize inputs to prevent injection attacks.
express-validatorincludes sanitization features.
Example with joi:
npm install joiconst 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.
npm install express-rate-limitAdd this after app.use(express.json());:
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 only3. Authentication/Authorization:
For APIs that require access control:
- Implement API key authentication, JWT (JSON Web Tokens), or OAuth tokens for clients calling your
/send-smsendpoint.
Example API Key Middleware:
// 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:
// 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:
node --version # Should be v18.x or v20.x7. Cookie Security (If using sessions):
If implementing session-based authentication, set security flags for cookies:
httpOnly: Prevents JavaScript access to cookiessecure: Ensures cookies are only sent over HTTPSsameSite: 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 thesendSmsfunction. 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:
| Region | Single SMS Cost | 3-Part SMS Cost | Notes |
|---|---|---|---|
| US | $0.0075 | $0.0225 | Standard rates |
| UK | $0.0400 | $0.1200 | Higher international costs |
| India | $0.0050 | $0.0150 | Requires 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/awaitwithaxiosleverages 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):
| Metric | Without Queue | With Queue (Redis) |
|---|---|---|
| Requests/second | 50–100 | 500–1000 |
| P95 Latency | 200–500 ms | 50–100 ms |
| Error Rate | 2–5% | <1% |
10. Monitoring, Observability, and Analytics
For production readiness:
- Health Checks: Add a health check endpoint that returns
200 OKif the server is running. Monitoring services can ping this endpoint.
Example Health Check:
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
sendSmsfunction). - Use tools like Prometheus with
prom-clientor APM services (Datadog, New Relic, Dynatrace).
- Request rate to
- 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:
| Error | Cause | Solution |
|---|---|---|
401 Unauthorized / 403 Forbidden | Incorrect SERVICE_PLAN_ID or API_TOKEN | Verify credentials in Sinch Dashboard and update .env |
400 Bad Request | Malformed request body (wrong number format, missing fields) | Check detailed error in Sinch response. Validate payload structure and number formats |
| Connection Errors / Timeouts | Network issue, wrong SINCH_REGION, DNS problems, firewall | Verify network connectivity. Check SINCH_REGION matches dashboard. Ensure firewalls allow HTTPS to *.sms.api.sinch.com |
429 Too Many Requests | Exceeding Sinch rate limits | Implement 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
fromnumber (SINCH_NUMBER) must be associated with yourSERVICE_PLAN_IDin 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:
- Check
.envcredentials → Verify in Sinch Dashboard - Test with
curllocally → Isolate network vs code issues - Check server logs for correlation ID → Track request through system
- Review Sinch Dashboard logs → Confirm API received request
- Contact Sinch support with correlation ID → Escalate if needed
12. Deployment and CI/CD
Basic Deployment (e.g., Heroku/Render):
-
Ensure Git Repository: Your code should be in a Git repository.
-
Platform Setup: Create an app on your chosen platform (Heroku, Render, Fly.io).
-
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.envfile. -
Procfile (for Heroku): Create a file named
Procfile(no extension) in your project root:Procfileweb: node index.jsRender often detects
node index.jsautomatically if specified inpackage.json'sstartscript. -
package.jsonStart Script: Add a start script to yourpackage.json:json{ "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" } } -
Deployment: Push your code to the platform via Git or use their CLI deployment commands.
Environment-Specific Considerations:
| Environment | Configuration | Rollback Strategy |
|---|---|---|
| Development | Local .env, debug logging | N/A |
| Staging | Platform env vars, structured logging | Git revert + redeploy |
| Production | Platform env vars + secrets manager, centralized logging, monitoring | Blue-green deployment or instant rollback via platform |
CI/CD Pipeline Example (GitHub Actions):
# .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
curlor Postman to send a POST request to your endpoint.
# 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
recipientormessage. - Send requests with invalid
recipientformat. - Temporarily modify
.envwith incorrect credentials to test401/403handling. - Send an empty
message.
- Send requests with missing
2. Automated Testing:
For robust applications, add automated tests.
Unit Tests (Jest Example):
npm install --save-dev jest supertest// __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):
// __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:
"scripts": {
"start": "node index.js",
"test": "jest"
}Run tests:
npm testFrequently 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.