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.
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:
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.jsrequire('dotenv').config(); // Load environment variables from .env file firstconst 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.
*/asyncfunctionsendSms(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 numberconst 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. Fromsource: 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.');
thrownewError('Sinch configuration is incomplete.');
}
const payload = {
from: sinchNumber,
to: [recipientNumber], // API expects an array of recipientsbody: 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 payloadtry {
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 2xxconsole.error('Status:', error.response.status);
console.error('Headers:', error.response.headers);
console.error('Data:', error.response.data);
// Re-throw a more specific errorthrownewError(`Sinch API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
} elseif (error.request) {
// The request was made but no response was receivedconsole.error('Request Error:', error.request);
thrownewError('Sinch API Error: No response received from server.');
} else {
// Something happened in setting up the request that triggered an Errorconsole.error('Axios Setup Error:', error.message);
thrownewError(`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 bodyconst 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 Unicodeif (messageLength > maxSingleSmsLength) {
console.warn(`Message length (${messageLength}) exceeds single SMS limit (${maxSingleSmsLength}). Message will be segmented.`);
}
// --- End Input Validation ---try {
const sinchResponse = awaitsendSms(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:
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.
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
# .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):
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:
Bash
npm install axios-retry
JavaScript
// Add near the top of index.js after requiring axiosconst axiosRetry = require('axios-retry');
// Configure retry logicaxiosRetry(axios, {
retries: 3, // 3 retry attemptsretryDelay: (retryCount, error) => {
// Respect Retry-After headerconst retryAfter = error.response?.headers['retry-after'];
if (retryAfter) {
returnNumber(retryAfter) * 1000; // Convert to milliseconds
}
// Exponential backoff with jitterconst baseDelay = retryCount * 1000;
const jitter = Math.random() * 500; // Add up to 500ms random jitterconsole.log(`Retry attempt: ${retryCount}, delay: ${baseDelay + jitter}ms`);
return baseDelay + jitter;
},
retryCondition: (error) => {
// Retry network errors, 5xx, and rate limitsreturn (
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 TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPDEFAULTCURRENT_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
constJoi = require('joi');
// Define validation schemaconst 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 endpointconst smsLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 100, // Limit each IP to 100 requests per windowmessage: 'Too many SMS requests from this IP. Try again after 15 minutes.',
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headerslegacyHeaders: 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 authenticationconstauthenticateApiKey = (req, res, next) => {
const apiKey = req.header('X-API-Key');
const validApiKey = process.env.API_KEY; // Store in .envif (!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:
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/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):
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 OK if the server is running. Monitoring services can ping this endpoint.
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 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:
Check .env credentials → Verify in Sinch Dashboard
Test with curl locally → 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 .env file.
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.
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"}}
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):
YAML
# .github/workflows/deploy.ymlname:DeploytoProductionon:push:branches: [ main ]
jobs:test:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3-uses:actions/setup-node@v3with:node-version:'18'-run:npmci-run:npmtestdeploy:needs:testruns-on:ubuntu-lateststeps:-uses:actions/checkout@v3-name:DeploytoRenderenv: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.
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.
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.
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.
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.
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.
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.
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'.
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.
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.
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.
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.
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.
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.
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.
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:
.envfiles, keeping credentials secure.System Architecture:
Alternative Architectures:
Prerequisites:
SERVICE_PLAN_IDandAPI_TOKENfrom the Sinch Customer Dashboard.Expected Outcome:
You'll have a running Node.js Express application that accepts POST requests to
/send-smsand 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
-yflag accepts default settings.This creates a
package.jsonfile 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.
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.jsonto prevent breaking changes. Yourpackage.jsonshould include:"dependencies": { "express": "^4.18.0", "axios": "^1.6.0", "dotenv": "^16.0.0" }Commit
package-lock.jsonto 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
.gitignoreand add these lines to prevent committing sensitive data and unnecessary files:This protects your credentials and keeps your repository clean.
6. Create Basic Express Server:
Open
index.jsand 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.
Open your browser or use
curl http://localhost:3000. You should see "Sinch SMS API Server is running!" PressCtrl+Cto 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 theapp.listencall:// 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:
SERVICE_PLAN_ID,API_TOKEN,SINCH_NUMBER, andSINCH_REGIONfrom environment variables usingprocess.env.us,eu). Defaults tousbut can be overridden viaSINCH_REGION./xms/v1/{servicePlanId}/batches).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.Authorization: UsesBearerauthentication with yourAPI_TOKEN.Content-Type: Must beapplication/json.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
sendSmsfunction to handle incoming SMS requests.1. Create the
/send-smsEndpoint:Add the following route handler in
index.js, between thesendSmsfunction definition and theapp.listencall:// 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:
POSTroute at/send-sms.express.json()middleware (added earlier) automatically parses the incoming JSON request body intoreq.body.recipientandmessagefromreq.body.string) ofrecipientandmessage.recipient.messageis not empty.400 Bad Requestif validation fails.sendSmsfunction within atry...catchblock.tryblock): Returns a200 OKstatus with a success message and the response data from Sinch (which includes abatch_id).catchblock): Returns a500 Internal Server Errorstatus with an error message and correlation ID for tracking.Understanding SMS Character Encoding and Length:
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:
+12345678900). Ensure this number is associated with the Service Plan ID you're using.2. Configure
.envFile:Open the
.envfile you created earlier and add your credentials:YOUR_..._HERE) with your actual credentials.SINCH_NUMBERincludes the leading+.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
.envfile to version control. Verify.envis listed in your.gitignorefile.5. Implementing Error Handling and Retry Logic
Refine logging and implement retry strategies for production reliability.
1. Enhance Logging:
Basic
console.logandconsole.errorwork for development. For production, structured logging libraries likewinstonorpinoprovide 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:
Retry-AfterheaderImplementation using
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:
batch_idreturned by Sinch, and delivery status updates.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:
/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:
joiorexpress-validatorfor complex rules and clearer error reporting.express-validatorincludes sanitization features.Example with
joi: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.
Add 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:
/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()ornew 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 attacksCitation3. 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
+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.Cost Estimation Examples:
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.
async/awaitwithaxiosleverages this.maxSockets).Performance Benchmarks (Typical):
10. Monitoring, Observability, and Analytics
For production readiness:
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() }); });/send-sms.sendSmsfunction).prom-clientor APM services (Datadog, New Relic, Dynatrace).11. Troubleshooting Common Issues
Common Issues:
401 Unauthorized/403 ForbiddenSERVICE_PLAN_IDorAPI_TOKEN.env400 Bad RequestSINCH_REGION, DNS problems, firewallSINCH_REGIONmatches dashboard. Ensure firewalls allow HTTPS to*.sms.api.sinch.com429 Too Many RequestsAdditional Troubleshooting Tips:
+15551234567). Validate/normalize client inputs.fromnumber (SINCH_NUMBER) must be associated with yourSERVICE_PLAN_IDin the Sinch dashboard.Troubleshooting Decision Flow:
.envcredentials → Verify in Sinch Dashboardcurllocally → Isolate network vs code issues12. 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:Render often detects
node index.jsautomatically if specified inpackage.json'sstartscript.package.jsonStart Script: Add a start script to yourpackage.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:
.env, debug loggingCI/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
axiospackage, obtain your Service Plan ID and API Token from the Sinch dashboard, then make a POST request tohttps://{region}.sms.api.sinch.com/xms/v1/{servicePlanId}/batcheswith Bearer token authentication. Includefrom,to, andbodyfields 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.statusfor HTTP status codes (401/403 for authentication, 400 for bad requests, 429 for rate limits, 5xx for server errors). Log error details includingerror.response.datafor 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
dotenvpackage. Never commit your.envfile to version control. Add.envto.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-limitmiddleware 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-retrypackage with exponential backoff and jitter. Configure 3-5 retry attempts, respectRetry-Afterheaders, 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:
node index.jscurlor 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}\" }"recipientormessage.recipientformat..envwith incorrect credentials to test401/403handling.message.2. Automated Testing:
For robust applications, add automated tests.
Unit Tests (Jest Example):
// __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 test