code examples
code examples
Building Two-Way SMS Messaging with Vonage Messages API, Node.js, and Express
Build production-ready two-way SMS with Vonage Messages API, Node.js, and Express. Covers JWT authentication, E.164 validation, webhooks, error handling, and security best practices.
Last Updated: October 5, 2025
This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to handle two-way SMS (Short Message Service) messaging via the Vonage Messages API (Application Programming Interface). You'll learn how to send outbound SMS messages and receive inbound messages through webhooks, enabling interactive communication.
Node.js Version Requirement: Node.js 18.x LTS (Long-Term Support) – Active LTS or Node.js 20.x LTS – Maintenance LTS or Node.js 22.x LTS – Active LTS. Node.js 24 entered Current status on May 6, 2025. Production applications should use Active LTS or Maintenance LTS releases.
Source: Node.js Release Schedule (nodejs.org/en/about/previous-releases, verified October 2025)
By the end of this tutorial, you will have a functional Node.js server capable of:
- Sending SMS messages programmatically using the Vonage Node.js SDK (Software Development Kit) (
@vonage/server-sdk). - Receiving incoming SMS messages sent to your Vonage virtual number via webhooks.
- Automatically replying to incoming SMS messages.
- Handling message delivery status updates.
This guide focuses on a practical setup, outlining steps towards production readiness. Essential security features for production, such as webhook signature verification, are discussed later as crucial additions to the basic implementation shown here.
Project overview and goals
Create a simple web service that leverages Vonage's communication capabilities to facilitate two-way SMS conversations. This solves the common need for applications to interact with users via SMS for notifications, alerts, customer support, or simple command processing.
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 API endpoints (webhooks).
- Vonage Messages API: A unified API for sending and receiving messages across various channels (focus on SMS).
- Vonage Node.js SDK (
@vonage/server-sdk): Simplifies interaction with Vonage APIs. dotenv: A module to load environment variables from a.envfile.ngrok: A tool to expose your local development server to the internet, allowing Vonage webhooks to reach it.
System architecture:
+-------------+ +-------------------+ +--------+ +---------------------+ +-------+
| User's Phone| ---- | Carrier Network | ---- | Vonage | ---- | Your Node.js/Express| ---- | User |
| (Sends SMS) | | | | (SMS) | | App (Webhook) | | |
+-------------+ +-------------------+ +--------+ +---------------------+ +-------+
^ | | ^
| (Receives SMS) | (Sends SMS) | (Sends Reply) | (Triggers Send)
+---------------------------------------------+---------------------+----------------------+- Outbound: Your application uses the Vonage SDK to send an SMS message via the Vonage platform to the user's phone.
- Inbound: A user sends an SMS to your Vonage virtual number. Vonage forwards this message via an HTTP POST request (webhook) to your application's designated endpoint.
- Reply: Your application receives the inbound webhook, processes the message, and can optionally use the Vonage SDK again to send a reply back to the user's phone number.
Prerequisites:
- Node.js and npm (or yarn): Node.js 18.x LTS, 20.x LTS, or 22.x LTS installed on your system. Download Node.js
- Vonage API Account: A free account is sufficient to start. Sign up for Vonage.
- Vonage CLI (Command-Line Interface): Installed globally (
npm install -g @vonage/cli). ngrok: Installed and authenticated. Download ngrok. A free account is sufficient. Note: Free ngrok sessions expire after 2 hours and generate new URLs on restart, requiring webhook URL updates in Vonage dashboard.- A text editor or IDE (Integrated Development Environment), e.g., VS Code.
- Basic understanding of JavaScript and Node.js concepts.
- Phone Number Format: All phone numbers must be in E.164 format (ITU-T Recommendation E.164: 7 – 15 digits, country code first, no leading + or 00 in API requests). Example:
447700900000for UK,14155550100for US.
Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025)
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it.
bashmkdir vonage-sms-app cd vonage-sms-app -
Initialize Node.js Project: Initialize the project using npm. The
-yflag accepts default settings.bashnpm init -yThis creates a
package.jsonfile. -
Install Dependencies: Install Express for the web server, the Vonage Server SDK for interacting with the API, and
dotenvfor managing environment variables.bashnpm install express @vonage/server-sdk dotenv -
Set up Environment Variables: Create a file named
.envin the root of your project directory. This file will store sensitive credentials and configuration. Never commit this file to version control.Code# .env # Vonage API Credentials (Optional for Messages API w/ Private Key, but potentially useful) VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Vonage Application Credentials (Required for Messages API) VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root where script is run # Vonage Virtual Number (Purchase this via Dashboard or CLI) VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Application Port APP_PORT=3000VONAGE_API_KEY,VONAGE_API_SECRET: Found directly on your Vonage API Dashboard. While the Messages API primarily uses the Application ID and Private Key for authentication, providing the API Key and Secret here allows the SDK to potentially perform other account-level actions if needed. They are generally not strictly required for sending/receiving messages when using Application authentication.VONAGE_APPLICATION_ID,VONAGE_PRIVATE_KEY_PATH: Required for the Messages API. We'll generate these shortly. The Application ID uniquely identifies your Vonage application configuration. The private key authenticates requests specific to this application. TheVONAGE_PRIVATE_KEY_PATHshould be the path to the key file relative to the directory where you run your Node.js script (typically the project root).VONAGE_NUMBER: The virtual phone number you rent from Vonage, capable of sending/receiving SMS. We'll acquire this next.APP_PORT: The local port your Express server will listen on.
-
Configure
.gitignore: Create a.gitignorefile in the project root to prevent committing sensitive files and unnecessary directories.Code# .gitignore node_modules .env private.key npm-debug.log *.log -
Acquire Vonage Credentials and Number:
-
API Key and Secret: Log in to your Vonage API Dashboard. Your API Key and Secret are displayed at the top. Copy these into your
.envfile if you wish to include them. -
Set Default SMS API: Crucially, navigate to Account Settings in the Vonage Dashboard. Scroll down to ""API Keys"" settings, find ""Default SMS Setting"" and ensure Messages API is selected. Save changes. This ensures webhooks use the Messages API format.
-
Purchase a Virtual Number: You need a Vonage number to send and receive SMS. You can buy one via the dashboard (Numbers -> Buy Numbers) or using the Vonage CLI (ensure you are logged in via
vonage loginfirst):bash# Replace XX with the desired 2-letter country code (e.g., US, GB, CA) vonage numbers:buy --country XX --features SMS --confirmCopy the purchased number (including the country code, e.g.,
14155550100) into theVONAGE_NUMBERfield in your.envfile.
-
-
Create a Vonage Application: The Messages API requires a Vonage Application to associate configuration (like webhook URLs) and authentication (via public/private keys).
- Go to your Vonage Dashboard and click ""Create a new application"".
- Give your application a meaningful name (e.g., ""Node Two-Way SMS App"").
- Click ""Generate public and private key"". Immediately save the
private.keyfile that downloads into the root of your project directory (vonage-sms-app/private.key). The public key is stored by Vonage. - Enable the Messages capability.
- You'll see fields for ""Inbound URL"" and ""Status URL"". We need
ngrokrunning first to fill these. Leave them blank for now, but keep this page open or note down the Application ID generated on this page. - Scroll down to ""Link virtual numbers"" and link the Vonage number you purchased earlier to this application.
- Click ""Create application"".
- Copy the generated Application ID into the
VONAGE_APPLICATION_IDfield in your.envfile.
-
Expose Local Server with
ngrok: To allow Vonage's servers to send webhook events (like incoming messages) to your local machine during development, we usengrok.-
Open a new terminal window/tab (keep your project terminal open).
-
Run
ngrok, telling it to forward traffic to the port your application will run on (defined in.envasAPP_PORT=3000).bashngrok http 3000 -
ngrokwill display output including a Forwarding URL (e.g.,https://<random-string>.ngrok-free.app). Copy thehttpsversion of this URL. This is your public base URL.
-
-
Configure Webhook URLs in Vonage Application:
- Go back to the Vonage Application you created (Dashboard -> Applications -> Your App Name -> Edit).
- Paste your
ngrokForwarding URL into the Inbound URL field and append/webhooks/inbound. Example:https://<random-string>.ngrok-free.app/webhooks/inbound - Paste your
ngrokForwarding URL into the Status URL field and append/webhooks/status. Example:https://<random-string>.ngrok-free.app/webhooks/status - Click ""Save changes"".
Now your Vonage application is configured to send incoming SMS messages and status updates to your (soon-to-be-running) local server via the ngrok tunnel.
2. Implementing core functionality: Sending and receiving SMS
Let's write the Node.js code using Express.
-
Create Server File: Create a file named
server.jsin your project root. -
Initialize Server and Vonage Client: Add the following code to
server.jsto set up the Express server, load environment variables, and initialize the Vonage client.javascript// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); // --- Configuration --- const app = express(); const port = process.env.APP_PORT || 3000; // Use port from .env or default to 3000 // --- Middleware --- // Parse incoming requests with JSON payloads app.use(express.json()); // Parse incoming requests with URL-encoded payloads app.use(express.urlencoded({ extended: true })); // --- Vonage Client Initialization --- // Use Application ID and Private Key for Messages API authentication const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, // Optional: For potential other SDK uses apiSecret: process.env.VONAGE_API_SECRET, // Optional: For potential other SDK uses applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH // SDK handles loading the key from this path }, { // Optional: Add custom logger or other options here // logger: customLogger }); // --- Helper Function for Sending SMS --- async function sendSms(to, text) { // Basic validation if (!to || !text) { console.error('Send SMS Error: Missing "to" or "text" parameter.'); return; // Avoid sending invalid requests } if (!process.env.VONAGE_NUMBER) { console.error('Send SMS Error: VONAGE_NUMBER environment variable not set.'); return; } // E.164 format validation: remove + prefix if present, verify 7-15 digits const cleanNumber = to.replace(/^\+/, ''); if (!/^\d{7,15}$/.test(cleanNumber)) { console.error(`Send SMS Error: Invalid E.164 format for number "${to}". Must be 7-15 digits (country code + number), no spaces or special characters.`); return; } try { const resp = await vonage.messages.send({ channel: 'sms', message_type: 'text', to: cleanNumber, // Recipient phone number (E.164 without + prefix) from: process.env.VONAGE_NUMBER.replace(/^\+/, ''),// Your Vonage virtual number (E.164 without + prefix) text: text // The message content }); console.log(`SMS sent successfully to ${to}. Message UUID: ${resp.message_uuid}`); return resp; // Return the response object if needed } catch (err) { console.error(`Error sending SMS to ${to}:`, err?.response?.data || err.message || err); // More specific error handling based on err.response.status might be needed // e.g., if (err.response.status === 401) { handle auth error } } } // --- Webhook Endpoints --- // Inbound SMS Webhook app.post('/webhooks/inbound', (req, res) => { console.log('--- Inbound SMS Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the full payload const { from, message } = req.body; // Basic validation of expected structure if (!from?.number || !message?.content?.text) { console.warn('Received incomplete inbound message payload.'); // Still send 200 OK to prevent Vonage retries for malformed requests res.status(200).send('OK'); return; } const senderNumber = from.number; const receivedText = message.content.text; console.log(`Message received from ${senderNumber}: ""${receivedText}""`); // --- Two-Way Logic: Respond to the incoming message --- const replyText = `Thanks for your message: ""${receivedText}"". We received it!`; console.log(`Sending reply to ${senderNumber}: ""${replyText}""`); // Send the reply asynchronously (don't wait for it before responding to Vonage) sendSms(senderNumber, replyText) .then(() => console.log(`Reply initiated to ${senderNumber}`)) .catch(err => console.error(`Failed to initiate reply to ${senderNumber}:`, err)); // Log reply failure separately // --- IMPORTANT: Acknowledge receipt to Vonage --- // Vonage expects a 200 OK response quickly to know the webhook was received. // Failure to respond promptly will result in retries. res.status(200).send('OK'); }); // Message Status Webhook app.post('/webhooks/status', (req, res) => { console.log('--- Message Status Update Received ---'); console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Log the full payload const { message_uuid, status, timestamp, to, error } = req.body; console.log(`Status for message ${message_uuid} to ${to}: ${status} at ${timestamp}`); if (error) { console.error(`Error details: Code ${error.code}, Reason: ${error.reason}`); // Application-specific logic can be added here based on status // e.g., update database record for the message_uuid } // Acknowledge receipt to Vonage res.status(200).send('OK'); }); // --- Root Endpoint (Optional: for basic testing) --- app.get('/', (req, res) => { res.send(`Vonage SMS App is running! Send an SMS to ${process.env.VONAGE_NUMBER} to test.`); }); // --- Start Server --- app.listen(port, () => { const now = new Date(); const dateStr = now.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); const timeStr = now.toLocaleTimeString(); console.log(`Server listening at http://localhost:${port}`); console.log(`Vonage number: ${process.env.VONAGE_NUMBER || 'Not Set'}`); console.log(`Webhook endpoints configured for ngrok URL (ensure ngrok is running on port ${port}):`); console.log(` Inbound: POST /webhooks/inbound`); console.log(` Status: POST /webhooks/status`); console.log(`(${dateStr} ${timeStr}) - Ready to receive messages.`); }); // --- Example: Sending an initial SMS on startup (for testing) --- // Comment this out for production deployment if not needed /* const testRecipient = 'REPLACE_WITH_YOUR_PERSONAL_PHONE_NUMBER'; // e.g., '15551234567' if (testRecipient.startsWith('REPLACE')) { console.warn(""Please replace 'REPLACE_WITH_YOUR_PERSONAL_PHONE_NUMBER' to test sending SMS on startup.""); } else { console.log(`Sending test SMS to ${testRecipient}...`); sendSms(testRecipient, 'Hello from the Vonage Node.js App!'); } */ // --- Graceful Shutdown (Optional but Recommended) --- process.on('SIGINT', () => { console.log('\nSIGINT received. Shutting down gracefully...'); // Add any cleanup logic here (e.g., close database connections) process.exit(0); });
Code explanation:
- Dependencies & Config: Loads
dotenv, importsexpressandVonage, sets up the Express app, and defines theport. - Middleware:
express.json()andexpress.urlencoded()are essential for parsing the incoming webhook request bodies sent by Vonage. - Vonage Client: Initialized using the
applicationIdand the path to theprivateKeyfile (VONAGE_PRIVATE_KEY_PATH). The SDK handles reading the key from the specified path. API Key/Secret are included optionally. sendSmsFunction: An asynchronous helper function that encapsulates the logic for sending an SMS usingvonage.messages.send(). It includes basic parameter validation and error handling using a try-catch block. It specifieschannel: 'sms'andmessage_type: 'text'./webhooks/inbound: This is the core endpoint for receiving SMS messages.- It listens for POST requests at the path configured in the Vonage dashboard.
- It logs the incoming request body (
req.body) for debugging. The structure contains details likefrom.number(sender) andmessage.content.text(message body). - It extracts the sender's number and the message text.
- Crucially, it sends a
200 OKstatus back to Vonage immediately usingres.status(200).send('OK');. This confirms receipt; without it, Vonage will retry sending the webhook. - It then constructs a reply message and calls the
sendSmsfunction asynchronously to send the reply back to the original sender. Sending the reply happens after acknowledging the webhook.
/webhooks/status: This endpoint receives delivery status updates for messages you've sent.- It logs the status payload, which includes the
message_uuid, the finalstatus(e.g.,delivered,failed,rejected), the recipient number (to), and potentialerrordetails. - It also sends a
200 OKresponse. You would typically add logic here to update message delivery status in a database or trigger alerts on failure.
- It logs the status payload, which includes the
- Root Endpoint (
/): A simple GET endpoint to verify the server is running via a web browser. - Server Start:
app.listen()starts the server on the configured port and logs useful startup information including the current date and time. - Test SMS (Commented Out): Includes an example of how to use
sendSmsdirectly, useful for initial testing but should be removed or adapted for production. - Graceful Shutdown: Basic handling for
SIGINT(Ctrl+C) to allow for cleanup if needed.
3. Building a complete API layer
The webhook endpoints (/webhooks/inbound, /webhooks/status) effectively form the API layer for interacting with Vonage.
- Authentication/Authorization: Vonage Messages API v1.0 supports both JWT (JSON Web Token) and Basic authentication. For webhook requests from Vonage to your application, you must implement JWT Signature Verification in production. Vonage signs webhook requests using JWT with your application's private key. You verify this signature to ensure the request genuinely originated from Vonage and wasn't tampered with.
JWT Signature Verification Implementation:
// Add this to your webhook handlers for production
app.post('/webhooks/inbound', (req, res) => {
// Verify JWT signature from Authorization header
const token = req.headers['authorization'];
if (!token) {
console.warn('Missing authorization header in webhook request');
return res.status(401).send('Unauthorized');
}
try {
// Verify JWT signature using Vonage SDK
// The SDK uses your application's public key (stored by Vonage) to verify
const isValid = vonage.credentials.verifySignature(token);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
// Signature verified, proceed with webhook processing
console.log('--- Inbound SMS Received (Signature Verified) ---');
// ... rest of your webhook handler code
} catch (error) {
console.error('Signature verification error:', error);
return res.status(401).send('Unauthorized');
}
// ... existing webhook handler code
});Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025), Vonage webhook security best practices
-
Request Validation: Basic validation is included (checking for
from.numberandmessage.content.text). For production, use a dedicated validation library (likeJoiorexpress-validator) to define schemas for the expected webhook payloads and reject unexpected or malformed requests early. -
Webhook Payload Structure:
- Inbound webhook includes:
channel(e.g., "sms"),message_uuid(UUID format),to(7-15 digits),from(7-15 digits),timestamp(ISO 8601 format, e.g., "2025-02-03T12:14:25Z"),text(UTF-8 encoded),sms.num_messages(concatenation count),usage.currency(ISO 4217, e.g., "EUR"),usage.price(stringified decimal) - Status webhook includes:
message_uuid,to,from,timestamp(ISO 8601),status(submitted/delivered/rejected/undeliverable),error.type(URL to error details),error.title(error code),error.detail(description),channel,sms.count_total(SMS segments)
- Inbound webhook includes:
Source: Vonage Messages API v1.0 Webhook Reference (developer.vonage.com/en/api/messages-olympus, verified October 2025)
- API Endpoint Documentation:
POST /webhooks/inbound: Receives inbound SMS messages.- Request Body (JSON): See Vonage Messages API Inbound Message Webhook Reference. Key fields:
from.type,from.number,to.type,to.number,message_uuid,message.content.type,message.content.text,timestamp. - Response:
200 OK(Empty body or text "OK").
- Request Body (JSON): See Vonage Messages API Inbound Message Webhook Reference. Key fields:
POST /webhooks/status: Receives message status updates.- Request Body (JSON): See Vonage Messages API Message Status Webhook Reference. Key fields:
message_uuid,to.type,to.number,from.type,from.number,timestamp,status,usage,error. - Response:
200 OK(Empty body or text "OK").
- Request Body (JSON): See Vonage Messages API Message Status Webhook Reference. Key fields:
4. Integrating with Vonage (Covered in Setup)
The integration steps involving API keys, application creation, number purchasing/linking, and webhook configuration were covered in the ""Setting up the project"" section. Secure handling of API keys and the private key is achieved by using environment variables (dotenv) and ensuring .env and private.key are in .gitignore.
5. Implementing error handling and logging
- Error Handling Strategy:
- Use
try...catchblocks around asynchronous operations, especially Vonage API calls (sendSms). - Log errors clearly using
console.error, including relevant context (e.g., recipient number, operation attempted). - For webhook handlers (
/webhooks/inbound,/webhooks/status), always send a200 OKresponse to Vonage, even if internal processing fails after receiving the request. Log the internal error separately. This prevents unnecessary retries from Vonage flooding your server. If the request itself is invalid before processing, a4xxmight be appropriate, but generally,200 OKis safest for acknowledged receipt. - Check for specific error conditions from Vonage responses if needed (e.g.,
err.response.statusorerr.response.datafrom the SDK).
- Use
Common Vonage API Error Codes:
| Error Code | Description | Recommended Action |
|---|---|---|
| 1000 | Throttled - Exceeded submission capacity | Implement exponential backoff, retry after delay |
| 1010 | Missing params - Incomplete request | Validate all required parameters before API call |
| 1020 | Invalid params - Parameter value invalid | Check parameter format (E.164, character limits) |
| 1120 | Illegal Sender Address - SenderID rejected | Use purchased Vonage number, check regional restrictions |
| 1170 | Invalid or Missing MSISDN - Phone number invalid/missing | Validate E.164 format (7-15 digits) |
| 1210 | Anti-Spam Rejection - Content/SenderID blocked by carrier | Review message content, avoid spam triggers |
| 1240 | Illegal Number - Recipient opted out (STOP) | Remove from contact list, maintain suppression database |
| 1250 | Unroutable - Number on unsupported network | Verify number validity, check carrier support |
| 1330 | Unknown - Carrier error or invalid recipient | Verify recipient number format and validity |
| 1460 | Daily message limit exceeded - 10DLC compliance | Check compliance registration, monitor daily quotas |
| 1470 | Fraud Defender Traffic Rule - Prefix blocked | Review traffic rules, contact support if legitimate |
Source: Vonage Messages API v1.0 Error Codes (developer.vonage.com/en/api/messages-olympus, verified October 2025)
-
Logging:
-
The current implementation uses
console.logandconsole.error. For production, use a more robust logging library likepinoorwinston. -
Configure log levels (e.g.,
info,warn,error,debug). -
Output logs in a structured format (like JSON) for easier parsing by log analysis tools.
-
Include timestamps and potentially request IDs in logs.
-
Example (Conceptual using Pino):
javascript// const pino = require('pino'); // const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); // ... replace console.log with logger.info, console.error with logger.error etc. // logger.info({ reqBody: req.body }, 'Inbound SMS Received');
-
-
Retry Mechanisms: Vonage handles retries for webhook delivery if it doesn't receive a
200 OKresponse. For outgoing messages (sendSms) that fail due to potentially transient network issues or Vonage service errors (e.g.,5xxstatus codes), you could implement a retry strategy with exponential backoff within yoursendSmsfunction or using a dedicated library likeasync-retry. However, be cautious about retrying errors related to invalid numbers or insufficient funds (4xxerrors).
6. Creating a database schema (Optional - Beyond Scope)
This basic guide doesn't include database integration. For a production application, you would typically store:
-
Messages: Incoming and outgoing messages (sender, recipient, text, timestamp, Vonage message UUID, status).
-
Conversations: Grouping messages by participants.
-
Users/Contacts: If managing known contacts.
-
Schema (Conceptual - PostgreSQL):
sqlCREATE TABLE messages ( message_id SERIAL PRIMARY KEY, vonage_message_uuid VARCHAR(255) UNIQUE, direction VARCHAR(10) NOT NULL, -- 'inbound' or 'outbound' sender_number VARCHAR(20) NOT NULL, recipient_number VARCHAR(20) NOT NULL, message_text TEXT, status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, read vonage_status_timestamp TIMESTAMPTZ, error_code VARCHAR(50), error_reason TEXT, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Index for querying by status or number CREATE INDEX idx_messages_status ON messages(status); CREATE INDEX idx_messages_recipient ON messages(recipient_number); CREATE INDEX idx_messages_sender ON messages(sender_number); -
Data Layer: Use an ORM (like
SequelizeorPrisma) or a query builder (Knex.js) to interact with the database, handle migrations, and manage connections. Update webhook handlers to save/update message records.
7. Adding security features
- Input Validation: As mentioned, use libraries like
Joiorexpress-validatorin webhook handlers to validate the structure and types of incoming data (req.body). Sanitize any data before storing or using it in replies if necessary, although SMS content is often treated as plain text. - Webhook Security: Implement Webhook Signature Verification (see Section 3) as the primary defense against spoofed requests. This is essential for production.
- API Key/Secret Security: Use
dotenvand.gitignoreto protect credentials. Use tools likegit-secretsto prevent accidental commits of secrets. Consider using a dedicated secrets management service in production (e.g., AWS Secrets Manager, HashiCorp Vault). - Rate Limiting: Implement rate limiting on your webhook endpoints using middleware like
express-rate-limitto prevent abuse or denial-of-service attacks. Configure sensible limits based on expected traffic. - Common Vulnerabilities (General Node/Express): Keep dependencies updated (
npm audit), use security headers (helmetmiddleware), protect against Cross-Site Scripting (XSS) if rendering user content in web views (not applicable here), and Cross-Site Request Forgery (CSRF) if you have web forms (not applicable here).
8. Handling special cases
- Character Encoding & Emojis: The Vonage Messages API handles UTF-8 encoding automatically. The
textparameter supports up to 1000 characters. The API automatically detects unicode characters and adjusts encoding accordingly (GSM-7 for standard text, UCS-2 for unicode/emojis) unless explicitly set viasms.encoding_typeparameter (options: "text", "unicode", "auto"). Spanish, French, German, and other European language characters with diacritics are typically supported in GSM-7 encoding.
SMS Concatenation: Messages exceeding single SMS limits are automatically split:
- GSM-7 encoding: 160 characters per single SMS, 153 characters per segment for concatenated messages
- UCS-2 encoding (unicode/emojis): 70 characters per single SMS, 67 characters per segment
Source: Vonage Messages API v1.0 documentation (developer.vonage.com/en/api/messages-olympus, verified October 2025)
-
Message Time-To-Live (TTL): The
ttlparameter (in seconds) controls delivery attempt duration. Default is 72 hours (259200 seconds), but effective maximum depends on carrier (typically 24-48 hours). Minimum recommended: 1800 seconds (30 minutes). Example:ttl: 90000(25 hours). -
Multipart SMS: Longer messages are automatically split by Vonage. The Messages API handles this transparently for sending. For inbound multipart messages, Vonage typically delivers them as a single webhook request with concatenated text. The
sms.num_messagesfield in the inbound webhook indicates how many SMS segments were concatenated. -
Alphanumeric Sender IDs: For outbound SMS,
fromcan sometimes be a text string (e.g., "MyBrand") instead of a number in supported countries. Check Vonage documentation and local regulations. US numbers generally require sending from a purchased Vonage number. Alphanumeric sender IDs are one-way only (no replies possible). -
International Numbers: Ensure numbers are in E.164 format (7-15 digits, country code first, no + or 00 prefix in API requests). Examples:
447700900000(UK),14155550100(US),61412345678(Australia),5511998765432(Brazil).
Source: ITU-T Recommendation E.164 (International numbering plan), Vonage API documentation
- Stop/Help Keywords: Be aware of carrier requirements and regulations regarding opt-out keywords (STOP, UNSUBSCRIBE) and help keywords (HELP). Vonage may offer features to manage opt-outs automatically, or you may need to implement logic in your inbound webhook to detect these keywords and take appropriate action (e.g., adding the number to a blocklist, sending a standard help response). For US 10DLC compliance, STOP/HELP keyword support is mandatory.
9. Implementing performance optimizations
- Asynchronous Operations: The code uses
async/awaitand handles the reply sending asynchronously after responding200 OKto the webhook. This is crucial for performance and responsiveness. - Webhook Response Time: Responding
200 OKquickly to webhooks is paramount. Avoid long-running synchronous operations within the webhook handler before sending the response. Offload heavy processing to background jobs if necessary (e.g., using message queues like RabbitMQ or Redis queues). - Node.js Clustering: For handling higher loads, use Node.js's built-in
clustermodule or a process manager likePM2in cluster mode to run multiple instances of your application across CPU cores. - Caching: If fetching user data or templates frequently, implement caching (e.g., using Redis or Memcached) to reduce database load. Not critical for this simple example.
- Resource Usage: Monitor CPU and memory usage. Optimize database queries if using a database.
10. Adding monitoring, observability, and analytics
- Health Checks: Add a simple health check endpoint (e.g.,
GET /health) that returns200 OKif the server is running and can connect to essential services (like Vonage, if possible, or a database). Monitoring services can ping this endpoint. - Performance Metrics: Use libraries like
prom-clientto expose application metrics (request latency, error rates, throughput) in a Prometheus-compatible format. Monitor Node.js event loop lag. - Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) to capture, aggregate, and alert on unhandled exceptions and logged errors.
- Logging & Dashboards: Ship logs (using a library like Pino configured for JSON output) to a centralized logging platform (e.g., Elasticsearch/Logstash/Kibana (ELK), Datadog Logs, Splunk). Create dashboards to visualize key metrics like inbound/outbound message volume, error rates, delivery rates (from status webhooks), and response times.
- Vonage Dashboard: Utilize the Vonage Dashboard's analytics and logs for insights into API usage, message delivery, and costs.
11. Troubleshooting and Caveats
ngrokIssues:- Not Running/Wrong Port: Ensure
ngrok http 3000(or yourAPP_PORT) is running in a separate terminal. - URL Expired: Free
ngrokURLs expire after a session or time limit. Restartngrokand update the webhook URLs in the Vonage dashboard if needed. Paid plans offer stable subdomains. - Firewall: Local or network firewalls might block
ngrok. Check your settings.
- Not Running/Wrong Port: Ensure
- Webhook Not Received:
- Check
ngrokis running and the URL is correct (HTTPS) in the Vonage Application settings (Inbound/Status URLs). - Verify the server is running (
node server.js) and listening on the correct port. - Check the
ngrokweb interface (http://127.0.0.1:4040) for incoming requests. If they appear there but not in your server logs, check server-side routing and middleware. - Check the Vonage Dashboard logs for webhook delivery failures or errors.
- Check
Frequently Asked Questions
How do I send SMS messages using Vonage Messages API with Node.js?
Send SMS messages using Vonage Messages API with Node.js by: (1) Install the @vonage/server-sdk package via npm, (2) Create a Vonage application in the dashboard and download the private.key file, (3) Purchase a Vonage virtual number with SMS capabilities, (4) Initialize the Vonage client with your Application ID and private key path, (5) Use vonage.messages.send() with parameters: channel: 'sms', message_type: 'text', to (recipient in E.164 format: 7–15 digits, no + prefix), from (your Vonage number), and text (message content up to 1000 characters). The API supports both JWT (JSON Web Token) and Basic authentication. Example: await vonage.messages.send({ channel: 'sms', message_type: 'text', to: '14155550100', from: '14155559999', text: 'Hello from Vonage!' }). The response includes a message_uuid for tracking. Node.js 18.x LTS, 20.x LTS, or 22.x LTS required. Automatic encoding detection handles GSM-7 (160 chars) and UCS-2/unicode (70 chars) with automatic message concatenation for longer content.
How do I set up webhooks to receive inbound SMS messages from Vonage?
Set up Vonage SMS webhooks by: (1) Create a Vonage Application in the dashboard (Dashboard → Applications → Create), (2) Enable the Messages capability, (3) Configure webhook URLs – set Inbound URL to https://your-domain.com/webhooks/inbound and Status URL to https://your-domain.com/webhooks/status, (4) For local development, use ngrok (ngrok http 3000) to expose your local server and use the generated HTTPS URL (note: free ngrok URLs expire after 2 hours), (5) Create Express POST endpoints matching your webhook URLs, (6) Parse incoming JSON with express.json() middleware, (7) Extract data from req.body (inbound webhook includes from.number, message.content.text, message_uuid, timestamp in ISO 8601 format), (8) Crucial: Always respond with 200 OK immediately to prevent Vonage retries (Vonage retries failed webhook deliveries), (9) Process messages asynchronously after sending the response. Webhook payloads include channel, message_uuid (UUID format), phone numbers in E.164 format (7–15 digits), timestamp (ISO 8601), and usage data (currency in ISO 4217, price as stringified decimal).
What is E.164 phone number format and how do I validate it for Vonage?
E.164 format is the ITU-T international phone numbering standard requiring: (1) 7–15 total digits (country code + subscriber number), (2) Country code first (e.g., 1 for US/Canada, 44 for UK, 61 for Australia), (3) No leading + or 00 prefix in Vonage API requests (unlike display format), (4) No spaces, hyphens, or special characters. Validation regex: /^\d{7,15}$/ after removing + prefix. Examples: 14155550100 (US), 447700900000 (UK), 5511998765432 (Brazil). Implementation: const cleanNumber = phoneNumber.replace(/^\+/, ''); if (!/^\d{7,15}$/.test(cleanNumber)) { throw new Error('Invalid E.164 format'); }. Common mistakes: (1) Including + prefix in API calls (accepted in display only), (2) Not removing spaces/formatting, (3) Wrong digit count (too short/long), (4) Missing country code. Vonage API error 1170 ("Invalid or Missing MSISDN") indicates E.164 validation failure. Use E.164 format consistently across all Vonage Messages API calls for to and from parameters.
How do I implement JWT webhook signature verification for Vonage?
Implement JWT (JSON Web Token) signature verification for Vonage webhooks by: (1) Extract JWT token from the Authorization header in incoming webhook requests, (2) Verify the signature using Vonage SDK method vonage.credentials.verifySignature(token), (3) Return 401 Unauthorized if token is missing or invalid, (4) Only process webhook payload after successful verification. Implementation: const token = req.headers['authorization']; if (!token) return res.status(401).send('Unauthorized'); try { const isValid = vonage.credentials.verifySignature(token); if (!isValid) return res.status(401).send('Unauthorized'); } catch (error) { return res.status(401).send('Unauthorized'); }. Vonage uses your application's public key (generated during application creation) to sign webhooks. The SDK verifies using the corresponding private key stored locally. Critical for production: Prevents spoofed webhook requests, man-in-the-middle attacks, and replay attacks. Messages API v1.0 supports both JWT and Basic authentication. JWT is recommended for production deployments. Without verification, malicious actors could send fake webhook requests to your endpoints, potentially triggering unauthorized SMS sends or data corruption.
What are the common Vonage API error codes and how do I handle them?
Common Vonage Messages API error codes and handling: 1000 (Throttled – exceeded submission capacity) → implement exponential backoff retry (1s, 2s, 4s, 8s intervals), 1120 (Illegal Sender Address – SenderID rejected) → use purchased Vonage number, check regional restrictions for alphanumeric sender IDs, 1170 (Invalid MSISDN – phone number invalid) → validate E.164 format (7–15 digits, no + prefix), 1210 (Anti-Spam Rejection – content/SenderID blocked) → review message content, avoid spam triggers (ALL CAPS, excessive punctuation, suspicious URLs), 1240 (Illegal Number – recipient opted out via STOP) → maintain suppression database, remove from contact list, 1460 (Daily limit exceeded – 10DLC compliance) → register for 10DLC, monitor daily quotas, 1470 (Fraud Defender Traffic Rule) → review traffic rules in dashboard, contact support. Error handling strategy: (1) Use try-catch blocks around vonage.messages.send(), (2) Log errors with context (recipient, error code, timestamp), (3) Categorize as retryable (1000, 5xx) vs permanent (1240, 1170), (4) Implement retry logic with exponential backoff for transient errors only, (5) Alert on permanent failures. Access errors via err.response.data or err.response.status from SDK.
How do I handle SMS message delivery status updates from Vonage?
Handle Vonage SMS delivery status updates by: (1) Configure Status URL webhook in your Vonage Application (Dashboard → Applications → Your App → Edit → Status URL: https://your-domain.com/webhooks/status), (2) Create Express POST endpoint at /webhooks/status, (3) Parse incoming JSON status updates from req.body, (4) Extract key fields: message_uuid (UUID for tracking), status (submitted/delivered/rejected/undeliverable), timestamp (ISO 8601 format), to (recipient number), error object (if failed: error.type URL, error.title code, error.detail description), channel, sms.count_total (SMS segments sent), (5) Always respond 200 OK immediately to acknowledge receipt, (6) Process status asynchronously: update database records, trigger alerts for failures, track delivery rates. Status flow: submitted → delivered (success) OR submitted → rejected/undeliverable (failure). Store message_uuid from send response to correlate with status updates. Example: const { message_uuid, status, error } = req.body; if (error) { console.error(Message ${message_uuid} failed: ${error.detail}); } await db.updateMessageStatus(message_uuid, status);. Monitor delivery rates (target: 95%+ for transactional SMS). Status updates arrive seconds to minutes after sending, depending on carrier.
How do I configure ngrok for local Vonage webhook development?
Configure ngrok for local Vonage webhook development by: (1) Download and install ngrok from ngrok.com/download, (2) Create free ngrok account and authenticate: ngrok config add-authtoken YOUR_TOKEN, (3) Start ngrok tunnel to your local port: ngrok http 3000 (match your Express APP_PORT), (4) Copy the HTTPS Forwarding URL from ngrok output (format: https://abc123.ngrok-free.app), (5) Update Vonage Application webhook URLs: Inbound URL: https://abc123.ngrok-free.app/webhooks/inbound, Status URL: https://abc123.ngrok-free.app/webhooks/status, (6) Keep ngrok running in separate terminal window while developing, (7) View webhook requests in ngrok web interface at http://127.0.0.1:4040. Important limitations: Free ngrok sessions expire after 2 hours, URLs change on restart (requires updating Vonage webhook URLs each time), maximum 40 requests/minute. For stable development URLs, use ngrok paid plan ($8/month) with reserved domains. Production deployment: Replace ngrok with permanent domain (AWS, Heroku, Vercel) with HTTPS enabled. Ngrok alternatives: localtunnel, serveo, CloudFlare Tunnel (free tier available).
How do I handle SMS character encoding and message concatenation?
Handle SMS character encoding in Vonage Messages API by: (1) Automatic detection: API automatically detects character types and selects GSM-7 (standard) or UCS-2 (unicode) encoding unless explicitly set via sms.encoding_type parameter (options: "text", "unicode", "auto"), (2) GSM-7 encoding supports 160 characters per single SMS, 153 characters per segment for concatenated messages (7 characters reserved for UDH – User Data Header). Includes: A-Z, 0-9, basic punctuation, Spanish/French/German diacritics (á, é, ñ, ü), (3) UCS-2 encoding (unicode/emojis) supports 70 characters per single SMS, 67 characters per segment. Triggered by: emojis, Chinese/Japanese/Arabic characters, special symbols, (4) Text limit: 1000 characters maximum in text parameter, (5) Concatenation: Automatic – Vonage splits long messages transparently. Inbound webhook field sms.num_messages shows segment count. Status webhook field sms.count_total shows segments sent. Cost impact: Each segment billed separately (161-character message costs 2× single SMS). Best practice: Keep messages ≤160 characters (GSM-7) or ≤70 characters (unicode) to avoid concatenation charges. Validate character count client-side before sending.
What security best practices should I follow for production Vonage SMS applications?
Production security best practices for Vonage SMS applications: (1) JWT Signature Verification – implement vonage.credentials.verifySignature() on all webhook endpoints to prevent spoofed requests (critical, see Section 3), (2) Environment Variables – store credentials in .env file, never hardcode API keys/secrets, add .env and private.key to .gitignore, use secrets manager in production (AWS Secrets Manager, HashiCorp Vault), (3) Input Validation – use Joi or express-validator to validate webhook payloads, sanitize user inputs, validate E.164 format before API calls, (4) Rate Limiting – implement express-rate-limit middleware on webhook endpoints (recommended: 100 requests/minute per IP), prevent DoS attacks, (5) HTTPS Only – enforce HTTPS for webhook URLs (Vonage requirement), use TLS 1.2+ certificates, (6) Dependency Security – run npm audit regularly, update dependencies, use npm audit fix, (7) Error Handling – never expose sensitive data in error messages, log errors securely with context (timestamp, user ID, request ID), (8) Webhook Response – always respond 200 OK to valid requests (even if internal processing fails) to prevent retry floods, (9) Security Headers – use helmet middleware for Express (XSS protection, CSP, HSTS), (10) Monitoring – integrate error tracking (Sentry, Bugsnag), monitor failed authentications, track unusual traffic patterns.
How do I deploy a Vonage SMS application to production?
Deploy Vonage SMS application to production by: (1) Choose hosting provider: AWS (EC2, Lambda), Heroku, Google Cloud Platform, DigitalOcean, or Vercel (Node.js support required), (2) Update webhook URLs: Replace ngrok URLs in Vonage Application settings with permanent domain HTTPS URLs (Dashboard → Applications → Your App → Edit → Inbound/Status URLs), (3) Environment configuration: Set production environment variables (VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_NUMBER, APP_PORT), upload private.key file securely (use secrets manager, not version control), (4) Node.js version: Use Active LTS (Node.js 18.x, 20.x, or 22.x), avoid Current versions for production stability, (5) Process management: Use PM2 (pm2 start server.js -i max for cluster mode) or Docker containers for reliability, enable auto-restart on crash, (6) SSL/TLS certificate: Configure HTTPS (Let's Encrypt free certificates, or CloudFlare SSL), Vonage requires HTTPS webhook URLs, (7) Implement security: Enable JWT signature verification (Section 3), rate limiting, input validation, security headers (helmet middleware), (8) Monitoring: Set up health checks (GET /health endpoint), error tracking (Sentry), logging (Pino/Winston to centralized platform), performance metrics (Prometheus), (9) Database: Integrate PostgreSQL/MongoDB for message persistence, use connection pooling, implement database migrations, (10) Testing: Load test webhook endpoints, verify error handling, test failover scenarios.
Source: Vonage Messages API v1.0 documentation, Node.js production deployment best practices (verified October 2025)