code examples
code examples
How to Send SMS with Node.js Using Vonage Messages API: Complete Express Tutorial
Learn how to send SMS programmatically with Node.js and Express using the Vonage Messages API. Step-by-step guide covering setup, security, error handling, and production deployment.
Expected: {"success":false,"message":"Missing "to" phone number or "text" message in request body."} (Status 400)
Build a production-ready Node.js and Express application that sends SMS messages programmatically using the Vonage Messages API. This comprehensive tutorial covers project setup, API integration, security best practices, error handling, and deployment strategies.
By the end, you'll have a REST API endpoint that accepts a phone number and message text, then sends an SMS via Vonage. Use this foundation to integrate SMS functionality into larger applications for notifications, two-factor authentication (2FA), OTP verification, or customer alerts.
Time to complete: 45–60 minutes
Skill level: Intermediate (requires basic JavaScript and Node.js knowledge)
Project Overview and Goals
What you're building:
A Node.js web server using Express that exposes a single API endpoint (POST /send-sms). When this endpoint receives a request with a destination phone number and message text, it uses the Vonage Messages API to send an SMS.
Problem solved:
Programmatically send SMS messages from your web application without managing direct carrier integrations.
Technologies:
- Node.js: JavaScript runtime for server-side applications. Offers strong performance, a large ecosystem (npm), and asynchronous I/O suitable for API calls
- Express: Minimal and flexible Node.js web framework for setting up routes and handling HTTP requests
- Vonage Messages API: Communication platform enabling SMS, MMS, WhatsApp, and other messaging channels with robust global infrastructure
@vonage/server-sdk: Official Vonage Node.js librarydotenv: Module for loading environment variables from.envfiles to securely manage API credentials
System architecture:
+-------------+ +------------------------+ +-----------------+ +--------------+
| Client |------>| Node.js/Express API |------>| Vonage API |------>| Mobile Phone |
| (e.g. curl, | | (Your Application) | | (Messages API) | | (Recipient) |
| Postman) | | POST /send-sms | +-----------------+ +--------------+
+-------------+ +------------------------+
|
| Uses @vonage/server-sdk
| Reads credentials from .envPrerequisites:
- Node.js 14+ and npm (or yarn): Download from nodejs.org
- Vonage API Account: Sign up at Vonage API Dashboard. New accounts receive free credits
- Vonage Application: Create a Vonage application with Messages capabilities enabled
- Private Key: Associated with your Vonage application
- Vonage Virtual Number: Purchase or rent a phone number through your Vonage account capable of sending SMS. Expect costs of $1–$5/month for number rental plus per-message fees (~$0.01–$0.05 per SMS)
- Basic JavaScript/Node.js knowledge: Familiarity with fundamental concepts
- Terminal access: For running commands
1. Set Up Your Node.js SMS Project
Create the project directory, initialize Node.js, and install dependencies.
-
Create project directory:
Open your terminal and create a new directory for your project.
bashmkdir vonage-sms-sender cd vonage-sms-sender -
Initialize Node.js project:
Create a
package.jsonfile with default settings.bashnpm init -y -
Install dependencies:
Install
expressfor the web server,@vonage/server-sdkfor Vonage API interaction, anddotenvfor environment variable management.bashnpm install express @vonage/server-sdk dotenvexpress: Framework for building the API@vonage/server-sdk: Simplifies Vonage Messages API callsdotenv: Loads environment variables from.envfiles for secure configuration
-
Create project files:
Create the main application file, environment variables file, and Git ignore rules.
macOS/Linux:
bashtouch index.js .env .gitignoreWindows (Command Prompt):
cmdtype nul > index.js type nul > .env type nul > .gitignoreWindows (PowerShell):
powershellNew-Item index.js, .env, .gitignoreindex.js: Contains your Express server and API logic.env: Stores sensitive credentials like API keys. Never commit this file to version control..gitignore: Specifies files Git should ignore (like.envandnode_modules)
-
Configure
.gitignore:Open
.gitignoreand add these lines to prevent committing sensitive information:Code# Dependencies node_modules/ # Environment variables .env # Vonage private key file private.key # Logs logs *.log # Runtime data pids *.pid *.seed *.pid.lock # Optional IDE directories/files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? -
Verify project structure:
Your project directory should look like this:
textvonage-sms-sender/ ├── .env ├── .gitignore ├── index.js ├── node_modules/ ├── package-lock.json └── package.json
2. Configure Vonage API Credentials
Obtain credentials from Vonage and configure your application to use them securely via environment variables.
-
Log in to Vonage Dashboard:
Access your Vonage API Dashboard.
-
Create a Vonage Application:
- Navigate to "Applications" in the left-hand menu
- Click "Create a new application"
- Name your application (e.g., "Node SMS Sender Guide")
- Click "Generate public and private key". Important: A file named
private.keydownloads immediately. Save this file in the root of your project directory (vonage-sms-sender/). You cannot download it again. Vonage stores the public key - Scroll to the "Capabilities" section
- Toggle on "Messages". You'll see fields for "Inbound URL" and "Status URL". For sending SMS, these aren't strictly required immediately, but Vonage requires them. Enter placeholder URLs like
https://example.com/webhooks/inboundandhttps://example.com/webhooks/status. Update these with real endpoints later if you plan to receive messages or delivery receipts - Click "Generate new application"
- On the application details page, copy the Application ID – you'll need it for your
.envfile
-
Link your Vonage number:
- Navigate to "Numbers" > "Your numbers" in the dashboard
- If you don't have a number, go to "Buy numbers", find one with SMS capability in your desired country, and purchase it. Expect costs of $1–$5/month for number rental
- In "Your numbers", find the number you want to use for sending SMS
- Click the gear icon or "Manage" button next to the number
- Under "Messaging Settings", select the Vonage Application you created ("Node SMS Sender Guide") from the dropdown menu
- Click "Save". Copy this phone number (including country code) – this is your sender ID (
VONAGE_FROM_NUMBER)
Choosing the right number type:
- Long code (standard number): Most common for person-to-person messaging. Suitable for low-volume SMS
- Short code: 5–6 digit numbers for high-volume messaging. Requires approval and higher costs
- Toll-free: Free for recipients. Good for customer support. May have restrictions in some countries
-
Get API Key and Secret (optional but recommended):
While you primarily use Application ID and Private Key for Messages API authentication, your main account API Key and Secret are on the dashboard landing page ("API settings"). Store these for future use with other Vonage APIs.
-
Configure environment variables (
.env):Open the
.envfile and add these variables, replacing placeholders with your actual credentials:Code# Vonage Credentials VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE VONAGE_PRIVATE_KEY_PATH=./private.key VONAGE_FROM_NUMBER=YOUR_VONAGE_NUMBER_HERE # Optional: Vonage Account API Key/Secret (for other APIs) # VONAGE_API_KEY=YOUR_API_KEY_HERE # VONAGE_API_SECRET=YOUR_API_SECRET_HERE # Server Configuration PORT=3000VONAGE_APPLICATION_ID: The Application ID from your Vonage applicationVONAGE_PRIVATE_KEY_PATH: Relative path fromindex.jsto yourprivate.keyfile../private.keyassumes it's in the same directory asindex.jsVONAGE_FROM_NUMBER: Your Vonage virtual number in E.164 format (e.g.,+14155550100)PORT: Port number your Express server listens on
Security:
.envis in.gitignore, preventing accidental commits of secret credentials. Ensureprivate.keyis also in.gitignore.Recommended: Create a
.env.examplefile with placeholder values to help other developers set up their own.envfile:CodeVONAGE_APPLICATION_ID=your_application_id VONAGE_PRIVATE_KEY_PATH=./private.key VONAGE_FROM_NUMBER=+1234567890 PORT=3000If you lose your private key: You cannot download it again. Generate a new key pair in the Vonage Dashboard under your application settings, then download and replace the old
private.keyfile.
3. Implement the SMS Sending API Endpoint
Write the Node.js/Express code to create the server and SMS sending endpoint.
-
Set up Express server (
index.js):Open
index.jsand add the initial setup:javascript// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); // --- Vonage Client Initialization --- // Ensure necessary environment variables are loaded if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) { console.error('Error: VONAGE_APPLICATION_ID or VONAGE_PRIVATE_KEY_PATH not set in .env'); process.exit(1); // Exit if essential config is missing } const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH }); // --- Express App Setup --- 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()); // Middleware to parse URL-encoded request bodies app.use(express.urlencoded({ extended: true })); // --- API Endpoints --- // (Add /send-sms endpoint here) // --- Basic Health Check Endpoint --- app.get('/health', (req, res) => { res.status(200).send('OK'); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening on http://localhost:${port}`); });require('dotenv').config();: Loads variables from.envintoprocess.env. Must be called at the top- Imports
expressandVonagefrom the SDK - Initializes the
Vonageclient using Application ID and private key path from environment variables. Includes a check to ensure these critical variables are set - Sets up Express app (
app) - Uses
express.json()andexpress.urlencoded()middleware to parse incoming request bodies - Includes a
/healthendpoint for monitoring - Starts the server with
app.listen
-
Add CORS support (if needed for frontend integration):
Install the
corspackage:bashnpm install corsUpdate
index.js:javascript// index.js (near the top) const cors = require('cors'); // ... const app = express(); app.use(cors()); // Enable CORS for all routes // ... (rest of middleware) -
Implement the
/send-smsendpoint:Add this route handler within the
// --- API Endpoints ---section ofindex.js:javascript// index.js (inside API Endpoints section) app.post('/send-sms', async (req, res) => { // Extract and validate input const { to, text } = req.body; const from = process.env.VONAGE_FROM_NUMBER; if (!to || !text) { console.error('Missing "to" or "text" in request body'); return res.status(400).json({ success: false, message: 'Missing "to" phone number or "text" message in request body.' }); } if (!from) { console.error('Error: VONAGE_FROM_NUMBER not set in .env'); return res.status(500).json({ success: false, message: 'Server configuration error: Sender number not set.' }); } // Validate message length (160 chars for single SMS) if (text.length > 1600) { // 10 messages max as example limit return res.status(400).json({ success: false, message: 'Message too long. Maximum 1600 characters (10 SMS segments).' }); } console.log(`Attempting to send SMS from ${from} to ${to} with text: "${text}"`); try { const resp = await vonage.messages.send({ message_type: "text", text: text, to: to, from: from, channel: "sms" }); console.log('Message sent successfully:', resp); // Vonage Messages API returns message_uuid on success res.status(200).json({ success: true, message_uuid: resp.message_uuid }); } catch (error) { console.error('Error sending SMS via Vonage:', error); // Provide specific feedback based on error let errorMessage = 'Failed to send SMS.'; let statusCode = 500; if (error.response?.data) { console.error('Vonage API Error Details:', error.response.data); // Extract specific error message from Vonage response errorMessage = error.response.data.title || error.response.data.detail || errorMessage; // Set appropriate status code based on error type if (error.response.status >= 400 && error.response.status < 500) { statusCode = 400; // Client error } } else if (error.message) { errorMessage = error.message; } res.status(statusCode).json({ success: false, message: errorMessage, errorDetails: error.response?.data }); } });- Defines a
POSTroute at/send-sms - Uses an
asyncfunction to handle the asynchronousvonage.messages.sendcall withawait - Input validation: Extracts
to(recipient number) andtext(message content) from the JSON request body. Checks they exist and that the message isn't too long. Returns400 Bad Requestor500 Internal Server Errorif validation fails - Vonage call: Calls
vonage.messages.send()with:message_type: Set to"text"for standard SMStext: SMS message contentto: Recipient's phone number (should be in E.164 format, e.g.,+14155550101)from: Your Vonage virtual number (sender ID) from.envchannel: Must be"sms"
- Success handling: Logs the response (includes
message_uuid) and sends a200 OKresponse withsuccess: trueand themessage_uuid - Error handling: Logs detailed errors and sends a
400or500response withsuccess: falseand an error message, potentially including Vonage API response details
Example request/response:
Request:
bashcurl -X POST http://localhost:3000/send-sms \ -H "Content-Type: application/json" \ -d '{"to": "+14155550101", "text": "Hello from Vonage!"}'Success response (200):
json{ "success": true, "message_uuid": "abcd1234-5678-90ef-ghij-klmnopqrstuv" }Error response (400):
json{ "success": false, "message": "Missing \"to\" phone number or \"text\" message in request body." } - Defines a
4. Advanced Error Handling and Logging for Production
Refine error handling and logging for production readiness.
Logging Strategy
Current implementation uses console.log for information and console.error for failures.
Production logging: Use a dedicated logging library:
- Winston: Structured logging with multiple transports (file, console, remote services)
- Pino: High-performance JSON logging
Install Winston:
npm install winstonConfigure Winston in index.js:
// index.js (near the top, after dotenv)
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// Log to console in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Replace console.log and console.error with logger
// Example: logger.info('Server started'), logger.error('Error occurred', { error })Retry Mechanisms
Vonage internal retries: Vonage handles some retries internally for transient network issues when delivering messages to carriers.
Application-level retries: Network issues can occur between your server and the Vonage API. For critical messages, implement application-level retries with exponential backoff.
Install async-retry:
npm install async-retryAdd retry logic to /send-sms endpoint:
// index.js (at the top)
const retry = require('async-retry');
// ... inside app.post('/send-sms') ...
try {
const resp = await retry(async (bail) => {
try {
const result = await vonage.messages.send({
message_type: "text",
text: text,
to: to,
from: from,
channel: "sms"
});
return result;
} catch (error) {
// Don't retry client errors (4xx)
if (error.response?.status >= 400 && error.response?.status < 500) {
bail(error); // Stop retrying
return;
}
throw error; // Retry on 5xx or network errors
}
}, {
retries: 3, // Number of retries
factor: 2, // Exponential backoff factor
minTimeout: 1000, // Initial timeout (1 second)
maxTimeout: 5000 // Maximum timeout (5 seconds)
});
logger.info('Message sent successfully', { message_uuid: resp.message_uuid });
res.status(200).json({ success: true, message_uuid: resp.message_uuid });
} catch (error) {
// ... existing error handling ...
}Idempotency
Prevent duplicate messages by implementing idempotency:
- Accept an
idempotency_keyin the request body - Store sent message UUIDs with their idempotency keys in a database or cache (Redis)
- Before sending, check if an idempotency key was already processed. If yes, return the cached response instead of sending again
Example:
// Pseudocode - requires Redis or database
const idempotencyKey = req.body.idempotency_key;
if (idempotencyKey) {
const cachedResult = await redis.get(`idempotency:${idempotencyKey}`);
if (cachedResult) {
return res.status(200).json(JSON.parse(cachedResult));
}
}
// Send message...
const resp = await vonage.messages.send({...});
// Cache result
if (idempotencyKey) {
await redis.setex(`idempotency:${idempotencyKey}`, 86400, JSON.stringify({
success: true,
message_uuid: resp.message_uuid
}));
}5. Security Best Practices for SMS APIs
Implement security best practices to protect your application and Vonage account.
Input Validation
Use validation libraries for robust input checking:
Install joi:
npm install joiAdd validation to /send-sms endpoint:
// index.js (at the top)
const Joi = require('joi');
// Define schema
const smsSchema = Joi.object({
to: Joi.string().pattern(/^\+[1-9]\d{1,14}$/).required(), // E.164 format
text: Joi.string().min(1).max(1600).required()
});
// ... inside app.post('/send-sms') ...
const { error, value } = smsSchema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
message: error.details[0].message
});
}
const { to, text } = value;
// ... rest of endpoint logic ...Rate Limiting
Protect your API from abuse with rate limiting:
Install express-rate-limit:
npm install express-rate-limitConfigure rate limiting:
// index.js (near the top, after express)
const rateLimit = require('express-rate-limit');
// Create rate limiter
const smsLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many SMS requests from this IP. Try again after 15 minutes.',
standardHeaders: true, // Return rate limit info in RateLimit-* headers
legacyHeaders: false, // Disable X-RateLimit-* headers
});
// Apply to SMS endpoint
app.post('/send-sms', smsLimiter, async (req, res) => {
// ... existing endpoint logic ...
});Authentication
Protect your endpoint with API key authentication:
Generate API keys: Use a UUID generator or secure random string generator.
Store API keys: Use environment variables or a database.
Implement middleware:
// index.js
const API_KEYS = new Set([
process.env.API_KEY_1,
process.env.API_KEY_2
].filter(Boolean)); // Remove undefined keys
function authenticateApiKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !API_KEYS.has(apiKey)) {
return res.status(401).json({
success: false,
message: 'Invalid or missing API key'
});
}
next();
}
// Apply to SMS endpoint
app.post('/send-sms', authenticateApiKey, smsLimiter, async (req, res) => {
// ... existing endpoint logic ...
});Usage:
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{"to": "+14155550101", "text": "Hello!"}'HTTPS/TLS
Development: Use HTTP locally.
Production: Always use HTTPS. Most hosting platforms (Heroku, Render, AWS) provide HTTPS by default. If self-hosting:
- Obtain SSL certificates from Let's Encrypt (free)
- Configure your reverse proxy (Nginx, Apache) or Node.js HTTPS server
Helmet
Use Helmet to set security-related HTTP headers:
Install helmet:
npm install helmetConfigure helmet:
// index.js (near the top)
const helmet = require('helmet');
const app = express();
app.use(helmet()); // Apply Helmet middleware early
// ... (rest of middleware) ...Security Checklist
- Environment variables secured (
.envin.gitignore) - Private key file secured (
private.keyin.gitignore) - Input validation implemented (phone numbers, message length)
- Rate limiting configured
- API key authentication implemented
- HTTPS enabled in production
- Helmet middleware applied
- Error messages don't expose sensitive information
6. Handle Special Cases
Consider edge cases relevant to SMS messaging.
Phone Number Formatting
Vonage expects numbers in E.164 format (e.g., +14155550100). Standardize input using libphonenumber-js:
Install libphonenumber-js:
npm install libphonenumber-jsValidate and format phone numbers:
// index.js (at the top)
const { parsePhoneNumber, isValidPhoneNumber } = require('libphonenumber-js');
// ... inside app.post('/send-sms') ...
const { to, text } = req.body;
// Validate and format phone number
if (!isValidPhoneNumber(to)) {
return res.status(400).json({
success: false,
message: 'Invalid phone number format. Use E.164 format (e.g., +14155550100).'
});
}
const phoneNumber = parsePhoneNumber(to);
const formattedTo = phoneNumber.format('E.164');
// Use formattedTo in vonage.messages.send()
const resp = await vonage.messages.send({
message_type: "text",
text: text,
to: formattedTo, // Use formatted number
from: from,
channel: "sms"
});Character Limits and Encoding
- GSM-7 encoding: 160 characters per SMS segment
- UCS-2 encoding (Unicode): 70 characters per SMS segment for non-Latin characters
- Concatenated SMS: Longer messages are split into multiple segments
Vonage handles concatenation automatically. Be mindful of costs – multi-part messages consume multiple message credits.
International Sending
Sender ID requirements vary by country:
| Country | Sender ID Type | Notes |
|---|---|---|
| US/Canada | Long code or toll-free | Alphanumeric sender IDs not supported |
| UK | Long code or alphanumeric | Alphanumeric sender IDs supported |
| India | Sender ID registration required | Strict regulations, pre-registration needed |
| UAE | Pre-registered sender IDs only | Alphanumeric sender IDs require approval |
| China | Not supported | SMS to mainland China blocked |
Consult Vonage's country-specific documentation before sending internationally.
Delivery Failures
Not all SMS messages get delivered. Common reasons:
- Invalid or disconnected number
- Phone powered off or out of coverage
- Carrier blocking or filtering
- Number porting in progress
Implement Status Webhooks (Delivery Receipts):
-
Update your Vonage Application's "Status URL" to your webhook endpoint (e.g.,
https://yourdomain.com/webhooks/status). -
Create the webhook endpoint:
javascript// index.js app.post('/webhooks/status', (req, res) => { const { message_uuid, status, error_text } = req.body; logger.info('Message status update', { message_uuid, status, error_text }); // Store status in database or trigger alerts // Status values: submitted, delivered, rejected, expired, etc. res.status(200).send('OK'); });
Test Numbers (Free Tier)
On Vonage trial accounts, you can only send SMS to verified test numbers. To add a test number:
- Go to Vonage Dashboard > Settings > Test Numbers
- Enter the phone number
- Verify via SMS or voice call
Sending to unverified numbers results in a "Non-Whitelisted Destination" error.
SMS Compliance and Spam Regulations
Compliance requirements:
- Obtain consent: Recipients must opt-in to receive messages
- Provide opt-out: Include "Reply STOP to unsubscribe" in your messages
- Honor opt-outs: Maintain a suppression list and stop sending to opted-out numbers
- TCPA (US): Telephone Consumer Protection Act requires prior express written consent for marketing messages
- GDPR (EU): General Data Protection Regulation requires consent and data protection measures
- CTIA Guidelines: Follow Cellular Telecommunications Industry Association best practices
Implementation example:
// Maintain opt-out list (use database in production)
const optOutList = new Set();
app.post('/send-sms', authenticateApiKey, smsLimiter, async (req, res) => {
const { to, text } = req.body;
// Check opt-out list
if (optOutList.has(to)) {
return res.status(400).json({
success: false,
message: 'Recipient has opted out of receiving messages.'
});
}
// ... rest of endpoint logic ...
});
// Handle opt-out webhook (inbound SMS)
app.post('/webhooks/inbound', (req, res) => {
const { from, text } = req.body;
if (text.trim().toUpperCase() === 'STOP') {
optOutList.add(from);
logger.info('User opted out', { phone: from });
// Send confirmation (required by TCPA)
vonage.messages.send({
message_type: "text",
text: "You have been unsubscribed. Reply START to resume messages.",
to: from,
from: process.env.VONAGE_FROM_NUMBER,
channel: "sms"
});
}
res.status(200).send('OK');
});7. Test Your SMS Implementation
Ensure your implementation works correctly.
Start the Server
Verify your .env file contains correct Vonage credentials.
node index.jsYou should see: Server listening on http://localhost:3000
Manual Testing with curl
Replace YOUR_RECIPIENT_NUMBER with a valid phone number (use a verified test number if on a trial account).
Test successful SMS send:
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{
"to": "YOUR_RECIPIENT_NUMBER",
"text": "Hello from my Node.js Vonage App!"
}'Expected output:
Terminal (server):
Server listening on http://localhost:3000
Attempting to send SMS from YOUR_VONAGE_NUMBER to YOUR_RECIPIENT_NUMBER with text: "Hello from my Node.js Vonage App!"
Message sent successfully: { message_uuid: '...' }Terminal (curl):
{"success":true,"message_uuid":"SOME_UUID_FROM_VONAGE"}Recipient phone receives the SMS message.
Test Error Cases
Missing parameters:
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{"text": "Missing recipient"}'
# Expected: {"success":false,"message":"Missing \"to\" phone number or \"text\" message in request body."} (Status 400)Invalid phone number:
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{
"to": "invalid-number",
"text": "Testing invalid recipient"
}'
# Expected: {"success":false,"message":"Invalid phone number format..."} (Status 400)Non-whitelisted number (trial account):
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{
"to": "+15555555555",
"text": "Testing non-whitelisted number"
}'
# Expected: {"success":false,"message":"Non-Whitelisted Destination",...} (Status 400/500)Rate limit exceeded:
# Send 101 requests rapidly
for i in {1..101}; do
curl -X POST http://localhost:3000/send-sms \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-api-key" \
-d '{"to": "+14155550101", "text": "Test '$i'"}'
done
# Expected (on 101st request): {"success":false,"message":"Too many SMS requests..."} (Status 429)Automated Testing
Install Jest:
npm install --save-dev jest supertestCreate test file (index.test.js):
const request = require('supertest');
const express = require('express');
// Mock Vonage SDK
jest.mock('@vonage/server-sdk');
const { Vonage } = require('@vonage/server-sdk');
describe('SMS API', () => {
let app;
let mockSend;
beforeEach(() => {
// Set up mock
mockSend = jest.fn().mockResolvedValue({ message_uuid: 'test-uuid-123' });
Vonage.mockImplementation(() => ({
messages: {
send: mockSend
}
}));
// Set required environment variables
process.env.VONAGE_APPLICATION_ID = 'test-app-id';
process.env.VONAGE_PRIVATE_KEY_PATH = './test-private.key';
process.env.VONAGE_FROM_NUMBER = '+14155550100';
process.env.API_KEY_1 = 'test-api-key';
// Load app (in real scenario, export app from index.js)
app = require('./index');
});
afterEach(() => {
jest.clearAllMocks();
});
test('POST /send-sms - success', async () => {
const response = await request(app)
.post('/send-sms')
.set('X-API-Key', 'test-api-key')
.send({
to: '+14155550101',
text: 'Test message'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message_uuid).toBe('test-uuid-123');
expect(mockSend).toHaveBeenCalledWith({
message_type: 'text',
text: 'Test message',
to: '+14155550101',
from: '+14155550100',
channel: 'sms'
});
});
test('POST /send-sms - missing parameters', async () => {
const response = await request(app)
.post('/send-sms')
.set('X-API-Key', 'test-api-key')
.send({ text: 'Test message' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('Missing');
});
test('POST /send-sms - invalid API key', async () => {
const response = await request(app)
.post('/send-sms')
.set('X-API-Key', 'wrong-key')
.send({
to: '+14155550101',
text: 'Test message'
});
expect(response.status).toBe(401);
expect(response.body.success).toBe(false);
});
test('POST /send-sms - Vonage API error', async () => {
mockSend.mockRejectedValue({
response: {
status: 400,
data: { title: 'Invalid number format' }
}
});
const response = await request(app)
.post('/send-sms')
.set('X-API-Key', 'test-api-key')
.send({
to: 'invalid',
text: 'Test message'
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('Invalid');
});
});Add test script to package.json:
{
"scripts": {
"test": "jest",
"start": "node index.js"
}
}Run tests:
npm testVerification Checklist
- Project dependencies installed (
npm install) -
.envfile created with correct Vonage Application ID, Private Key Path, and From Number -
private.keyfile downloaded and placed at specified path -
.envandprivate.keyin.gitignore - Vonage number linked to correct Vonage Application in dashboard
- Server starts without errors (
node index.js) -
/healthendpoint returnsOK(Status 200) - Valid request to
/send-smsreturns{"success":true, "message_uuid":"..."}(Status 200) - Recipient phone receives SMS message
- Invalid requests return appropriate error responses (Status 400/500)
- Server logs show relevant information
- Rate limiting works (Status 429 after limit exceeded)
- API key authentication works (Status 401 for invalid keys)
8. Troubleshooting Common SMS Issues
Credentials Not Found
Error: Credentials could not be found or similar authentication errors.
Solutions:
- Verify
VONAGE_APPLICATION_IDandVONAGE_PRIVATE_KEY_PATHin.envare correct - Ensure
private.keyfile exists at the exact path specified inVONAGE_PRIVATE_KEY_PATHrelative to where you runnode index.js - Confirm
private.keyfile content is correct (should start with-----BEGIN PRIVATE KEY-----) - Ensure
dotenvis loaded (require('dotenv').config();at the top ofindex.js)
Non-Whitelisted Destination
Error: Non-Whitelisted Destination error.
Solutions:
- You're using a Vonage trial account. Add recipient numbers to your verified test list in Vonage Dashboard > Settings > Test Numbers
- Upgrade your account by adding payment details
Invalid From Number
Error: Invalid 'From' number or sender ID errors.
Solutions:
- Ensure
VONAGE_FROM_NUMBERin.envis a valid number from your Vonage account in E.164 format (e.g.,+14155550100) - Confirm this number is linked to your Vonage Application (matching
VONAGE_APPLICATION_ID) - Check if the number can send SMS to the destination country. Some numbers have restrictions
Messages Not Received
Issue: SMS not delivered to recipient.
Solutions:
- Verify recipient number (
to) is correct and in E.164 format - Check server logs for
message_uuidand any Vonage errors in thecatchblock - Check Vonage Dashboard logs for message status details
- Consider temporary carrier issues, device problems (phone off, poor signal), or blocked numbers
- Implement Delivery Receipts (status webhooks) for better tracking
Rate Limits Exceeded
Error: Status 429 or rate limit messages.
Solutions:
- If you implemented
express-rate-limit, check the limit configuration. Default is 100 requests per 15 minutes per IP - Vonage has platform-wide rate limits. Check your account tier and limits in the dashboard
- Implement exponential backoff and retry logic in your client application
SDK Version Compatibility
Issue: Unexpected behavior after updating @vonage/server-sdk.
Solutions:
- Check the SDK changelog for breaking changes
- Pin SDK version in
package.jsonfor stability:"@vonage/server-sdk": "3.x.x"(replace with your version) - Test thoroughly after major version upgrades
Vonage Rate Limits and Quotas
Vonage imposes the following limits (as of 2024):
- Free tier: ~1,000 messages (varies by country)
- API rate limits:
- 100 requests/second per account
- 1,000 requests/second aggregate across all Vonage APIs
- SMS throughput: Depends on number type:
- Long code: 1 message/second
- Short code: Up to 100 messages/second
- Toll-free: Up to 3 messages/second
Check your specific limits in the Vonage Dashboard under "Settings" > "API Settings".
FAQ
Q: Can I use this with TypeScript?
A: Yes. Install @types/express and @types/node, rename index.js to index.ts, and compile with TypeScript.
Q: How do I handle inbound SMS?
A: Create a webhook endpoint (POST /webhooks/inbound) and configure it in your Vonage Application's "Inbound URL". The endpoint receives from, to, text, and other parameters.
Q: Can I send MMS?
A: Yes. Change message_type to "image" and add image.url parameter. Requires MMS-capable Vonage number.
Q: How do I track message delivery?
A: Implement status webhooks (see "Delivery Failures" section). Vonage sends updates to your "Status URL" endpoint.
Q: What's the cost per message?
A: Varies by destination country. US SMS typically costs $0.0075–$0.0150 per segment. Check Vonage pricing.
9. Deploy Your SMS Application to Production
Deploy your Node.js application to a production environment with proper configuration management.
Choose a Hosting Platform
Platform comparison:
| Platform | Type | Pros | Cons | Cost |
|---|---|---|---|---|
| Heroku | PaaS | Simple deployment, add-ons ecosystem | Higher cost, limited free tier | ~$7/month (Eco dyno) |
| Render | PaaS | Modern PaaS, free tier, auto-deploy | Newer platform, fewer add-ons | Free tier available, ~$7/month (Starter) |
| AWS Elastic Beanstalk | PaaS | AWS ecosystem integration, scalable | More complex setup | Pay per usage (~$10–50/month) |
| AWS EC2 | IaaS | Full control, highly scalable | Requires server management | Pay per usage (~$5–100/month) |
| DigitalOcean | IaaS | Simple VPS, predictable pricing | Manual setup required | ~$6–40/month |
| AWS Lambda | Serverless | Pay per request, auto-scaling | Cold starts, API Gateway costs | Pay per invocation (~$0.20 per 1M requests) |
Environment Variable Management
Critical: Never commit .env or private.key to version control.
Platform-specific configuration:
- Heroku: Use Config Vars (Dashboard > Settings > Config Vars or
heroku config:set) - Render: Environment Variables (Dashboard > Environment)
- AWS: Parameter Store or Secrets Manager
- Docker: Environment variables in
docker-compose.ymlor-eflags
For private.key file:
-
Securely copy the file to your server during deployment (use SCP, CI/CD secure file handling, or secret managers)
-
Set
VONAGE_PRIVATE_KEY_PATHto the file location on the server -
Alternative: Store the private key content as an environment variable and write it to a file at runtime:
javascript// index.js const fs = require('fs'); const privateKeyPath = './private.key'; if (process.env.VONAGE_PRIVATE_KEY_CONTENT) { fs.writeFileSync(privateKeyPath, process.env.VONAGE_PRIVATE_KEY_CONTENT); } const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyPath });
Deploy to Heroku
Prerequisites: Install Heroku CLI.
Steps:
-
Log in to Heroku:
bashheroku login -
Create Heroku app:
bashheroku create your-app-name -
Add Procfile:
Create
Procfilein project root:Procfileweb: node index.js -
Set config vars:
Replace placeholders with your actual values:
bashheroku config:set VONAGE_APPLICATION_ID=your_application_id heroku config:set VONAGE_FROM_NUMBER=+14155550100 heroku config:set API_KEY_1=your_secret_api_key # Option 1: Set private key path (requires copying file during deployment) heroku config:set VONAGE_PRIVATE_KEY_PATH=./private.key # Option 2: Store private key content as environment variable heroku config:set VONAGE_PRIVATE_KEY_CONTENT="$(cat private.key)" -
Commit changes:
bashgit add . git commit -m "Add Procfile and prepare for Heroku" -
Deploy:
bashgit push heroku main -
Verify deployment:
bashheroku logs --tail heroku openTest the
/healthendpoint:bashcurl https://your-app-name.herokuapp.com/health # Expected: OK
Deploy with Docker
Create Dockerfile:
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application files
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "index.js"]Create docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- VONAGE_APPLICATION_ID=${VONAGE_APPLICATION_ID}
- VONAGE_FROM_NUMBER=${VONAGE_FROM_NUMBER}
- VONAGE_PRIVATE_KEY_PATH=/app/private.key
- API_KEY_1=${API_KEY_1}
- PORT=3000
volumes:
- ./private.key:/app/private.key:ro
restart: unless-stoppedCreate .env file for Docker Compose:
VONAGE_APPLICATION_ID=your_application_id
VONAGE_FROM_NUMBER=+14155550100
API_KEY_1=your_secret_api_keyBuild and run:
docker-compose up -dTest:
curl http://localhost:3000/healthCI/CD Pipeline
GitHub Actions example (.github/workflows/deploy.yml):
name: Deploy to Production
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
env:
VONAGE_APPLICATION_ID: ${{ secrets.VONAGE_APPLICATION_ID }}
VONAGE_PRIVATE_KEY_CONTENT: ${{ secrets.VONAGE_PRIVATE_KEY_CONTENT }}
VONAGE_FROM_NUMBER: ${{ secrets.VONAGE_FROM_NUMBER }}
API_KEY_1: ${{ secrets.API_KEY_1 }}
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Heroku
uses: akhileshns/heroku-deploy@v3.12.12
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: "your-app-name"
heroku_email: "your-email@example.com"Setup secrets in GitHub:
Go to Repository > Settings > Secrets and variables > Actions, add:
VONAGE_APPLICATION_IDVONAGE_PRIVATE_KEY_CONTENT(paste entire private key content)VONAGE_FROM_NUMBERAPI_KEY_1HEROKU_API_KEY
GitLab CI example (.gitlab-ci.yml):
stages:
- test
- deploy
test:
stage: test
image: node:18-alpine
script:
- npm ci
- npm run lint
- npm test
variables:
VONAGE_APPLICATION_ID: $VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_CONTENT: $VONAGE_PRIVATE_KEY_CONTENT
VONAGE_FROM_NUMBER: $VONAGE_FROM_NUMBER
API_KEY_1: $API_KEY_1
deploy:
stage: deploy
image: ruby:3.1
script:
- gem install dpl
- dpl --provider=heroku --app=your-app-name --api-key=$HEROKU_API_KEY
only:
- mainRollback Strategy
Heroku rollback:
# View recent releases
heroku releases
# Rollback to previous release
heroku rollback
# Rollback to specific version
heroku rollback v42Docker rollback:
# Tag images with version numbers
docker build -t your-app:v1.2.3 .
# Switch to previous version
docker-compose down
docker tag your-app:v1.2.2 your-app:latest
docker-compose up -dMonitoring and Observability
Recommended tools:
- Application monitoring: New Relic, Datadog, Sentry
- Log management: Logtail, Papertrail, CloudWatch Logs
- Uptime monitoring: UptimeRobot, Pingdom
- Error tracking: Sentry, Rollbar
Integrate Sentry for error tracking:
npm install @sentry/node// index.js (at the top)
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0,
});
// Add Sentry request handler after body parsers
app.use(Sentry.Handlers.requestHandler());
// Add routes here
// Add Sentry error handler before other error handlers
app.use(Sentry.Handlers.errorHandler());Set up health check monitoring:
Configure your monitoring service to ping https://your-app.com/health every 1–5 minutes and alert on failures.
Conclusion
You've built a production-ready Node.js and Express application that sends SMS messages programmatically via the Vonage Messages API. This comprehensive guide covered:
- Complete project setup with Node.js, Express, and Vonage SDK
- Secure credential management with environment variables
- Core SMS API implementation with robust error handling
- Security best practices (authentication, rate limiting, input validation)
- Special case handling (phone formatting, international sending, compliance)
- Comprehensive testing strategies (manual and automated)
- Production deployment with CI/CD pipelines and monitoring
Use this foundation to integrate SMS capabilities into your applications for notifications, OTP verification, two-factor authentication, alerts, and customer engagement.
Next steps:
- Implement inbound SMS handling for two-way conversations
- Add MMS support for sending images and media
- Integrate with WhatsApp Business API
- Build a message queue for high-volume sending
- Implement advanced analytics and reporting
- Explore Vonage Verify API for streamlined OTP workflows
Resources:
Frequently Asked Questions
How to send SMS with Node.js and Express
Use the Vonage Messages API and the @vonage/server-sdk. Set up an Express server, create a /send-sms endpoint, and use the SDK to send messages. Don't forget to configure your Vonage credentials in a .env file.
What is the Vonage Messages API?
The Vonage Messages API is a communication platform for sending SMS, MMS, WhatsApp messages, and more. It provides a simple and reliable way to integrate messaging into your applications, abstracting away the complexity of dealing with carriers directly.
Why use Node.js and Express for sending SMS?
Node.js, with its asynchronous nature, is efficient for I/O operations like API calls. Express simplifies routing and HTTP request handling, making it ideal for creating the API endpoint for sending SMS messages.
How to set up Vonage credentials for Node.js
Create a Vonage Application in the Vonage Dashboard, link a Vonage virtual number, and download your private key. Store your Application ID, Private Key Path, and Vonage Number in a .env file for secure access.
What is the purpose of private.key file?
The private.key file is used to authenticate your Node.js application with the Vonage API. It should be kept secure and never committed to version control. Its path is specified in the VONAGE_PRIVATE_KEY_PATH environment variable.
How to install necessary dependencies for Node.js SMS app
Use npm install express @vonage/server-sdk dotenv. This installs Express for the web server, the Vonage Server SDK for interacting with the API, and dotenv for managing environment variables.
How to create a Vonage Application?
Log in to your Vonage Dashboard, navigate to 'Applications', and click 'Create a new application'. Generate your public and private keys, enable the 'Messages' capability, and link a Vonage virtual number.
How to handle errors when sending SMS with Node.js
Implement a try-catch block around the vonage.messages.send() call. Return appropriate HTTP status codes (400/500) and informative JSON error messages for client-side and API errors. Consider adding retries for transient network issues.
How to implement input validation for /send-sms endpoint
Use middleware like Joi or express-validator to validate incoming phone numbers and message text. Check for required parameters and enforce limits on text length to ensure only valid data reaches the Vonage API.
What is the project structure for the Node.js SMS sender app
The main project files are index.js (containing the server logic), .env (for environment variables), .gitignore, package.json, and package-lock.json. The private.key is in the root directory.
How to deploy Node.js SMS application to Heroku
Use the Heroku CLI. Create a Procfile, set config vars for your Vonage credentials and private key path, commit changes, and push to Heroku. Ensure your private.key is handled securely during deployment.
When should I use application-level retries for sending SMS?
Implement retries with exponential backoff when network issues might disrupt communication between your server and the Vonage API. Use libraries like async-retry for easier implementation, but avoid retrying on certain client errors.
Why use dotenv in the Node.js SMS project
Dotenv loads environment variables from the .env file into process.env, allowing you to securely manage your Vonage API credentials and server configuration without exposing them in your code.
How to troubleshoot 'Credentials could not be found' error with Vonage
Double-check that your .env file has the correct VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, and that the private.key file exists at the specified path. Verify dotenv is correctly loaded in index.js.
What is rate limiting and why is it important for SMS sending
Rate limiting prevents abuse by restricting the number of requests from a single IP address within a time window. Use express-rate-limit to protect your Vonage account and API endpoint.