Send SMS with Node.js, Express, and Vonage: A Developer Guide
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage API. We'll cover everything from project setup and API key management to sending messages and handling potential issues.
By the end of this tutorial, you will have a functional Express API endpoint capable of accepting a phone number and message content, then using the Vonage Node.js SDK to dispatch the SMS. This serves as a foundational building block for applications requiring SMS notifications, alerts, two-factor authentication codes, or other communication features.
Project Overview and Goals
- Goal: To create a simple REST API endpoint using Node.js and Express that sends an SMS message to a specified recipient via the Vonage SMS API.
- Problem Solved: Enables developers to programmatically send SMS messages from their applications, automating communication workflows.
- Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework used to create the API endpoint.
- Vonage Node.js SDK: A library provided by Vonage to simplify interaction with their APIs, specifically the SMS API in this case.
dotenv
: A module to load environment variables from a.env
file, keeping sensitive credentials out of the codebase.
- Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. You can download them from nodejs.org.
- Vonage API Account: Sign up for free at Vonage API Dashboard. New accounts receive free credit for testing.
- A Text Editor or IDE: Such as VS Code, Sublime Text, or WebStorm.
- A Tool for Testing APIs: Such as
curl
, Postman, or Insomnia.
1. Setting up the Project
Let's start by creating our project directory and setting up the necessary dependencies and configuration.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it.
mkdir vonage-sms-guide cd vonage-sms-guide
-
Initialize Node.js Project: Initialize the project using npm. The
-y
flag accepts the default settings.npm init -y
This creates a
package.json
file. -
Install Dependencies: Install Express for the web server, the Vonage Server SDK for interacting with the API, and
dotenv
for managing environment variables.npm install express @vonage/server-sdk dotenv
-
Enable ES Modules: To use modern
import
/export
syntax, open yourpackage.json
file and add the""type"": ""module""
line. Update the dependencies section as shown below.{ ""name"": ""vonage-sms-guide"", ""version"": ""1.0.0"", ""description"": """", ""main"": ""index.js"", ""scripts"": { ""start"": ""node index.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""@vonage/server-sdk"": ""^3.13.0"", ""dotenv"": ""^16.4.5"", ""express"": ""^4.19.2"" }, ""type"": ""module"" }
(Note: The versions shown are examples. Your
package.json
will reflect the specific versions installed bynpm install
.) -
Create Environment File (
.env
): Create a file named.env
in the root of your project directory. This file will store your sensitive API credentials and configuration. Never commit this file to version control.# .env # Server Configuration PORT=3000 # Vonage API Credentials VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Vonage Sender Information # Use either a purchased Vonage number (in E.164 format, e.g., +14155552671) # or a registered Alphanumeric Sender ID (e.g., ""MyApp"") VONAGE_VIRTUAL_NUMBER=YOUR_VONAGE_NUMBER_OR_SENDER_ID
We'll populate the
VONAGE_
values in the next section. -
Create
.gitignore
File: To prevent accidentally committing sensitive files and unnecessary directories, create a.gitignore
file in the root of your project:# .gitignore # Dependencies node_modules/ # Environment variables .env # Log files *.log # Operating system files .DS_Store Thumbs.db
-
Project Structure: We will use a simple structure with two main files:
lib/sms.js
: Contains the logic for initializing the Vonage SDK and sending the SMS.index.js
: Sets up the Express server and defines the API endpoint.
Create the
lib
directory:mkdir lib
2. Integrating with Vonage
Now, let's get the necessary credentials and configure our Vonage account.
-
Retrieve API Credentials:
- Log in to your Vonage API Dashboard.
- On the main dashboard page, you should see your API key and API secret.
- Copy these values.
- Open your
.env
file and replaceYOUR_API_KEY
andYOUR_API_SECRET
with the actual values you just copied.
# .env (Example - Use your actual credentials) PORT=3000 VONAGE_API_KEY=a1b2c3d4 VONAGE_API_SECRET=e5f6g7h8i9j0k1l2 VONAGE_VIRTUAL_NUMBER=YOUR_VONAGE_NUMBER_OR_SENDER_ID
-
Configure Sender Number/ID: Vonage needs a ""from"" number or identifier for your SMS messages. You have two main options:
- Vonage Virtual Number: Purchase a virtual phone number through the dashboard (Numbers -> Buy numbers). These numbers support two-way messaging (sending and receiving). Use the number in E.164 format (e.g.,
+14155552671
). - Alphanumeric Sender ID: In supported countries, you can register a custom sender ID (like your brand name, e.g.,
""MyApp""
). This is for one-way messaging only. See Vonage documentation for registration requirements.
Update the
VONAGE_VIRTUAL_NUMBER
in your.env
file with your chosen number or Sender ID.# .env (Example using a US number) PORT=3000 VONAGE_API_KEY=a1b2c3d4 VONAGE_API_SECRET=e5f6g7h8i9j0k1l2 VONAGE_VIRTUAL_NUMBER=+14155552671
- Vonage Virtual Number: Purchase a virtual phone number through the dashboard (Numbers -> Buy numbers). These numbers support two-way messaging (sending and receiving). Use the number in E.164 format (e.g.,
-
Trial Account Limitation (Important Caveat):
- If you are using a free trial Vonage account, you can only send SMS messages to phone numbers that you have verified within the dashboard.
- To add and verify test numbers:
- Go to your Vonage Dashboard.
- Navigate to the ""Getting started"" section or look for a section like ""Test Numbers"" or ""Sandbox"". The exact location might change, but it's usually prominent for new accounts.
- Add the phone number(s) you want to send test messages to.
- Vonage will send a verification code via SMS or voice call to that number. Enter the code in the dashboard to complete verification.
- Attempting to send messages to unverified numbers on a trial account will result in a
""Non-Whitelisted Destination""
error. You must upgrade your account by adding payment details to send messages globally without whitelisting.
3. Implementing the SMS Sending Logic
Let's write the code that handles the interaction with the Vonage SDK.
Create the file lib/sms.js
:
// lib/sms.js
import { Vonage } from '@vonage/server-sdk';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Validate essential environment variables
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_VIRTUAL_NUMBER) {
console.error(""Error: Missing Vonage API credentials or sender number in .env file."");
process.exit(1); // Exit if critical config is missing
}
// Initialize Vonage client using API Key and Secret
// Note: This guide uses the legacy SMS API authentication method (API Key/Secret)
// via `vonage.sms.send()` for simplicity. Vonage generally recommends the
// newer Messages API (using Application ID/Private Key and `vonage.messages.send()`)
// for new applications as it supports more channels and features.
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET
});
const sender = process.env.VONAGE_VIRTUAL_NUMBER;
/**
* Sends an SMS message using the Vonage SMS API.
* @param {string} recipient - The recipient's phone number in E.164 format (e.g., +14155552671).
* @param {string} message - The text message content.
* @returns {Promise<object>} A promise that resolves with the Vonage API response data on success.
* @throws {Error} Throws an error if the SMS sending fails.
*/
export const sendSms = async (recipient, message) => {
console.log(`Attempting to send SMS from ${sender} to ${recipient}`);
// Input validation (basic)
if (!recipient || !message) {
throw new Error(""Recipient phone number and message text are required."");
}
// Basic E.164 format check (can be more robust)
if (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
console.warn(`Recipient number ${recipient} might not be in valid E.164 format.`);
// Depending on requirements, you might throw an error here instead.
// throw new Error(""Recipient phone number must be in E.164 format (e.g., +14155552671)."");
}
// Use the vonage.sms.send method (part of the older SMS API interface within the SDK)
return new Promise((resolve, reject) => {
vonage.sms.send(sender, recipient, message, (err, responseData) => {
if (err) {
console.error(""Vonage API Error:"", err);
// Provide a more specific error message if possible
reject(new Error(`Failed to send SMS via Vonage: ${err.message || err}`));
} else {
// Check the status of the first (and likely only) message in the response
if (responseData.messages[0]['status'] === ""0"") {
console.log(""SMS submitted successfully:"", responseData.messages[0]);
resolve(responseData); // Resolve with the full response data
} else {
const errorCode = responseData.messages[0]['status'];
const errorText = responseData.messages[0]['error-text'];
console.error(`Vonage Message Failed - Code: ${errorCode}, Error: ${errorText}`);
// Reject with a specific error message including details from Vonage
reject(new Error(`Vonage error ${errorCode}: ${errorText}`));
}
}
});
});
};
Explanation:
- Imports: We import
Vonage
from the SDK anddotenv
to load environment variables. - Configuration Loading:
dotenv.config()
loads variables from.env
. Basic checks ensure critical variables are present. - Vonage Initialization: We create a
Vonage
instance using theapiKey
andapiSecret
from the environment variables. A comment clarifies that this uses the legacy SMS API authentication and mentions the newer Messages API as the generally recommended alternative. sendSms
Function:- Takes
recipient
andmessage
as arguments. - Includes basic logging and input validation (checking for presence and a simple E.164 format regex).
- Wraps the
vonage.sms.send(...)
call in aPromise
for easierasync
/await
handling in the API layer. vonage.sms.send
takes the sender, recipient, message text, and a callback function.- Callback Handling:
- Checks for network/request errors (
err
). - If no request error, checks
responseData.messages[0]['status']
. A status of""0""
indicates success. - If the status is not
""0""
, it logs the error code and text provided by Vonage and rejects the promise with a descriptive error. - On success, it logs details and resolves the promise with the
responseData
.
- Checks for network/request errors (
- Takes
4. Building the Express API Endpoint
Now, let's create the server and the endpoint that will use our sendSms
function.
Create the file index.js
in the project root:
// index.js
import express from 'express';
import dotenv from 'dotenv';
import { sendSms } from './lib/sms.js'; // Import our SMS sending function
// Load environment variables
dotenv.config();
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 }));
// Simple Root Route for Health Check/Info
app.get('/', (req, res) => {
res.status(200).json({
message: ""Vonage SMS API Guide Server is running."",
sendEndpoint: ""/send-sms (POST)"",
requiredBody: { to: ""+14155552671"", text: ""Your message content"" }
});
});
/**
* API Endpoint: POST /send-sms
* Sends an SMS message via Vonage.
* Request Body: JSON { ""to"": ""recipient_number"", ""text"": ""message_content"" }
* - ""to"": Recipient phone number in E.164 format (e.g., +14155552671)
* - ""text"": The message body
* Responses:
* - 200 OK: { success: true, data: { message-count: '1', messages: [...] } }
* - 400 Bad Request: { success: false, message: ""Validation error details"" }
* - 500 Internal Server Error: { success: false, message: ""Error details from Vonage or server"" }
*/
app.post('/send-sms', async (req, res) => {
const { to, text } = req.body;
// --- Input Validation ---
if (!to || !text) {
return res.status(400).json({
success: false,
message: ""Missing required fields: 'to' (recipient phone number) and 'text' (message content) are required in the request body.""
});
}
// Optional: More specific validation (e.g., length checks, format) could go here
// Using the basic E.164 check inside sendSms for now.
console.log(`Received request to send SMS to: ${to}`);
try {
// --- Call the SMS Sending Logic ---
const result = await sendSms(to, text); // Uses the function from lib/sms.js
console.log(""Vonage API Response:"", result);
// --- Send Success Response ---
res.status(200).json({
success: true,
message: ""SMS submitted successfully to Vonage."",
data: result // Include the full response from Vonage
});
} catch (error) {
// --- Handle Errors ---
console.error(""Error in /send-sms endpoint:"", error);
// Default error message and status
let statusCode = 500;
let errorMessage = ""Failed to send SMS due to an internal server error."";
// Check if it's a Vonage-specific error message from our lib/sms.js rejection
// Note: Relying on string matching can be brittle; see Section 6 for improvements.
if (error.message.startsWith(""Vonage error"") || error.message.includes(""Failed to send SMS via Vonage"")) {
errorMessage = error.message;
// Potentially adjust status code based on Vonage error (e.g., 400 for bad number format if validation was stricter)
// For simplicity, keeping 500 for most Vonage backend errors unless clearly client-input related.
// The ""Non-Whitelisted Destination"" error is common on trial accounts.
if (error.message.includes(""Non-Whitelisted Destination"")) {
errorMessage = ""Failed to send SMS: The recipient number is not whitelisted for this trial account. Please verify the number in the Vonage dashboard."";
statusCode = 400; // Consider this a client-side setup issue
} else if (error.message.includes(""Invalid Credentials"")) {
errorMessage = ""Failed to send SMS: Invalid Vonage API Key or Secret. Please check your .env file."";
statusCode = 500; // Server config issue
}
} else if (error.message.includes(""Recipient phone number and message text are required"")) {
// Error from our basic validation within sendSms
statusCode = 400;
errorMessage = error.message;
}
res.status(statusCode).json({
success: false,
message: errorMessage
});
}
});
// Start the server
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}`);
console.log(`API Endpoint available at POST http://localhost:${PORT}/send-sms`);
});
Explanation:
- Imports: Import
express
and oursendSms
function. - Initialization: Load
dotenv
, create the Expressapp
, and define thePORT
. - Middleware: Use
express.json()
andexpress.urlencoded()
to parse incoming JSON and form data in request bodies. - Root Route (
/
): A simple GET endpoint to confirm the server is running. /send-sms
Endpoint (POST):- Defined using
app.post
. - Extracts
to
andtext
fromreq.body
. - Input Validation: Performs a basic check to ensure
to
andtext
are provided. Returns a400 Bad Request
if not. - Call
sendSms
: Calls the imported function within atry...catch
block to handle potential errors (both network errors and Vonage API errors returned via promise rejection). - Success Response: If
sendSms
resolves successfully, sends a200 OK
response containing the success status and the data received from the Vonage API. - Error Handling: If
sendSms
rejects, thecatch
block executes. It logs the error and sends an appropriate error response (usually500 Internal Server Error
, but checks for specific known error messages like validation failures or whitelisting issues to potentially return a400 Bad Request
).
- Defined using
- Server Start:
app.listen
starts the server on the specified port.
5. Verification and Testing
Let's run the application and test the endpoint.
-
Ensure
.env
is Correct: Double-check that your.env
file has the correctVONAGE_API_KEY
,VONAGE_API_SECRET
, andVONAGE_VIRTUAL_NUMBER
. Also, ensure the phone number you will send to is whitelisted if you're on a trial account. -
Run the Server: Open your terminal in the project root directory and run:
npm start
Or directly:
node index.js
You should see the output:
Server listening at http://localhost:3000 API Endpoint available at POST http://localhost:3000/send-sms
-
Test with
curl
: Open a new terminal window. Replace[REPLACE_WITH_YOUR_VERIFIED_NUMBER]
with the verified recipient phone number (in E.164 format, e.g.,+14155552671
) and adjust the message text as needed.curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""[REPLACE_WITH_YOUR_VERIFIED_NUMBER]"", ""text"": ""Hello from Node.js and Vonage!"" }'
-
Expected Success Response: If the request is successful, you should see output in the terminal where
curl
was run similar to this (themessage-id
,remaining-balance
, andmessage-price
will vary):{ ""success"": true, ""message"": ""SMS submitted successfully to Vonage."", ""data"": { ""message-count"": ""1"", ""messages"": [ { ""to"": ""1xxxxxxxxxx"", ""message-id"": ""a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6"", ""status"": ""0"", ""remaining-balance"": ""1.85000000"", ""message-price"": ""0.00650000"", ""network"": ""12345"" } ] } }
You should also receive the SMS on the recipient phone shortly after. The terminal running the Node.js server will show log messages.
-
Example Error Response (Missing Field): If you forget the
text
field:curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{""to"": ""[REPLACE_WITH_YOUR_VERIFIED_NUMBER]""}'
Response:
{ ""success"": false, ""message"": ""Missing required fields: 'to' (recipient phone number) and 'text' (message content) are required in the request body."" }
-
Example Error Response (Non-Whitelisted Number on Trial):
{ ""success"": false, ""message"": ""Failed to send SMS: The recipient number is not whitelisted for this trial account. Please verify the number in the Vonage dashboard."" }
6. Error Handling and Logging
- Current Implementation:
- The
lib/sms.js
function checks thestatus
code from the Vonage response and rejects the promise with a specific error message if it's not""0""
. - The
index.js
endpoint uses atry...catch
block to handle promise rejections fromsendSms
. - Basic input validation checks for required fields (
to
,text
) inindex.js
. - Specific error messages for common issues like missing fields, whitelisting problems, and invalid credentials are provided in the API response based on string matching of error messages.
console.log
andconsole.error
are used for logging basic information and errors to the terminal.
- The
- Production Enhancements:
- Structured Logging: Use a dedicated logging library like Pino or Winston for structured JSON logs, which are easier to parse and analyze. Include request IDs for tracing.
- Centralized Error Handling: Implement Express error-handling middleware for a more consistent way to catch and format errors across different routes.
- Detailed Vonage Errors: Consult the Vonage SMS API reference for a full list of status codes (
error-text
) and handle critical ones more explicitly if needed (e.g., insufficient funds, barring). - Robust Error Identification: Relying on string matching (
error.message.includes(...)
) can be brittle if error messages change. For production, consider using custom error classes thrown fromlib/sms.js
or checking specific error codes provided by the Vonage SDK (if available) for more reliable error type detection in thecatch
block. - Retry Mechanisms: For transient network errors or specific Vonage error codes indicating temporary issues, implement a retry strategy with exponential backoff using libraries like
async-retry
. Be careful not to retry errors caused by invalid input.
7. Security Considerations
- API Credentials: Crucially, keep your
VONAGE_API_KEY
andVONAGE_API_SECRET
secure. Use environment variables (.env
locally, secure configuration management in deployment) and never commit them to version control (.gitignore
). - Input Validation: The current validation is basic. In production, use a robust validation library like
joi
orexpress-validator
to strictly validate the format and length of theto
phone number andtext
content. Sanitize input if it's ever reflected back or stored. - Rate Limiting: Protect your endpoint from abuse and accidental loops by implementing rate limiting. Use middleware like
express-rate-limit
to restrict the number of requests a single IP address can make within a certain time window. - Authentication/Authorization: This example endpoint is open. In a real application, you must protect it. Implement authentication (e.g., API keys specific to your users, JWT tokens) to ensure only authorized clients can trigger SMS sends.
- HTTPS: Always run your Express application behind a reverse proxy (like Nginx or Caddy) configured with TLS/SSL (HTTPS) in production to encrypt traffic.
8. Troubleshooting and Caveats
Non-Whitelisted Destination
Error: The most common issue on trial accounts. Ensure the recipient number is added and verified in the Vonage dashboard under Test Numbers.Invalid Credentials
Error: Double-checkVONAGE_API_KEY
andVONAGE_API_SECRET
in your.env
file match the dashboard exactly. Ensure the.env
file is being loaded correctly (check for typos indotenv.config()
or file naming).- Incorrect
VONAGE_VIRTUAL_NUMBER
: Ensure the sender ID or number in.env
is valid, purchased/registered in your Vonage account, and correctly formatted (E.164 for numbers). - Invalid Recipient Number Format: Ensure the
to
number in your API request is in E.164 format (e.g.,+14155552671
). While our code warns, strict validation might be needed. - Insufficient Funds: If you have a paid account, check your balance in the Vonage dashboard.
- API Endpoint Not Found (404): Check the endpoint path (
/send-sms
), HTTP method (POST
), and the server port. - JSON Parsing Errors: Ensure the
Content-Type: application/json
header is sent with your request and the JSON body is valid. - Vonage API Downtime: While rare, check the Vonage Status Page if you suspect an outage. Implement retries for resilience.
- Node.js/npm Version Issues: Ensure you're using a compatible LTS version of Node.js.
9. Deployment (Conceptual)
Deploying this application involves running the Node.js process on a server and managing environment variables securely.
- Choose a Platform: Options include PaaS (Heroku, Vercel, Render), IaaS (AWS EC2, Google Cloud Compute Engine, DigitalOcean Droplets), or container orchestration (Docker/Kubernetes).
- Environment Variables: Do not upload your
.env
file. Use the platform's mechanism for setting environment variables (e.g., Heroku Config Vars, AWS Parameter Store/Secrets Manager, Docker environment flags). SetNODE_ENV=production
. - Build Step (Optional): If using TypeScript or a build process, ensure you run the build step before deploying.
- Process Manager: Use a process manager like PM2 to keep your Node.js application running reliably, manage logs, and handle restarts.
- Reverse Proxy (Recommended): Set up Nginx or Caddy as a reverse proxy to handle incoming requests, manage TLS/SSL termination (HTTPS), and potentially serve static files or perform load balancing.
- CI/CD: Implement a Continuous Integration/Continuous Deployment pipeline (e.g., GitHub Actions, GitLab CI, Jenkins) to automate testing and deployment.
Conclusion
You have successfully built a Node.js and Express application capable of sending SMS messages using the Vonage API with API Key/Secret authentication. We covered project setup, retrieving and securing API credentials, implementing the core sending logic with the Vonage SDK, creating an API endpoint, testing, and key considerations for error handling, security, and deployment.
This basic implementation serves as a solid foundation. You can extend it by:
- Implementing webhook endpoints to receive incoming SMS messages or delivery receipts.
- Adding more robust validation and error handling.
- Integrating a database to log sent messages.
- Building a user interface to trigger sends.
- Exploring other Vonage APIs (Voice, Verify, Video) or migrating to the Messages API for multi-channel support.
Further Reading
- Vonage Node.js SDK Repository
- Vonage SMS API Documentation (Legacy API used in this guide)
- Vonage Messages API Documentation (Recommended for new applications, uses Application ID/Private Key)
- Express.js Documentation