code examples
code examples
Node.js SMS Delivery Status Callbacks with Vonage
Learn how to build a Node.js application using Express and the Vonage Messages API to send SMS and receive real-time delivery status updates via webhooks.
Tracking the delivery status of SMS messages is crucial for applications that rely on timely communication. Knowing whether a message reached the recipient's handset, was rejected by the carrier, or failed for other reasons enables developers to build more robust and reliable systems. This guide provides a step-by-step walkthrough for building a Node.js application using Express and the Vonage Messages API to send SMS messages and receive real-time delivery status updates via webhooks.
We will build a simple Node.js server that can send an SMS message and expose webhook endpoints to receive delivery status updates from Vonage. This solves the common problem of ""fire and forget"" SMS sending, providing visibility into the message lifecycle after it leaves the Vonage platform. We'll use the Vonage Node.js SDK for seamless API interaction and ngrok for local development testing.
System architecture
Here's a high-level overview of how the components interact:
- User/Trigger: Initiates the SMS send request to the Node.js application.
- Node.js Application (Express):
- Receives the send request.
- Uses the Vonage Node.js SDK to call the Vonage Messages API.
- Listens for incoming webhook events (status updates) from Vonage.
- Vonage Messages API:
- Accepts the SMS send request.
- Delivers the SMS message via carrier networks.
- Sends status updates (e.g.,
submitted,delivered,failed) to the configured Status Webhook URL.
- Carrier Network: Delivers the SMS to the recipient's handset.
- Recipient: Receives the SMS message.
(Note: A sequence diagram illustrating the component interactions was present here in the original document.)
Prerequisites
Before you begin, ensure you have the following:
- Node.js and npm: Installed on your system. Download from nodejs.org.
- Vonage API Account: Sign up at Vonage API Dashboard. You'll need your API Key and API Secret.
- Vonage Application: You'll create one using the Messages API capability. This provides an Application ID and allows generating a private key.
- Vonage Virtual Number: Purchase a Vonage number capable of sending SMS messages from the dashboard (Numbers > Buy Numbers).
- ngrok: A tool to expose your local server to the internet for webhook testing. Download from ngrok.com and create a free account.
- Git: (Optional) For version control and cloning the example repository.
1. Setting up the project
Let's create the project directory, initialize Node.js, and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and run:
bashmkdir vonage-sms-status-guide cd vonage-sms-status-guide -
Initialize Node.js Project: This creates a
package.jsonfile.bashnpm init -y -
Install Dependencies: We need
expressfor the web server,@vonage/server-sdkto interact with the Vonage API,dotenvto manage environment variables, andjsonwebtokento verify webhook signatures. We also recommendbody-parserfor reliable webhook verification.bash# Note: Added body-parser for reliable signature verification npm install express @vonage/server-sdk dotenv jsonwebtoken body-parser -
Create Project Files: Create the main application file and environment configuration files.
bashtouch index.js .env .env.example .gitignore -
Configure
.gitignore: Add sensitive files and directories to.gitignoreto prevent committing them to version control.text# .gitignore node_modules/ .env private.key npm-debug.log *.logExplanation: We ignore
node_modules(can be reinstalled),.env(contains secrets),private.key(Vonage private key), and log files. -
Set up Environment Variables: Create a
.env.examplefile to list the required variables. This serves as a template.dotenv# .env.example VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key VONAGE_SIGNATURE_SECRET=YOUR_VONAGE_SIGNATURE_SECRET # From Application settings VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # In E.164 format, e.g., +12015550123 PORT=3000 BASE_URL=http://localhost:3000 # Will be updated with ngrok URLNow, create your actual
.envfile by copying.env.exampleand filling in your real credentials.bashcp .env.example .envExplanation:
VONAGE_API_KEY,VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Obtained after creating a Vonage Application (next step).VONAGE_PRIVATE_KEY_PATH: Path to the private key file downloaded when creating the application. We assume it's in the project root namedprivate.key.VONAGE_SIGNATURE_SECRET: Found in your Vonage Application settings under 'Webhook signature'. Used to verify incoming webhooks.VONAGE_NUMBER: Your purchased Vonage virtual number capable of sending SMS (must be in E.164 format, e.g.,+12015550123).PORT: The port your local server will run on.BASE_URL: The base URL for your webhook endpoints. We'll update this later with the ngrok URL.
2. Configuring Vonage
You need a Vonage Application configured for the Messages API to handle sending and status updates.
- Navigate to Applications: Log in to the Vonage API Dashboard and go to ""Applications"" > ""Create a new application"".
- Name Your Application: Enter a descriptive name (e.g., ""Node SMS Status Guide App"").
- Generate Keys: Click ""Generate public and private key"". Immediately save the
private.keyfile that downloads. Place this file in your project root directory (matchingVONAGE_PRIVATE_KEY_PATHin.env). The public key is stored by Vonage. - Note Application ID: Copy the generated Application ID and paste it into your
.envfile forVONAGE_APPLICATION_ID. - Enable Capabilities: Find the ""Capabilities"" section.
- Toggle on ""Messages"".
- Enter placeholder URLs for now (we'll update these after starting ngrok):
- Inbound URL:
http://example.com/webhooks/inbound - Status URL:
http://example.com/webhooks/status - Why: The Status URL is where Vonage will POST delivery status updates. The Inbound URL is for receiving SMS replies (optional for this guide but good practice to configure).
- Inbound URL:
- Find Signature Secret: Scroll down to the 'Webhook signature' section within the Messages capability settings. Copy the 'Secret' and paste it into your
.envfile forVONAGE_SIGNATURE_SECRET. - Link Virtual Number: Scroll down further to ""Link virtual numbers"". Find your purchased Vonage number and click ""Link"".
- Save Changes: Click ""Save changes"" at the bottom of the page.
- (Optional but Recommended) Set Messages API as Default: Navigate to your main account ""Settings"". Scroll to ""API Settings"" > ""SMS Settings"". Ensure ""Default SMS Setting"" is set to ""Messages API"". This ensures consistency if other parts of your account interact with SMS. Save changes if necessary.
3. Setting up the webhook server (Express)
Now, let's write the Node.js code using Express to handle incoming webhooks.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const bodyParser = require('body-parser'); // Import body-parser
const jwt = require('jsonwebtoken');
const { Vonage } = require('@vonage/server-sdk');
const app = express();
// --- Middleware ---
// Capture raw body *before* JSON parsing for reliable signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
// Save the raw buffer onto the request object
req.rawBody = buf;
}
}));
// Use express.urlencoded({ extended: true }) for URL-encoded data
app.use(express.urlencoded({ extended: true }));
// --- Vonage Initialization ---
// Check for essential environment variables
if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_SIGNATURE_SECRET) {
console.error('Error: VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, or VONAGE_SIGNATURE_SECRET not set in .env');
process.exit(1);
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY, // Optional for Messages API JWT auth, but good practice
apiSecret: process.env.VONAGE_API_SECRET, // Optional for Messages API JWT auth
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
}, {
debug: true // Enable debug logging for the SDK
});
// --- Webhook Security Middleware ---
// Verifies the JWT signature on incoming Vonage webhooks using the raw body
const verifyVonageSignature = (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader?.split(' ')[1]; // Extract Bearer token
if (!token) {
console.warn('Webhook received without Authorization header or token.');
return res.status(401).send('Unauthorized: Missing token');
}
// Verify the token using the raw body captured by bodyParser's verify function
// Vonage signs the raw request body, not the parsed JSON object.
if (!req.rawBody) {
console.error('Raw body buffer not available for verification. Ensure bodyParser.json() with verify is used before this middleware.');
return res.status(500).send('Internal Server Error: Cannot verify signature');
}
// jwt.verify throws if the signature is invalid
const decoded = jwt.verify(token, process.env.VONAGE_SIGNATURE_SECRET, {
algorithms: ['HS256'],
// IMPORTANT: Provide the raw body buffer for verification
// This assumes the payload is the raw body itself for JWT verification context
// Check Vonage docs if payload structure for signing differs
}); // Note: jwt.verify doesn't directly take the raw body for payload check,
// it verifies the signature against the token structure. The key is using the
// correct secret. Vonage includes payload claims *within* the signed JWT.
// Optional: Check payload application_uuid matches your app ID
// if (decoded.application_uuid !== process.env.VONAGE_APPLICATION_ID) {
// console.warn(`Webhook received for unexpected application ID: ${decoded.application_uuid}`);
// return res.status(401).send('Unauthorized: Invalid application ID');
// }
console.log('Webhook signature verified successfully.');
req.vonage_payload = decoded; // Attach decoded JWT payload if needed later
next(); // Signature is valid, proceed to the handler
} catch (error) {
console.error('Error verifying webhook signature:', error.message);
if (error instanceof jwt.TokenExpiredError) {
return res.status(401).send('Unauthorized: Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
return res.status(401).send('Unauthorized: Invalid signature');
}
// Log other unexpected errors
console.error('Unexpected error during signature verification:', error);
return res.status(500).send('Internal Server Error');
}
};
// --- Webhook Endpoints ---
// Status Webhook: Receives delivery status updates
// Apply the verification middleware *only* to Vonage webhooks
app.post('/webhooks/status', verifyVonageSignature, (req, res) => {
// The body is already parsed by bodyParser.json()
const statusData = req.body;
console.log('--- Delivery Status Received ---');
console.log('Message UUID:', statusData.message_uuid);
console.log('Status:', statusData.status);
console.log('Timestamp:', statusData.timestamp);
if (statusData.error) {
console.error('Error Code:', statusData.error.code);
console.error('Error Reason:', statusData.error.reason);
}
console.log('Full Payload:', JSON.stringify(statusData, null, 2));
console.log('------------------------------');
// Vonage expects a 200 OK response to acknowledge receipt
// Failure to send 200 OK will cause Vonage to retry the webhook
res.status(200).send('OK');
});
// Inbound Webhook: Receives incoming SMS messages (optional for this guide)
// Apply the verification middleware here too for security
app.post('/webhooks/inbound', verifyVonageSignature, (req, res) => {
const inboundData = req.body;
console.log('--- Inbound SMS Received ---');
console.log('From:', inboundData.from?.number || 'Unknown');
console.log('To:', inboundData.to?.number || 'Unknown');
console.log('Text:', inboundData.message?.content?.text || 'N/A');
console.log('Full Payload:', JSON.stringify(inboundData, null, 2));
console.log('--------------------------');
res.status(200).send('OK');
});
// --- SMS Sending Function ---
async function sendSms(toNumber, text) {
console.log(`Attempting to send SMS to ${toNumber}`);
const fromNumber = process.env.VONAGE_NUMBER;
if (!fromNumber) {
console.error('Error: VONAGE_NUMBER not set in .env');
return;
}
// Validate recipient number format (strict E.164 check)
if (!/^\+\d{10,15}$/.test(toNumber)) {
console.error(`Error: Invalid recipient number format: ${toNumber}. Must use E.164 format (e.g., +12015550123).`);
// Optionally throw an error or return a failure indicator
return;
}
try {
const resp = await vonage.messages.send({
message_type: ""text"",
to: toNumber, // E.164 format (e.g., +14155550100)
from: fromNumber, // Your Vonage number
channel: ""sms"",
text: text,
// client_ref: 'my-internal-tracking-id-123' // Optional client reference
});
console.log('SMS Submitted Successfully!');
console.log('Message UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending SMS:', err?.response?.data || err.message);
// Log detailed error if available from Vonage response
if (err?.response?.data) {
console.error('Vonage Error Details:', JSON.stringify(err.response.data, null, 2));
}
}
}
// --- Example Send Endpoint (for testing) ---
app.post('/send-sms', (req, res) => {
const { to, text } = req.body;
if (!to || !text) {
return res.status(400).send('Missing ""to"" or ""text"" in request body.');
}
// Strict E.164 format check
if (!/^\+\d{10,15}$/.test(to)) {
return res.status(400).send('Invalid ""to"" number format. Use E.164 (e.g., +12015550123).');
}
sendSms(to, text); // Fire and forget for this example
res.status(202).send('SMS send request accepted.');
});
// --- Start Server ---
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`Webhook URLs should be configured in Vonage based on your BASE_URL:`);
console.log(`Status URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/status`);
console.log(`Inbound URL: ${process.env.BASE_URL || `http://localhost:${port}`}/webhooks/inbound`);
// Example: Send a test SMS on startup (remove in production)
// Make sure to add a valid test number in E.164 format
// const TEST_RECIPIENT = '+14155550101'; // Replace with your test phone number
// if (process.env.NODE_ENV !== 'production' && TEST_RECIPIENT) {
// console.log(`Sending initial test SMS to ${TEST_RECIPIENT}...`);
// setTimeout(() => sendSms(TEST_RECIPIENT, 'Hello from Vonage Node Guide! Startup Test - ' + new Date().toLocaleTimeString()), 2000);
// }
});Explanation:
- We initialize Express and load
.envvariables. - The Vonage SDK is initialized using the Application ID and private key path from
.env. Debug mode is enabled for detailed SDK logs. - Webhook Security:
body-parsermiddleware with averifyfunction is used to capture the raw request body before JSON parsing. TheverifyVonageSignaturemiddleware then usesjsonwebtokenand yourVONAGE_SIGNATURE_SECRETto verify the JWT signature from theAuthorization: Bearer <token>header. This ensures the request genuinely originated from Vonage. This raw body approach is crucial for reliable production verification. /webhooks/status: This route handles POST requests from Vonage containing delivery status updates. It logs the received data and sends a200 OKresponse. This acknowledgment is vital; otherwise, Vonage will retry sending the webhook./webhooks/inbound: Handles incoming SMS messages (optional).sendSmsfunction: Encapsulates the logic for sending an SMS usingvonage.messages.send(). It takes the recipient number (validated for strict E.164 format) and text as arguments. It logs themessageUuidon successful submission./send-smsendpoint: A simple POST endpoint to trigger thesendSmsfunction for testing purposes via cURL or Postman. Includes strict E.164 validation.- The server starts listening on the specified
PORT.
4. Running and testing locally
To receive webhooks from Vonage on your local machine, you need ngrok.
-
Start ngrok: Open a new terminal window and run ngrok, pointing it to the port your Node.js app is running on (default is 3000).
bashngrok http 3000ngrok will display output similar to this:
Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-cal-1) Web Interface http://127.0.0.1:4040 Forwarding https://<random-id>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00Copy the
https://<random-id>.ngrok-free.appURL. This is your public base URL. -
Update
.env: Open your.envfile and set theBASE_URLto your ngrok Forwarding URL:dotenv# .env # ... other variables BASE_URL=https://<random-id>.ngrok-free.app -
Update Vonage Application Webhooks: Go back to your application settings in the Vonage Dashboard. Update the Messages capability webhook URLs using your ngrok
BASE_URL:- Inbound URL:
https://<random-id>.ngrok-free.app/webhooks/inbound - Status URL:
https://<random-id>.ngrok-free.app/webhooks/status - Click ""Save changes"".
- Inbound URL:
-
Run the Node.js Application: In your original terminal window (where your project code is), start the server:
bashnode index.jsYou should see output indicating the server is running and listening.
-
Test Sending SMS: Open another terminal or use a tool like Postman to send a POST request to your local
/send-smsendpoint. Replace<your_test_phone>with your actual mobile number in E.164 format (e.g.,+14155550101).bashcurl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+14155550101"", ""text"": ""Testing Vonage Status Callbacks! Time: '\''$(date)'\''"" }'You should see:
- Logs in your Node.js application terminal showing the SMS submission attempt and the
messageUuid. - Shortly after, you should receive the SMS on your test phone.
- Logs in your Node.js application terminal showing the SMS submission attempt and the
-
Observe Status Webhooks: Watch your Node.js application terminal. As the message progresses through the delivery lifecycle, Vonage will send POST requests to your
/webhooks/statusendpoint. You should see logs like:Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: submitted Timestamp: 2023-10-26T10:00:05.123Z Full Payload: { ... } ------------------------------ Webhook signature verified successfully. --- Delivery Status Received --- Message UUID: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Status: delivered Timestamp: 2023-10-26T10:00:07.456Z Full Payload: { ... } ------------------------------Common statuses include
submitted,delivered,rejected,failed,accepted(intermediate carrier status),undeliverable. -
Inspect with ngrok Web Interface: Open
http://127.0.0.1:4040in your browser. This interface shows all requests forwarded by ngrok, allowing you to inspect the exact headers (includingAuthorization) and payloads received from Vonage, which is invaluable for debugging.
5. Error handling and logging
Production applications require more robust error handling and logging.
- Webhook Handler Reliability: Wrap the logic inside your webhook handlers (
/webhooks/status,/webhooks/inbound) intry...catchblocks to prevent the server from crashing due to unexpected errors in processing the payload. Always ensure a200 OKis sent back to Vonage unless there's a signature verification failure (which returns401). - Logging: While
console.logis used in this guide for simplicity, replace it with a structured logger like Winston or Pino in production. This allows for log levels (info, warn, error), formatting (JSON), and easier integration with log management systems.- Log critical information:
message_uuid,status,timestamp, recipient/sender numbers, and any error codes/reasons. - Log errors during SMS sending (
catchblock insendSms) and webhook processing.
- Log critical information:
- Vonage Retry Mechanism: Remember that Vonage retries webhook delivery if it doesn't receive a
200 OKresponse within a short timeout (typically a few seconds). Ensure your processing is fast or happens asynchronously (e.g., push the payload to a queue) to avoid timeouts and duplicate processing from retries. Your endpoint must be idempotent if possible. - Detailed Send Errors: The
catchblock insendSmsshould inspect theerr.response.dataobject from the Vonage SDK for detailed error information provided by the API (e.g., invalid number format, insufficient funds).
// Example: Enhanced Status Webhook Handler (Conceptual)
const pino = require('pino'); // Example using Pino
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
app.post('/webhooks/status', verifyVonageSignature, async (req, res) => {
const statusData = req.body;
const logData = { // Structure your logs
message_uuid: statusData.message_uuid,
status: statusData.status,
timestamp: statusData.timestamp,
error_code: statusData.error?.code,
error_reason: statusData.error?.reason,
webhook_type: 'status'
};
logger.info(logData, 'Processing status update for message');
try {
// --- Add your business logic here ---
// Example: Update a database record based on status
// await updateMessageStatusInDB(statusData.message_uuid, statusData.status, statusData.timestamp, statusData.error);
if (statusData.error) {
logger.warn(logData, `Message status update indicates error`);
}
// ------------------------------------
res.status(200).send('OK'); // Acknowledge receipt *after* basic processing attempt
} catch (error) {
logger.error({ err: error, ...logData }, 'Error processing status webhook');
// Still send 200 OK if the error is in *your* processing logic,
// unless you specifically want Vonage to retry (use with caution).
// If the error indicates bad data you can't handle, 200 might still be appropriate
// to prevent endless retries. Log the error thoroughly.
// Consider sending 500 only for unexpected server errors preventing acknowledgement.
res.status(500).send('Internal Server Error during processing'); // Or potentially 200 OK depending on error strategy
}
});6. Security considerations
Securing your application and webhooks is vital.
- Webhook Signature Verification: Always verify the JWT signature using the
VONAGE_SIGNATURE_SECRETand the raw request body, as implemented in theverifyVonageSignaturemiddleware. This is the primary mechanism to ensure the request genuinely originated from Vonage and hasn't been tampered with. - API Credential Security:
- Never commit your
.envfile orprivate.keyto version control. Use.gitignore. - In production, use environment variables provided by your hosting platform or a dedicated secrets management service (like AWS Secrets Manager, HashiCorp Vault, etc.) instead of a
.envfile.
- Never commit your
- HTTPS: Always use HTTPS for your webhook URLs in production. ngrok provides HTTPS forwarding, and your deployment environment should also be configured for HTTPS.
- Input Validation: Sanitize and validate any user-provided input, especially if exposing the
/send-smsendpoint publicly. The E.164 check is a good start; also consider message lengths and potentially filter content. - Rate Limiting: If the
/send-smsendpoint is exposed, implement rate limiting (e.g., usingexpress-rate-limit) to prevent abuse.
7. Deployment
Deploying this application involves moving beyond ngrok.
- Choose a Hosting Platform: Options include Heroku, AWS (EC2, Lambda, Elastic Beanstalk), Google Cloud (Cloud Run, App Engine), DigitalOcean, Vercel, Render, etc.
- Environment Variables: Configure your production environment variables (
VONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_APPLICATION_ID,VONAGE_SIGNATURE_SECRET,VONAGE_NUMBER,PORT,VONAGE_PRIVATE_KEY_PATH) securely through your hosting provider's interface or secrets management. Ensure theprivate.keyfile is deployed securely to the location specified byVONAGE_PRIVATE_KEY_PATH. - Stable Public URL: Your deployed application will have a stable public URL (e.g.,
https://your-app-name.herokuapp.com). Update the webhook URLs in your Vonage Application settings to use this production URL (must be HTTPS). - Procfile/Dockerfile: Depending on the platform, you might need a
Procfile(Heroku) orDockerfile(container-based deployments) to define how to start your application (node index.js). - CI/CD: Set up a Continuous Integration/Continuous Deployment pipeline (e.g., GitHub Actions, GitLab CI, Jenkins) to automate testing and deployment.
8. Troubleshooting and Caveats
- Webhook Not Received:
- Check ngrok/server logs for errors (startup, request handling, signature verification).
- Verify the Status URL in the Vonage Dashboard exactly matches your ngrok/production URL +
/webhooks/status(case-sensitive, HTTPS in production). - Ensure your server is running and accessible from the internet (ngrok status should be ""online""; check production deployment status).
- Check the Vonage Dashboard API Logs and Application event logs for errors reported by Vonage when trying to reach your webhook (e.g., 4xx/5xx responses, timeouts).
- Firewall issues might block incoming requests to your server.
- Invalid Signature (401 Unauthorized):
- Double-check
VONAGE_SIGNATURE_SECRETin your environment variables matches the secret in the Vonage Application settings exactly (no extra spaces, etc.). - Confirm that
bodyParser.json({ verify: ... })is correctly capturing the raw body (req.rawBody) before theverifyVonageSignaturemiddleware runs. Use the ngrok web interface (http://127.0.0.1:4040) to inspect theAuthorizationheader on incoming requests. - Ensure the system clocks on your server and Vonage's servers are reasonably synchronized (JWTs have expiration times).
- Double-check
- SMS Not Sending:
- Check Node.js logs for errors from the
sendSmsfunction. Look closely aterr.response.datafor specific Vonage error codes and descriptions. - Verify API Key/Secret/Application ID/Private Key path are correct in your environment variables.
- Ensure your Vonage account has sufficient credit.
- Check if the destination number (
to) is valid and strictly in E.164 format (e.g.,+14155550101). - Confirm the Vonage number (
from) linked to the Application is SMS-capable for the destination country and correctly formatted in E.164. - Trial Account Limitations: New Vonage accounts might have restrictions (e.g., only sending to verified numbers listed in the dashboard) until topped up or fully verified.
- Check Node.js logs for errors from the
- Delayed Status Updates: Delivery receipts depend on downstream carriers; delays are possible. Not all carriers provide timely or reliable DLRs.
submittedoracceptedstatuses usually appear quickly, butdeliveredorfailedcan take longer or sometimes not arrive at all for certain destinations/networks. - Payload Variations: While the general structure is consistent, webhook payloads might occasionally have minor variations or new fields added by Vonage. Rely on documented core fields like
message_uuid,status,timestamp. Refer to the official Vonage Messages API documentation for the definitive schemas.
9. Verification checklist
Before considering the implementation complete, verify the following:
- Vonage account created and API credentials noted.
- Vonage Application created with Messages capability enabled.
- Private key downloaded, stored securely, and path correctly set in environment variables.
- Application ID and Signature Secret correctly set in environment variables.
- Vonage SMS-capable number purchased, correctly formatted (E.164), and linked to the Application.
- Status and Inbound webhook URLs correctly configured in Vonage Application settings (pointing to ngrok or production HTTPS URL).
- Node.js project initialized, dependencies installed (
express,@vonage/server-sdk,dotenv,jsonwebtoken,body-parser). -
.envfile created (or environment variables set) with all necessary variables filled. -
index.jscontains Express server setup, Vonage SDK initialization,bodyParserwith raw body capture,verifyVonageSignaturemiddleware, and webhook handlers (/webhooks/status). -
verifyVonageSignaturemiddleware is applied to webhook routes and correctly uses the raw body for verification. - Webhook handlers log incoming data and return
200 OKupon successful processing acknowledgment. -
sendSmsfunction correctly usesvonage.messages.sendand validates thetonumber format (strict E.164). - Application runs locally without errors (
node index.js). - ngrok (or production deployment) correctly forwards requests to the Node.js application.
- Sending a test SMS via the
/send-smsendpoint (or other trigger) succeeds (check logs). - Test SMS is received on the target device.
- Status webhook events (
submitted,delivered, etc.) are received and logged by the/webhooks/statusendpoint.