This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage SMS API and receive real-time delivery status updates (Delivery Receipts or DLRs) through webhooks.
By the end of this tutorial, you will have a functional application capable of:
- Sending SMS messages programmatically.
- Receiving and processing delivery status updates from Vonage.
- Handling basic configuration, security, and error logging.
This solves the common need for applications to not only send notifications via SMS but also to confirm whether those messages were successfully delivered to the end user's handset, which is crucial for reliable communication workflows.
Technologies Used
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express.js: A minimal and flexible Node.js web application framework used to create API endpoints and handle webhooks.
- Vonage SMS API: Enables sending and receiving SMS messages globally. We'll use the
@vonage/server-sdk
Node.js library. - dotenv: A module to load environment variables from a
.env
file intoprocess.env
. - ngrok: A tool to expose local development servers to the internet, necessary for testing webhooks.
- nodemon (Optional): A utility that monitors for changes in your source and automatically restarts your server, useful during development.
- express-validator: Middleware for request validation.
System Architecture
The basic flow of the application is as follows:
- Sending: An API client (e.g., Postman, another service) makes a POST request to our application's
/send-sms
endpoint. - Our Express application receives the request, validates it, and uses the Vonage SDK to send the SMS message via the Vonage API.
- Vonage delivers the SMS to the recipient's carrier network.
- Receiving Delivery Receipt: Once the final status of the message is known (e.g., delivered, failed, expired), Vonage sends a POST request (webhook) containing the delivery status information to a predefined endpoint in our application (
/delivery-receipt
). - Our Express application receives the webhook, parses the status, and logs it (in a real-world scenario, this would likely update a database record).
+-------------+ +------------------------+ +----------------+ +-----------------+
| API Client |------>| Your Node.js/Express App |------>| Vonage API |------>| Recipient Phone |
| (Postman) | (1) | (localhost:3000) | (2) | (sms.nexmo.com)| (3) | |
+-------------+ +------------------------+ +----------------+ +-----------------+
^ | |
| (5) Webhook POST /delivery-receipt | (4) Delivery Status |
+--------------------------------------+----------------------+
(via ngrok tunnel)
Prerequisites
- Node.js and npm (or yarn): Installed on your system. (Download Node.js)
- Vonage API Account: Sign up for free at Vonage API Dashboard.
- Vonage API Key and Secret: Found on the main page of your Vonage Dashboard after signing in.
- Vonage Virtual Number: You need a Vonage phone number capable of sending SMS. You can purchase one from the dashboard: Numbers > Buy Numbers.
- ngrok: Installed and authenticated. (Download ngrok)
- Basic understanding of JavaScript, Node.js, Express, and REST APIs.
1. Setting Up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir vonage-sms-dlr-guide cd vonage-sms-dlr-guide
-
Initialize Node.js Project: Initialize the project using npm, accepting the defaults.
npm init -y
This creates a
package.json
file. -
Enable ES Modules: Since we'll be using ES Module
import
syntax (e.g.,import express from 'express'
), we need to tell Node.js to treat.js
files as modules. Open yourpackage.json
file and add the line"type": "module"
at the top level:// package.json { "name": "vonage-sms-dlr-guide", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", // Add this line "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" // Dependencies will be added below }
-
Install Dependencies: Install Express for the web server, the Vonage Server SDK for interacting with the API, dotenv for managing environment variables, express-validator for input validation, and optionally nodemon for development convenience.
npm install express @vonage/server-sdk dotenv express-validator npm install --save-dev nodemon # Optional: for development
-
Configure
nodemon
(Optional): If you installednodemon
, add a development script to yourpackage.json
. Openpackage.json
and add/modify thescripts
section:// package.json snippet (inside the main JSON object) "scripts": { "start": "node index.js", "dev": "nodemon index.js", // Add this line "test": "echo \"Error: no test specified\" && exit 1" },
This allows you to run
npm run dev
to start your server with auto-reloading on file changes. -
Create Core Files: Create the main application file and files for environment variables and ignored files.
touch index.js .env .gitignore
-
Configure
.gitignore
: Prevent sensitive information and unnecessary files from being committed to version control. Add the following to your.gitignore
file:# .gitignore node_modules .env npm-debug.log *.log
-
Configure
.env
: Add placeholders for your Vonage credentials and number. We will also add aBASE_URL
which will be our ngrok URL later. Do not commit this file to Git.# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER PORT=3000 BASE_URL=http://localhost:3000 # Replace with ngrok URL during testing
VONAGE_API_KEY
: Your API key from the Vonage Dashboard.VONAGE_API_SECRET
: Your API secret from the Vonage Dashboard.VONAGE_NUMBER
: The Vonage virtual number you purchased (in E.164 format, e.g.,+14155550100
).PORT
: The port your Express server will listen on.BASE_URL
: The public-facing base URL of your application. Crucial for webhook configuration.
2. Implementing Core Functionality: Sending SMS
Now, let's write the code to initialize Express and the Vonage SDK, and create an endpoint to send an SMS message.
// index.js
import express from 'express';
import dotenv from 'dotenv';
import { Vonage } from '@vonage/server-sdk';
// Load environment variables
dotenv.config();
// Initialize Express app
const app = express();
app.use(express.json()); // Middleware to parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Middleware to parse URL-encoded bodies
// --- Vonage Setup ---
// Validate essential environment variables
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET || !process.env.VONAGE_NUMBER) {
console.error('Error: Missing Vonage API credentials or number in .env file.');
process.exit(1); // Exit if critical configuration is missing
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
// Optional: Set a custom user agent for your requests
// userAgent: ""my-vonage-app/1.0.0""
});
// --- API Endpoints ---
// Endpoint to send an SMS
app.post('/send-sms', async (req, res) => {
const { to, text } = req.body;
// Basic validation (More robust validation will be added later)
if (!to || !text) {
return res.status(400).json({ error: 'Missing ""to"" or ""text"" field in request body.' });
}
// Very basic check - E.164 format (like +14155550100) is recommended and better validated later
if (!/^\+?\d{10,15}$/.test(to)) {
return res.status(400).json({ error: '""to"" field must be a valid phone number, preferably in E.164 format (e.g., +14155550100).' });
}
console.log(`Attempting to send SMS to: ${to}`);
try {
const resp = await vonage.sms.send({
to: to,
from: process.env.VONAGE_NUMBER,
text: text,
// Optional: Specify 'unicode' for messages with non-GSM characters (like emojis)
// type: 'unicode'
});
console.log('Message submission response:', resp);
// Note: `resp` contains information about the *submission* to Vonage,
// not the final delivery status. The `message-id` is key here.
if (resp.messages[0].status === '0') {
res.status(200).json({
message: 'SMS submitted successfully!',
messageId: resp.messages[0]['message-id']
});
} else {
// Handle submission errors reported directly by the SDK response
console.error(`Message submission failed with error: ${resp.messages[0]['error-text']}`);
res.status(400).json({ // Use 400 for submission errors like invalid number format reported here
error: `Message submission failed: ${resp.messages[0]['error-text']}`,
errorCode: resp.messages[0].status
});
}
} catch (error) {
console.error('Error sending SMS via Vonage API:', error);
// Provide more context if available from the Vonage error object
let errorMessage = 'Failed to send SMS due to a server or network error.';
if (error.response && error.response.data) {
console.error('Vonage API Error Details:', error.response.data);
errorMessage = error.response.data.title || error.response.data.detail || errorMessage;
// Handle specific known error codes if needed
// if (error.response.data.status === '401') errorMessage = 'Authentication failed. Check API key/secret.';
}
res.status(500).json({ error: errorMessage, details: error.message });
}
});
// --- Start Server ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
// Remind user about ngrok if BASE_URL is still localhost
if (process.env.BASE_URL && process.env.BASE_URL.includes('localhost')) {
console.warn('Reminder: Your BASE_URL in .env is set to localhost. You will need to update it with your ngrok URL and configure Vonage webhooks for delivery receipts to work.');
}
});
// --- Graceful Shutdown (Optional but recommended) ---
process.on('SIGINT', () => {
console.log('Received SIGINT. Shutting down gracefully...');
// Add any cleanup logic here (e.g., close database connections)
process.exit(0);
});
Explanation:
- Imports: We import
express
,dotenv
, andVonage
from@vonage/server-sdk
. - Environment Variables:
dotenv.config()
loads variables from.env
. We add a check to ensure critical Vonage variables are present. - Express Setup: We initialize Express and use middleware (
express.json
,express.urlencoded
) to easily parse incoming request bodies. - Vonage Initialization: We create a
Vonage
client instance using the API key and secret from our environment variables. /send-sms
Endpoint:- Defines a
POST
route at/send-sms
. - Extracts the recipient number (
to
) and message content (text
) from the request body. - Performs basic validation (improved later) to ensure
to
andtext
are provided andto
looks somewhat like a phone number (clarifying E.164 format is preferred). - Uses
vonage.sms.send()
within anasync
function to send the message. This method requiresto
,from
(your Vonage number), andtext
. - Important: The response from
vonage.sms.send()
confirms submission to Vonage, not final delivery. It includes astatus
code ('0'
means success) and themessage-id
. We check this status for immediate feedback. - Includes
try...catch
for handling network or API-level errors during the call. It logs errors and returns appropriate status codes (400 for submission errors, 500 for server errors).
- Defines a
- Server Start: The server starts listening on the port defined in
.env
(or default 3000). A warning reminds the user about ngrok if theBASE_URL
hasn't been updated. - Graceful Shutdown: Handles
SIGINT
(Ctrl+C) for a cleaner shutdown process.
3. Handling Delivery Status Callbacks (Webhooks)
Vonage uses webhooks to send Delivery Receipts (DLRs) back to your application. We need an endpoint to receive these POST requests.
-
Set up
ngrok
: Open a new terminal window and runngrok
to expose your local server running on port 3000.ngrok http 3000
ngrok
will display forwarding URLs (e.g.,https://<random-string>.ngrok-free.app
). Copy thehttps
URL. This is your public URL. -
Update
.env
: Replace theBASE_URL
in your.env
file with thehttps
URL provided by ngrok.# .env # ... other variables ... BASE_URL=https://<random-string>.ngrok-free.app
Restart your Node.js application (
npm run dev
ornpm start
) for the changes in.env
to take effect. -
Configure Vonage Webhooks:
- Go to your Vonage API Dashboard.
- Navigate to Settings in the left-hand menu.
- Find the API settings section (you might need to scroll down).
- In the SMS Settings card, ensure "SMS API" is selected.
- In the Delivery receipts (DLR) field under "Webhook URL for DLR", enter your full ngrok webhook URL:
YOUR_NGROK_HTTPS_URL/delivery-receipt
(e.g.,https://<random-string>.ngrok-free.app/delivery-receipt
). - Set the HTTP Method to
POST
. - Click Save changes.
- Note: This guide focuses on the SMS API's specific DLR format. Vonage also offers a newer Messages API which uses a different endpoint (
/webhooks/dlr
) and provides a unified webhook format for multiple channels (SMS, WhatsApp, etc.). For new implementations, you might consider using the Messages API instead, although its setup is slightly different.
-
Implement the Webhook Endpoint: Add the following route handler to your
index.js
file, typically before theapp.listen
call.// index.js // ... (after the /send-sms endpoint) ... // Endpoint to receive Delivery Receipts (DLRs) from Vonage app.post('/delivery-receipt', (req, res) => { // Vonage might send DLRs as application/json or application/x-www-form-urlencoded // Express middleware handles parsing both based on Content-Type const params = req.body; // Log the raw DLR for debugging console.log('--- Delivery Receipt Received ---'); console.log(JSON.stringify(params, null, 2)); // Pretty print the JSON or parsed form data // Basic validation: Check for essential DLR fields (names vary slightly based on format) // Common fields: messageId (or message-id), msisdn (recipient), status, err-code, price, scts (timestamp) const messageId = params['message-id'] || params.messageId; const status = params.status; const recipient = params.msisdn; if (!messageId || !status || !recipient) { console.warn('Received incomplete DLR:', params); // Respond with 200 OK even if incomplete, Vonage expects this. return res.status(200).send('OK'); } const errorCode = params['err-code'] || params.errCode; // Error code if status is 'failed' or 'rejected' const price = params.price; // Cost of the message segment const timestamp = params.scts; // Timestamp of the status update const networkCode = params['network-code'] || params.networkCode; // Network code console.log(`DLR Info: Recipient=${recipient}, Message ID=${messageId}, Status=${status}, ErrorCode=${errorCode || 'N/A'}, Timestamp=${timestamp}`); // --- TODO: Implement your business logic here --- // Examples: // - Find the message in your database using the messageId. // - Update its status to the received 'status'. // - Store the errorCode, timestamp, price, etc. // - Trigger follow-up actions based on the status (e.g., retry failed messages, notify internally). // - Log metrics for deliverability analysis. // Example: Log specific statuses if (status === 'delivered') { console.log(`Message ${messageId} successfully delivered to ${recipient}.`); } else if (status === 'failed' || status === 'rejected') { console.error(`Message ${messageId} to ${recipient} failed with status: ${status}, error code: ${errorCode}. Check Vonage docs for details.`); // You might want to query Vonage documentation or map codes to reasons here. } else { console.log(`Message ${messageId} to ${recipient} has status: ${status}.`); } // IMPORTANT: Always respond to Vonage webhooks with a 200 OK status. // Failure to do so may result in Vonage retrying the webhook, leading to duplicate processing. res.status(200).send('OK'); }); // ... (app.listen call) ...
Explanation:
- Route Definition: We define a
POST
route at/delivery-receipt
, matching the URL configured in the Vonage dashboard. - Parsing: We access the incoming webhook data from
req.body
. Express'sjson()
andurlencoded()
middleware handle parsing based on theContent-Type
header sent by Vonage. - Logging: The raw DLR payload is logged for inspection. Key fields like
message-id
,status
,err-code
,msisdn
(recipient), andscts
(timestamp) are extracted and logged. Note that field names might vary slightly (e.g.,message-id
vsmessageId
), so we check for both common variants. - Business Logic Placeholder: A
TODO
comment highlights where you would integrate this data into your application's logic (e.g., database updates). - Status Handling: Basic examples show how to check for specific statuses like
delivered
orfailed
. - Crucial Response: We always send a
200 OK
response back to Vonage usingres.status(200).send('OK')
. This acknowledges receipt of the webhook. If Vonage doesn't receive a 200 OK, it will assume the webhook failed and may retry sending it.
4. Building a Complete API Layer
Let's refine the /send-sms
endpoint with robust validation using express-validator
.
-
Update
/send-sms
with Validation: Modify the/send-sms
endpoint definition inindex.js
.// index.js import express from 'express'; import dotenv from 'dotenv'; import { Vonage } from '@vonage/server-sdk'; import { body, validationResult } from 'express-validator'; // Import validator // ... (dotenv.config, express app setup, Vonage init) ... // Validation rules for /send-sms const sendSmsValidationRules = [ body('to') .notEmpty().withMessage('Recipient phone number (""to"") is required.') // isMobilePhone is generally good, but E.164 is the most reliable format. // Provide flexibility but guide towards E.164. .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format for ""to"" field. Recommended format is E.164 (e.g., +14155550100).'), body('text') .notEmpty().withMessage('Message text (""text"") is required.') .isLength({ min: 1, max: 1600 }).withMessage('Message text must be between 1 and 1600 characters.'), // Example length constraints ]; // Middleware to handle validation results const validateRequest = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { // Log validation errors for debugging console.warn('Validation Errors:', errors.array()); return res.status(400).json({ errors: errors.array() }); } next(); }; // Endpoint to send an SMS (Updated with validation) app.post('/send-sms', sendSmsValidationRules, validateRequest, async (req, res) => { // Validation passed if we reach here const { to, text } = req.body; console.log(`Attempting to send SMS to: ${to}`); try { const resp = await vonage.sms.send({ to: to, from: process.env.VONAGE_NUMBER, text: text, }); console.log('Message submission response:', resp); if (resp.messages[0].status === '0') { res.status(200).json({ message: 'SMS submitted successfully!', messageId: resp.messages[0]['message-id'] }); } else { console.error(`Message submission failed with error: ${resp.messages[0]['error-text']}`); res.status(400).json({ // Use 400 for submission errors error: `Message submission failed: ${resp.messages[0]['error-text']}`, errorCode: resp.messages[0].status }); } } catch (error) { console.error('Error sending SMS via Vonage API:', error); let errorMessage = 'Failed to send SMS due to a server or network error.'; if (error.response && error.response.data) { console.error('Vonage API Error Details:', error.response.data); errorMessage = error.response.data.title || error.response.data.detail || errorMessage; } res.status(500).json({ error: errorMessage, details: error.message }); } }); // ... (delivery-receipt endpoint, app.listen) ...
Changes:
- Imported
body
andvalidationResult
fromexpress-validator
. - Defined
sendSmsValidationRules
array specifying rules forto
andtext
. We useisMobilePhone
for general validation but message guides towards E.164. Added length constraints fortext
. - Created a
validateRequest
middleware function to checkvalidationResult
and return errors if any. - Added the rules array and the validation middleware to the
app.post('/send-sms', ...)
definition before the main async handler. - Removed the initial, basic validation from the main handler as
express-validator
now handles it more robustly. - Error handling logic remains similar, checking the direct response status from
vonage.sms.send
and catching broader API/network errors.
- Imported
-
API Testing Examples (
curl
):-
Send SMS (Success): Replace placeholders with your actual recipient number (use E.164 format like
+14155550100
).curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+14155550100"", ""text"": ""Hello from Vonage and Node.js! This is a test with validation."" }'
Expected Response (200 OK):
{ ""message"": ""SMS submitted successfully!"", ""messageId"": ""some-message-id-from-vonage"" }
-
Send SMS (Validation Error - Missing 'text'):
curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+14155550100"" }'
Expected Response (400 Bad Request):
{ ""errors"": [ { ""type"": ""field"", ""msg"": ""Message text (\""text\"") is required."", ""path"": ""text"", ""location"": ""body"" }, { ""type"": ""field"", ""value"": """", ""msg"": ""Message text must be between 1 and 1600 characters."", ""path"": ""text"", ""location"": ""body"" } ] }
-
Send SMS (Validation Error - Invalid 'to'):
curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""invalid-number"", ""text"": ""Testing invalid number"" }'
Expected Response (400 Bad Request):
{ ""errors"": [ { ""type"": ""field"", ""value"": ""invalid-number"", ""msg"": ""Invalid phone number format for \""to\"" field. Recommended format is E.164 (e.g., +14155550100)."", ""path"": ""to"", ""location"": ""body"" } ] }
-
Delivery Receipt Webhook (Simulated): You can't easily simulate the exact Vonage DLR POST with
curl
without knowing a validmessage-id
beforehand, but you can test if your endpoint receives a POST correctly. Use the ngrok inspector (http://127.0.0.1:4040
in your browser) to see the real DLRs coming from Vonage after sending a message.
-
5. Integrating with Vonage Services (Recap & Details)
Let's consolidate the key integration points with Vonage.
-
API Credentials:
- How to Obtain: Log in to the Vonage API Dashboard. Your API Key and API Secret are displayed prominently on the main page ("API key" and "Secret").
- Secure Storage: Store these values in your
.env
file (VONAGE_API_KEY
,VONAGE_API_SECRET
) and ensure.env
is listed in your.gitignore
. Never hardcode credentials directly in your source code. - Purpose: Used by the
@vonage/server-sdk
to authenticate your API requests.
-
Virtual Number:
- How to Obtain: In the dashboard, navigate to Numbers > Buy Numbers. Search for numbers by country and features (ensure SMS is selected). Purchase a number. Alternatively, use the Vonage CLI (
vonage numbers:search GB --features=SMS
,vonage numbers:buy <number> GB
). - Secure Storage: Store the purchased number (in E.164 format, e.g.,
+14155550100
) in your.env
file (VONAGE_NUMBER
). - Purpose: Acts as the 'sender ID' for your outgoing SMS messages in many regions (required in the US/Canada).
- How to Obtain: In the dashboard, navigate to Numbers > Buy Numbers. Search for numbers by country and features (ensure SMS is selected). Purchase a number. Alternatively, use the Vonage CLI (
-
Webhook Configuration (Delivery Receipts):
- How to Configure: Dashboard > Settings > API settings > SMS Settings card.
- URL: Set the "Delivery receipts (DLR)" URL to
YOUR_NGROK_HTTPS_URL/delivery-receipt
. Must be publicly accessible (hencengrok
). - Method: Set to
POST
. - Purpose: Tells Vonage where to send the delivery status updates for messages sent using your API key via the SMS API.
- Environment Variable (
BASE_URL
): Storing the ngrok URL (or your production URL) in.env
(BASE_URL
) helps manage this, although it's primarily used here for the reminder log message. The critical part is pasting the correct URL into the Vonage dashboard.
6. Error Handling, Logging, and Retry Mechanisms
-
Error Handling Strategy:
- API Calls (
/send-sms
): Usetry...catch
blocks aroundvonage.sms.send()
. Check the direct response status (resp.messages[0].status
). Log detailed errors (console.error
). Checkerror.response.data
for specific Vonage API error details if available during exceptions. Return appropriate HTTP status codes (400 for validation/submission errors, 500 for server/API errors) with clear JSON error messages. - Webhooks (
/delivery-receipt
): Log incoming DLRs. Usetry...catch
around your internal processing logic (e.g., database updates). Crucially, always return200 OK
to Vonage, even if your internal processing fails, to prevent retries. Log internal processing errors separately.
- API Calls (
-
Logging:
- Use
console.log
for informational messages (sending attempts, DLR received, status updates). - Use
console.warn
for non-critical issues (e.g., validation errors, incomplete DLRs). - Use
console.error
for actual errors (API call failures, submission failures, internal processing failures). - Production: Replace
console.*
with a structured logger like Pino or Winston. This allows for log levels, JSON formatting, and easier integration with log management systems.# Example using Pino (Install: npm install pino) # import pino from 'pino'; # const logger = pino(); # logger.info({ messageId: '...', status: 'submitted' }, 'SMS submitted'); # logger.error({ err: error, messageId: '...' }, 'Failed to send SMS');
- Use
-
Retry Mechanisms (Vonage):
- Sending: If
vonage.sms.send()
fails due to a temporary network issue or a 5xx error from Vonage (caught in thecatch
block), you might implement application-level retries (e.g., using a library likeasync-retry
) with exponential backoff. However, be cautious not to retry on permanent errors (like invalid number format - 4xx errors or submission errors indicated in the response). - Receiving DLRs: Vonage handles retries automatically if your webhook endpoint doesn't respond with
200 OK
within a reasonable timeout (usually several seconds). Your primary responsibility is to ensure your/delivery-receipt
endpoint is reliable and responds quickly with200 OK
. Offload heavy processing (like database writes) to happen asynchronously after sending the 200 OK, perhaps using a message queue.
- Sending: If
-
Common Vonage DLR Statuses/Errors:
delivered
: Successfully delivered to the handset.accepted
: Accepted by the carrier, awaiting final status.failed
: Delivery failed (e.g., number invalid, blocked). Checkerr-code
.expired
: Message validity period expired before delivery.rejected
: Rejected by Vonage or carrier (e.g., spam filter, permission issue). Checkerr-code
.buffered
: Temporarily stored, awaiting delivery attempt.- Refer to the Vonage SMS API Error Codes Documentation for detailed
err-code
meanings.
7. Database Schema and Data Layer (Conceptual)
While this guide doesn't implement a database, in a production system, you'd need one to track message status.
-
Why: To persistently store the
messageId
received when sending and associate it with the subsequent delivery status received via webhook. This allows you to query the status of any message sent. -
Example Schema (Conceptual - e.g., PostgreSQL):
CREATE TABLE sms_messages ( id SERIAL PRIMARY KEY, vonage_message_id VARCHAR(50) UNIQUE NOT NULL, -- From send response & DLR recipient_number VARCHAR(20) NOT NULL, sender_number VARCHAR(20) NOT NULL, -- Your Vonage number used message_text TEXT, status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, expired, rejected, accepted vonage_status_code VARCHAR(10), -- DLR err-code price DECIMAL(10, 5), -- DLR price network_code VARCHAR(10), -- DLR network-code submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Updated on DLR receipt dlr_timestamp TIMESTAMP WITH TIME ZONE -- DLR scts field ); -- Optional: Index for querying by message ID or status CREATE INDEX idx_sms_vonage_message_id ON sms_messages (vonage_message_id); CREATE INDEX idx_sms_status ON sms_messages (status);
-
Data Layer Logic:
- On Send (
/send-sms
success): Insert a new record intosms_messages
with thevonage_message_id
, recipient, sender, text, and setstatus
to'submitted'
. - On Receive DLR (
/delivery-receipt
):- Find the record in
sms_messages
using thevonage_message_id
from the webhook payload. - If found, update the
status
,vonage_status_code
,price
,network_code
,dlr_timestamp
, andlast_updated_at
fields based on the DLR data. - Handle cases where the
messageId
might not be found (log an error). - Implement logic based on the updated
status
(e.g., trigger notifications for failures).
- Find the record in
- On Send (
Conclusion
You have successfully built a Node.js application using Express that can send SMS messages via the Vonage API and receive delivery status updates through webhooks. You've learned how to:
- Set up a Node.js project with necessary dependencies.
- Initialize the Vonage Server SDK.
- Create an API endpoint (
/send-sms
) to send messages, including input validation. - Configure
ngrok
to expose your local server for webhook testing. - Set up webhook URLs in the Vonage Dashboard.
- Implement a webhook endpoint (
/delivery-receipt
) to receive and process DLRs. - Understand basic error handling, logging, and the importance of responding correctly to webhooks.
- Conceptualize how to integrate message tracking with a database.
This foundation allows you to build more complex communication workflows requiring reliable SMS delivery confirmation. Remember to replace placeholder credentials and URLs with your actual values and consider using a structured logger and robust database integration for production environments.