Build Two-Way SMS: Node.js, Express & Vonage Messages API Guide
A step-by-step guide to building a Node.js/Express application for sending and receiving SMS messages using the Vonage Messages API, covering setup, implementation, webhooks, and best practices.
This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to handle both sending and receiving SMS messages via the Vonage Messages API. We'll cover everything from project setup and core implementation to security considerations and deployment.
By the end of this tutorial, you will have a functional application capable of:
Sending SMS messages programmatically using the Vonage Node.js SDK.
Receiving inbound SMS messages sent to your Vonage virtual number via webhooks.
This enables use cases like notifications, alerts, basic customer interactions, or integrating SMS into existing workflows.
Project Overview and Goals
What We're Building: A simple Node.js server using Express that exposes a webhook to receive incoming SMS messages from Vonage and includes a script to send outgoing SMS messages.
Problem Solved: This provides a foundational structure for integrating two-way SMS communication into applications without needing complex infrastructure. It demonstrates the core mechanics of interacting with a powerful communications API.
Technologies Used:
Node.js: A JavaScript runtime environment for building server-side applications.
Express.js: A minimal and flexible Node.js web application framework for handling HTTP requests and routing.
Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. We'll use the @vonage/server-sdk Node.js library.
ngrok: A tool to expose local development servers to the internet, essential for testing webhooks.
dotenv: A module to load environment variables from a .env file into process.env.
System Architecture:
graph LR
A[Your Application (Node.js/Express)] <--> B(Vonage Messages API);
B <--> C(SMS Network);
C <--> D[End User's Phone];
subgraph Send SMS
A -- Send Request --> B;
end
subgraph Receive SMS
D -- Sends SMS --> C;
C -- Delivers to Vonage Number --> B;
B -- POST Request (Webhook) --> A;
end
subgraph Local Development
E[ngrok Tunnel] <--> A;
B -- Webhook via ngrok --> E;
end
Node.js and npm (or yarn) installed locally. Check with node -v and npm -v.
ngrok installed globally or available in your PATH (Download here).
A text editor (like VS Code).
Basic understanding of JavaScript and Node.js concepts.
A Vonage virtual phone number capable of sending/receiving SMS.
Final Outcome: A local Node.js application that can successfully send an SMS message when a script is run and log incoming SMS messages received on your Vonage number to the console. We will also discuss production considerations like security and deployment.
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 the project, then navigate into it.
Bash
mkdir vonage-sms-app
cd vonage-sms-app
Initialize npm:
Create a package.json file to manage project dependencies and scripts.
Bash
npm init -y
The -y flag accepts the default settings.
Install Dependencies:
We need express for the web server, @vonage/server-sdk to interact with the Vonage API, and dotenv to manage our credentials securely.
Bash
npm install express @vonage/server-sdk dotenv
Create Core Files:
Create the main files for our application logic and environment variables.
Bash
touch server.js send-sms.js .env .gitignore
server.js: Will contain the Express server code for receiving messages.
send-sms.js: Will contain the script for sending messages.
.env: Will store our secret API keys and configuration (DO NOT commit this file).
.gitignore: Specifies files and directories that Git should ignore.
Configure .gitignore:
Add essential patterns to your .gitignore file to prevent committing sensitive information and unnecessary files.
Set up Environment Variables (.env):
Open the .env file and add the following placeholders. We will populate these values in the ""Integrating with Vonage"" section.
.env
# Vonage API Credentials (Found in your Vonage Dashboard)
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application Details (Generated when creating a Vonage Application)
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key # Path to your downloaded private key file
# Phone Numbers
VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Your purchased Vonage number (E.164 format, e.g., 12015550123)
MY_TEST_NUMBER=YOUR_PERSONAL_PHONE_NUMBER # Your mobile number for testing (E.164 format)
# Server Configuration
PORT=3000
# Optional: Vonage Webhook Signature Secret (See Section 7)
# VONAGE_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET
Why .env? Using environment variables keeps sensitive credentials out of your codebase, making it more secure and easier to manage different configurations for development, staging, and production. The dotenv library loads these into process.env when the application starts.
Our basic project structure is now ready.
2. Implementing Core Functionality
We'll split the core logic into two parts: sending SMS and receiving SMS.
Sending SMS (send-sms.js)
This script will initialize the Vonage client using Application ID and Private Key authentication (recommended for server-side applications) and send a single SMS message.
Edit send-sms.js:
Add the following code:
JavaScript
// send-sms.jsrequire('dotenv').config(); // Load environment variables from .env fileconst { Vonage } = require('@vonage/server-sdk');
// --- Configuration ---const vonageNumber = process.env.VONAGE_NUMBER;
const recipientNumber = process.env.MY_TEST_NUMBER; // Your personal number for testingconst messageText = ""HellofromVonage and Node.js!"";
// --- Input Validation ---if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH) {
console.error(""Error: VonageApplicationID or PrivateKeyPath not set in .env file."");
process.exit(1);
}
if (!vonageNumber || !recipientNumber) {
console.error(""Error: VONAGE_NUMBER or MY_TEST_NUMBER not set in .env file."");
process.exit(1);
}
// --- Initialize Vonage Client ---// Authentication uses Application ID and Private Keyconst vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
}, { debug: false }); // Set debug: true for verbose SDK logging// --- Send SMS Function ---asyncfunctionsendSms() {
console.log(`Attempting to send SMS from ${vonageNumber} to ${recipientNumber}...`);
try {
const resp = await vonage.messages.send({
message_type: ""text"",
text: messageText,
to: recipientNumber,
from: vonageNumber, // Must be your Vonage virtual numberchannel: ""sms""
});
console.log(""SMSSentSuccessfully!"");
console.log(""MessageUUID:"", resp.message_uuid); // Unique ID for the message
} catch (err) {
console.error(""Error sending SMS:"");
// Log the detailed error response if availableif (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
process.exitCode = 1; // Indicate failure
}
}
// --- Execute ---sendSms();
Explanation:
require('dotenv').config(): Loads variables from .env.
new Vonage({...}): Initializes the SDK client. We use Application ID and Private Key for authentication, which is more secure for backend services than API Key/Secret.
vonage.messages.send({...}): The core function to send a message. We specify:
message_type: ""text"": Plain text SMS.
text: The content of the message.
to: The recipient's phone number (from .env).
from: The sender's phone number (must be your Vonage number, from .env).
channel: ""sms"": Specifies the channel.
async/await with try...catch: Handles the asynchronous nature of the API call and catches potential errors. Detailed error logging helps debugging.
Receiving SMS (server.js)
This script sets up an Express server to listen for incoming POST requests from Vonage on a specific webhook URL.
Edit server.js:
Add the following code:
JavaScript
// server.jsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
// --- Configuration ---constPORT = process.env.PORT || 3000;
const inboundWebhookPath = '/webhooks/inbound';
const statusWebhookPath = '/webhooks/status';
// --- Create Express App ---const app = express();
// --- Middleware ---// Vonage sends webhook data as JSON or URL-encoded form data depending on API/settings.// Use both parsers to be safe.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// --- Routes ---// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Vonage Inbound Message Webhook Handler
app.post(inboundWebhookPath, (req, res) => {
console.log(""--- InboundSMSReceived ---"");
console.log(""Timestamp:"", newDate().toISOString());
console.log(""RequestBody:"", JSON.stringify(req.body, null, 2)); // Log the entire payload// Extract key information (adjust based on actual payload structure from Vonage docs)constfrom = req.body.from;
const to = req.body.to;
const text = req.body.text;
const messageId = req.body.message_uuid;
console.log(`From: ${from}`);
console.log(`To: ${to}`);
console.log(`Message Text: ${text}`);
console.log(`Message UUID: ${messageId}`);
// **IMPORTANT:** Respond with 200 OK quickly!// Vonage expects a timely 200 response to acknowledge receipt.// Failure to respond promptly may result in retries from Vonage.// Offload any heavy processing (DB writes, external API calls)// to a background job/queue if necessary (See Section 9).
res.status(200).end();
});
// Vonage Status Webhook Handler (Delivery Receipts - DLRs)
app.post(statusWebhookPath, (req, res) => {
console.log(""--- MessageStatusUpdateReceived ---"");
console.log(""Timestamp:"", newDate().toISOString());
console.log(""RequestBody:"", JSON.stringify(req.body, null, 2));
// **IMPORTANT FOR PRODUCTION:** Parse the request body here!// You need to extract the message UUID, status (e.g., 'delivered', 'failed'),// timestamp, error codes, etc., to track the status of messages you SENT.// This is crucial for reliability and understanding message delivery success.// Example fields to look for (check Vonage docs for exact structure):// const messageUuid = req.body.message_uuid;// const status = req.body.status;// const timestamp = req.body.timestamp;// console.log(`Status for ${messageUuid}: ${status} at ${timestamp}`);// Update your database or application state based on the status.
res.status(200).end(); // Acknowledge receipt
});
// --- Error Handling Middleware (Basic) ---
app.use((err, req, res, next) => {
console.error(""--- UnhandledError ---"");
console.error(err.stack);
res.status(500).send('Something broke!');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`Inbound SMS Webhook available at http://localhost:${PORT}${inboundWebhookPath}`);
console.log(`Status Webhook available at http://localhost:${PORT}${statusWebhookPath}`);
console.log('Run ngrok to expose this server: ngrok http', PORT);
});
Explanation:
express(): Creates an Express application instance.
app.use(express.json()) & app.use(express.urlencoded(...)): Middleware to parse incoming request bodies. Vonage might send JSON or form data, so using both covers common cases.
app.post('/webhooks/inbound', ...): Defines the route handler for POST requests made by Vonage to /webhooks/inbound when an SMS is received.
console.log(req.body): Logs the data sent by Vonage (sender number, recipient number, message text, message ID, timestamp, etc.).
res.status(200).end(): Crucial step. Sends an HTTP 200 OK status back to Vonage immediately to acknowledge receipt. If Vonage doesn't receive a 200 OK quickly, it will assume the delivery failed and may retry sending the webhook, leading to duplicate processing.
/webhooks/status: Handler for Delivery Receipts (DLRs), which inform you about the status (e.g., delivered, failed) of messages you sent. Parsing the body is essential in production.
/health: A simple endpoint for health checks.
app.listen(PORT, ...): Starts the server on the specified port (from .env or default 3000).
3. Building a Complete API Layer (Optional Expansion)
The current setup uses a script (send-sms.js) for sending and a webhook handler (server.js) for receiving. For more complex applications, you might expose an API endpoint for sending SMS instead of using a standalone script.
Note: The following example demonstrates adding an API endpoint to server.js. Remember that the // TODO: comments highlight essential steps for production readiness, not optional improvements. Also, if you integrate this into server.js, ensure the Vonage client is initialized only once for the entire application.
Example API Endpoint for Sending:
You could add this to server.js:
JavaScript
// --- Add Vonage Client initialization near the top of server.js ---// Ensure this is done only ONCE if merging with the server.const { Vonage } = require('@vonage/server-sdk');
const vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH,
}, { debug: false });
// --- API Endpoint for Sending SMS ---
app.post('/api/send-sms', async (req, res) => {
// --- Authentication/Authorization (ESSENTIAL FOR PRODUCTION) ---// TODO: Implement proper API key check, JWT validation, session check,// or other auth mechanism suitable for your application.// Example placeholder: Check for a specific header// const apiKey = req.headers['x-api-key'];// if (!isValidApiKey(apiKey)) { // Replace isValidApiKey with your actual validation logic// return res.status(401).json({ error: 'Unauthorized' });// }// --- Request Validation (ESSENTIAL FOR PRODUCTION) ---const { to, text } = req.body;
if (!to || !text || typeof to !== 'string' || typeof text !== 'string') {
return res.status(400).json({ error: 'Invalid request body. Required: ""to"" (string), ""text"" (string)' });
}
// TODO: Add more robust validation:// - Check 'to' number format (E.164 recommended, use libphonenumber-js or similar)// - Check 'text' length limits (consider SMS segment limits)// - Sanitize input if used elsewhere (e.g., logging, database)const vonageNumber = process.env.VONAGE_NUMBER;
if (!vonageNumber) {
console.error(""VONAGE_NUMBER not configured."");
return res.status(500).json({ error: 'Server configuration error.' });
}
try {
console.log(`API sending SMS from ${vonageNumber} to ${to}`);
const resp = await vonage.messages.send({
message_type: ""text"", text, to, from: vonageNumber, channel: ""sms""
});
console.log(""APISMSSentSuccessfully. UUID:"", resp.message_uuid);
res.status(200).json({ success: true, message_uuid: resp.message_uuid });
} catch (err) {
console.error(""APIError sending SMS:"");
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
// Provide a generic error message to the client for security
res.status(500).json({ success: false, error: 'Failed to send SMS.' });
}
});
// --- Add error handling and server start from original server.js ---// ... (rest of server.js code: error handler, app.listen) ...
Testing with cURL:
Bash
curl -X POST http://localhost:3000/api/send-sms \
-H ""Content-Type: application/json"" \
# -H ""X-API-Key: YOUR_SECRET_API_KEY"" # If you implemented auth
-d '{
""to"": ""RECIPIENT_E164_NUMBER"",
""text"": ""API Test Message!""
}'
{""success"":false,""error"":""Failed to send SMS.""}
This demonstrates structuring the sending logic as a proper API endpoint. Remember that the placeholder authentication and validation comments (// TODO:) represent critical steps needed for any real-world application.
4. Integrating with Vonage
Now, let's configure our Vonage account and populate the .env file with the necessary credentials.
Get API Key and Secret:
On the main dashboard page, you'll find your API Key and API Secret (you might need to reveal it) usually under ""API settings"" or your account profile.
Copy the API key and paste it as the value for VONAGE_API_KEY in your .env file.
Copy the API secret and paste it as the value for VONAGE_API_SECRET in your .env file.
(Note: While we use Application ID/Private Key for sending via Messages API, the Key/Secret might be needed for other SDK functions or account management).
Purchase a Vonage Number:
Navigate to Numbers -> Buy numbers.
Search for a number with SMS capability in your desired country.
Purchase the number.
Copy the purchased number (in E.164 format, e.g., 12015550123 for a US number) and paste it as the value for VONAGE_NUMBER in your .env file.
Also, add your personal mobile number (for testing) in E.164 format as the value for MY_TEST_NUMBER.
Create a Vonage Application:
This application acts as a container for your configuration and security credentials (specifically the private key) needed for the Messages API.
Navigate to Applications -> Create a new application.
Give your application a name (e.g., Node Express SMS App).
Click Generate public and private key. This will automatically download a private.key file. Save this file securely in your project's root directory (the same place as your .env file). Make sure the path in your .env file (VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key) matches the location and name of this file. Ensure private.key is listed in your .gitignore file.
Under Capabilities, find Messages and toggle it ON.
Two URL fields will appear: Inbound URL and Status URL. We need ngrok running first to get the public URLs for these.
Run ngrok:
Open a new terminal window in your project directory. Start ngrok to expose your local server (running on port 3000, as defined in .env or the default in server.js) to the internet.
Bash
ngrok http 3000
ngrok will display output similar to this:
Code
Session Status online
Account Your Name (Plan: Free)
Version 3.x.x
Region United States (us-cal-1)
Web Interface http://127.0.0.1:4040
Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000
Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000 # <-- COPY THIS HTTPS URL
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https:// Forwarding URL. Keep this terminal window open while testing.
Configure Application Webhooks:
Go back to your Vonage Application configuration page (Step 4).
In the Inbound URL field, paste your ngrok HTTPS URL and append the path /webhooks/inbound. Example: https://xxxxxxxx.ngrok.io/webhooks/inbound
In the Status URL field, paste your ngrok HTTPS URL and append the path /webhooks/status. Example: https://xxxxxxxx.ngrok.io/webhooks/status
Click Generate application or Save changes.
You will be taken to the application's details page. Copy the Application ID displayed here.
Paste the Application ID as the value for VONAGE_APPLICATION_ID in your .env file.
Link Your Number to the Application:
Navigate to Numbers -> Your numbers.
Find the Vonage number you purchased earlier.
Click the Manage button (or the gear icon/three dots) next to the number.
In the Forwarding or Application Assignment section (UI might vary), select Forward to Application or Assign Application.
Choose the Application you just created (e.g., Node Express SMS App) from the dropdown menu.
Click Save.
Set Default SMS API (CRITICAL):
The Vonage Node.js SDK's vonage.messages.send method relies on the account's default API setting being Messages API.
In the Vonage Dashboard, navigate to Account -> Settings.
Scroll down to the API settings section.
Find Default SMS Setting.
Ensure Messages API is selected. If not, select it and click Save changes. This ensures the webhooks and API calls use the expected format and capabilities for the Messages API.
Your .env file should now be fully populated with your credentials and configuration, and your Vonage account should be correctly set up to route incoming messages to your local application via ngrok.
5. Implementing Error Handling, Logging, and Retry Mechanisms
Our basic scripts include some error handling, but let's refine it.
Sending Errors (send-sms.js):
The try...catch block already handles API errors.
Logging err.response.data provides detailed error information from Vonage (e.g., insufficient funds, invalid number format, throttling).
Retries: For transient network errors or temporary Vonage issues (like 429 Too Many Requests or 5xx server errors), you could implement a retry mechanism using libraries like async-retry around the vonage.messages.send call.
JavaScript
// Example using async-retry (install with: npm install async-retry)const retry = require('async-retry');
asyncfunctionsendSmsWithRetry() {
// Define message details here or pass them as argumentsconst messageDetails = {
message_type: ""text"",
text: ""Your message content"", // Replace with actual textto: process.env.MY_TEST_NUMBER,
from: process.env.VONAGE_NUMBER,
channel: ""sms""
};
awaitretry(async (bail, attemptNumber) => {
// If Vonage returns specific error codes indicating a permanent failure,// use bail() to stop retrying. Example: Invalid credentials (401), Invalid number (400).// Check err.response.status or err.response.data.type for specific conditions.try {
console.log(`Attempting to send SMS (attempt ${attemptNumber})...`);
const resp = await vonage.messages.send(messageDetails);
console.log(""SMSSentSuccessfully!"");
console.log(""MessageUUID:"", resp.message_uuid);
return resp; // Return result on success
} catch (err) {
console.warn(`Attempt ${attemptNumber} failed: ${err.message}`);
if (err.response && (err.response.status === 401 || err.response.status === 400)) {
console.error(""Permanent error detected, not retrying."");
bail(err); // Stop retrying for permanent errorsreturn; // Important to exit after bail
}
// For other errors (like 5xx, 429, network issues), throw to trigger retrythrow err;
}
}, {
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Initial timeout (ms)onRetry: (error, attempt) => {
console.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`);
}
});
}
// In send-sms.js, replace the call to sendSms() with:// sendSmsWithRetry().catch(err => {// console.error(""Failed to send SMS after multiple retries:"", err);// process.exitCode = 1;// });
Receiving Errors (server.js):
The main priority is sending res.status(200).end() quickly.
Wrap the core logic within the webhook handler in a try...catch block to handle unexpected errors during processing (e.g., issues parsing req.body, database errors if added).
Log errors thoroughly within the catch block.
Decide whether to still send 200 OK even if processing fails (to prevent Vonage retries) or send a 500 Internal Server Error if the failure is critical and retrying might help (though Vonage might retry anyway). Sending 200 OK is generally preferred unless you have specific retry logic tied to 5xx errors.
JavaScript
// Inside app.post('/webhooks/inbound', ...)
app.post(inboundWebhookPath, async (req, res) => { // Make handler async if using await insidetry {
console.log(""--- InboundSMSReceived ---"");
console.log(""Timestamp:"", newDate().toISOString());
console.log(""RequestBody:"", JSON.stringify(req.body, null, 2));
const { from, to, text, message_uuid } = req.body;
console.log(`From: ${from}`);
console.log(`To: ${to}`);
console.log(`Message Text: ${text}`);
console.log(`Message UUID: ${message_uuid}`);
// --- Add your business logic here ---// Example: await saveMessageToDatabase(from, to, text, message_uuid);// Example: await triggerAnotherApiCall(text);// --- End business logic ---// If business logic succeeds, acknowledge receipt
res.status(200).end();
} catch (error) {
console.error(""--- Error processing inbound SMS ---"");
console.error(""Timestamp:"", newDate().toISOString());
console.error(""RequestBody:"", JSON.stringify(req.body, null, 2));
console.error(""ErrorStack:"", error.stack);
// Decide on response:// Option 1 (Recommended): Acknowledge receipt even on processing error// to prevent Vonage retries for potentially malformed but acknowledged data.// Log the error thoroughly for investigation.if (!res.headersSent) { // Check if response already sent
res.status(200).end();
}
// Option 2: Signal server error (Vonage might retry, potentially causing duplicates if the error is intermittent)// Use this cautiously, only if a retry is genuinely desired for this specific error.// if (!res.headersSent) { // Ensure headers haven't already been sent// res.status(500).send('Internal Server Error');// }
}
});
// Note: res.status(200).end() is now inside the try/catch blocks.
Logging:
console.log is suitable for development.
For production, use a structured logging library like Pino or Winston. These enable:
Log Levels:info, warn, error, debug.
Structured Formats: JSON output for easier parsing by log analysis tools (Datadog, Splunk, ELK stack).
Transports: Sending logs to files, databases, or external services.
Log key events: Application start, server listening, API calls (request/response summaries), webhook received (key data), errors (with stack traces).
6. Creating a Database Schema and Data Layer (Optional Expansion)
If you need to store message history or related data, you'll need a database.
Choose a Database: PostgreSQL, MongoDB, MySQL, etc.
Choose an ORM/Driver: Prisma, Sequelize, Mongoose, pg, mysql2.
Define Schema:
Example (using Prisma schema syntax):
Prisma
// schema.prisma
datasource db {
provider = ""postgresql"" // or ""mysql"", ""mongodb"", etc.
url = env(""DATABASE_URL"")
}
generator client {
provider = ""prisma-client-js""
}
model SmsMessage {
id String @id @default(cuid())
vonageMsgId String @unique // The message_uuid from Vonage
direction String // ""inbound"" or ""outbound""
fromNumber String
toNumber String
body String?
status String? // e.g., ""submitted"", ""delivered"", ""failed"", ""received"" (for inbound)
vonageStatusPayload Json? // Store the full status webhook payload for reference
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([createdAt])
@@index([fromNumber])
@@index([toNumber])
}
Apply schema: npx prisma db push (for development) or npx prisma migrate dev (for migrations).
Use the Prisma client in your server.js (or a dedicated data service layer):
JavaScript
// server.js (add near top)const { PrismaClient } = require('@prisma/client');
const prisma = newPrismaClient();
// Inside app.post('/webhooks/inbound', ...) try block:// Make the handler async: app.post(inboundWebhookPath, async (req, res) => { ... });try {
// ... previous logging ...const { from, to, text, message_uuid } = req.body;
// NOTE: For production/high-volume, move this DB operation// to a background queue (See Section 9) to ensure the webhook// responds quickly.await prisma.smsMessage.create({
data: {
vonageMsgId: message_uuid,
direction: 'inbound',
fromNumber: from,
toNumber: to,
body: text,
status: 'received', // Initial status for inbound// Use Vonage timestamp if available and valid, otherwise fallbackcreatedAt: req.body.timestamp && !isNaN(Date.parse(req.body.timestamp)) ? newDate(req.body.timestamp) : newDate(),
},
});
console.log(`Inbound message ${message_uuid} saved to database.`);
// Acknowledge receipt AFTER successful processing (if not queueing)if (!res.headersSent) {
res.status(200).end();
}
} catch (error) {
console.error(""--- Error processing inbound SMS ---"");
console.error(""Timestamp:"", newDate().toISOString());
console.error(""RequestBody:"", JSON.stringify(req.body, null, 2));
console.error(""ErrorStack:"", error.stack);
console.error(""Database error during inbound processing:"", error);
// Still acknowledge receipt to prevent Vonage retries for DB issuesif (!res.headersSent) {
res.status(200).end();
}
} finally {
// Optional: Add graceful shutdown logic for Prisma connection// process.on('SIGINT', async () => { await prisma.$disconnect(); process.exit(); });
}
// res.status(200).end() moved inside try/catch/finally logic
Similarly, update send-sms.js or the /api/send-sms endpoint to record outbound messages and update their status based on the Status Webhook (/webhooks/status). Remember to handle potential database errors gracefully in all data access points.
Frequently asked questions
Use the Vonage Messages API with the Node.js SDK. After setting up a Vonage application and number, initialize the Vonage client with your API credentials. Then, use vonage.messages.send() with the recipient's number, your Vonage number, and the message text. This function handles sending the SMS through the Vonage platform.
The Vonage Messages API is a unified platform for sending and receiving messages across various channels like SMS, WhatsApp, Viber, and Facebook Messenger. This guide specifically focuses on SMS, enabling you to build applications that can send notifications, alerts, conduct basic customer interactions, or integrate SMS into existing workflows.
Ngrok creates a secure tunnel from your local development server to the public internet, allowing Vonage to reach your webhook endpoints during development. Since Vonage delivers webhook events via HTTP requests to public URLs, ngrok provides a way to make your local server temporarily accessible externally.
Create an Express.js route that handles POST requests to a specific webhook URL (e.g., /webhooks/inbound). Configure your Vonage application to send incoming SMS messages to this URL. The request body will contain message details like sender, recipient, and text. Respond to Vonage with a 200 OK status code to acknowledge receipt.
A Vonage Application ID is a unique identifier for your application within the Vonage platform. It's essential for authenticating and authorizing access to Vonage APIs, including the Messages API used for sending SMS. The guide demonstrates how to create an application and obtain its ID in the Vonage Dashboard.
Application ID/Private Key authentication is recommended for server-side applications interacting with Vonage APIs, especially those sending messages like SMS. This approach, as demonstrated in the provided code examples, enhances security over using API Key/Secret directly in your codebase.
Define POST routes in your Express application for inbound messages (/webhooks/inbound) and delivery receipts (/webhooks/status). Provide the ngrok HTTPS URL with these paths as webhooks in your Vonage Application settings. Ensure your Vonage number is linked to this application to receive incoming SMS and delivery status updates.
Yes, you can change the port your Express server runs on. Modify the PORT environment variable in your .env file or the default port value in server.js. Remember to update the ngrok command and Vonage webhook URLs accordingly if you change the port.
The dotenv library securely loads environment variables from a .env file into process.env. This allows you to store sensitive information like API keys and secrets outside your codebase, improving security and configuration management across different environments (development, staging, production).
Set up a POST route (e.g., /webhooks/status) to receive DLRs. Vonage sends these to this URL when the message status changes (e.g., 'delivered', 'failed'). Parse req.body in this route to extract the message UUID, status, and timestamp. This information is crucial for tracking message delivery success and handling failures in production.
Responding with a 200 OK status code to Vonage webhooks is crucial to acknowledge message receipt and prevent retries. Vonage expects a prompt 200 OK, and if it doesn't receive this, it might assume failure and resend the webhook, potentially leading to duplicate message processing and incorrect behavior.
Define a database schema (e.g., using Prisma) with relevant fields like sender, recipient, message text, timestamps, and message status. Implement data access logic in your webhook handler (/webhooks/inbound) to save received SMS details to your database. For production applications, consider using a message queue for database operations to avoid blocking the webhook response.
The inbound SMS webhook payload is a JSON object containing message details. Refer to the Vonage API documentation for the exact structure, but it typically includes sender (from), recipient (to), message content (text), a unique message identifier (message_uuid), and a timestamp. The article provides examples of how to extract these fields from req.body.
This guide provides a step-by-step walkthrough for building a Node.js application using the Express framework to handle both sending and receiving SMS messages via the Vonage Messages API. We'll cover everything from project setup and core implementation to security considerations and deployment.
By the end of this tutorial, you will have a functional application capable of:
This enables use cases like notifications, alerts, basic customer interactions, or integrating SMS into existing workflows.
Project Overview and Goals
What We're Building: A simple Node.js server using Express that exposes a webhook to receive incoming SMS messages from Vonage and includes a script to send outgoing SMS messages.
Problem Solved: This provides a foundational structure for integrating two-way SMS communication into applications without needing complex infrastructure. It demonstrates the core mechanics of interacting with a powerful communications API.
Technologies Used:
@vonage/server-sdkNode.js library.ngrok: A tool to expose local development servers to the internet, essential for testing webhooks.dotenv: A module to load environment variables from a.envfile intoprocess.env.System Architecture:
Prerequisites:
node -vandnpm -v.ngrokinstalled globally or available in your PATH (Download here).Final Outcome: A local Node.js application that can successfully send an SMS message when a script is run and log incoming SMS messages received on your Vonage number to the console. We will also discuss production considerations like security and deployment.
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 the project, then navigate into it.
mkdir vonage-sms-app cd vonage-sms-appInitialize npm: Create a
package.jsonfile to manage project dependencies and scripts.The
-yflag accepts the default settings.Install Dependencies: We need
expressfor the web server,@vonage/server-sdkto interact with the Vonage API, anddotenvto manage our credentials securely.Create Core Files: Create the main files for our application logic and environment variables.
touch server.js send-sms.js .env .gitignoreserver.js: Will contain the Express server code for receiving messages.send-sms.js: Will contain the script for sending messages..env: Will store our secret API keys and configuration (DO NOT commit this file)..gitignore: Specifies files and directories that Git should ignore.Configure
.gitignore: Add essential patterns to your.gitignorefile to prevent committing sensitive information and unnecessary files.Set up Environment Variables (
.env): Open the.envfile and add the following placeholders. We will populate these values in the ""Integrating with Vonage"" section.Why
.env? Using environment variables keeps sensitive credentials out of your codebase, making it more secure and easier to manage different configurations for development, staging, and production. Thedotenvlibrary loads these intoprocess.envwhen the application starts.Our basic project structure is now ready.
2. Implementing Core Functionality
We'll split the core logic into two parts: sending SMS and receiving SMS.
Sending SMS (
send-sms.js)This script will initialize the Vonage client using Application ID and Private Key authentication (recommended for server-side applications) and send a single SMS message.
Edit
send-sms.js: Add the following code:// send-sms.js require('dotenv').config(); // Load environment variables from .env file const { Vonage } = require('@vonage/server-sdk'); // --- Configuration --- const vonageNumber = process.env.VONAGE_NUMBER; const recipientNumber = process.env.MY_TEST_NUMBER; // Your personal number for testing const messageText = ""Hello from Vonage and Node.js!""; // --- Input Validation --- if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH) { console.error(""Error: Vonage Application ID or Private Key Path not set in .env file.""); process.exit(1); } if (!vonageNumber || !recipientNumber) { console.error(""Error: VONAGE_NUMBER or MY_TEST_NUMBER not set in .env file.""); process.exit(1); } // --- Initialize Vonage Client --- // Authentication uses Application ID and Private Key const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH, }, { debug: false }); // Set debug: true for verbose SDK logging // --- Send SMS Function --- async function sendSms() { console.log(`Attempting to send SMS from ${vonageNumber} to ${recipientNumber}...`); try { const resp = await vonage.messages.send({ message_type: ""text"", text: messageText, to: recipientNumber, from: vonageNumber, // Must be your Vonage virtual number channel: ""sms"" }); console.log(""SMS Sent Successfully!""); console.log(""Message UUID:"", resp.message_uuid); // Unique ID for the message } catch (err) { console.error(""Error sending SMS:""); // Log the detailed error response if available if (err.response && err.response.data) { console.error(JSON.stringify(err.response.data, null, 2)); } else { console.error(err); } process.exitCode = 1; // Indicate failure } } // --- Execute --- sendSms();Explanation:
require('dotenv').config(): Loads variables from.env.new Vonage({...}): Initializes the SDK client. We use Application ID and Private Key for authentication, which is more secure for backend services than API Key/Secret.vonage.messages.send({...}): The core function to send a message. We specify:message_type: ""text"": Plain text SMS.text: The content of the message.to: The recipient's phone number (from.env).from: The sender's phone number (must be your Vonage number, from.env).channel: ""sms"": Specifies the channel.async/awaitwithtry...catch: Handles the asynchronous nature of the API call and catches potential errors. Detailed error logging helps debugging.Receiving SMS (
server.js)This script sets up an Express server to listen for incoming POST requests from Vonage on a specific webhook URL.
Edit
server.js: Add the following code:// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); // --- Configuration --- const PORT = process.env.PORT || 3000; const inboundWebhookPath = '/webhooks/inbound'; const statusWebhookPath = '/webhooks/status'; // --- Create Express App --- const app = express(); // --- Middleware --- // Vonage sends webhook data as JSON or URL-encoded form data depending on API/settings. // Use both parsers to be safe. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // --- Routes --- // Health check endpoint app.get('/health', (req, res) => { res.status(200).send('OK'); }); // Vonage Inbound Message Webhook Handler app.post(inboundWebhookPath, (req, res) => { console.log(""--- Inbound SMS Received ---""); console.log(""Timestamp:"", new Date().toISOString()); console.log(""Request Body:"", JSON.stringify(req.body, null, 2)); // Log the entire payload // Extract key information (adjust based on actual payload structure from Vonage docs) const from = req.body.from; const to = req.body.to; const text = req.body.text; const messageId = req.body.message_uuid; console.log(`From: ${from}`); console.log(`To: ${to}`); console.log(`Message Text: ${text}`); console.log(`Message UUID: ${messageId}`); // **IMPORTANT:** Respond with 200 OK quickly! // Vonage expects a timely 200 response to acknowledge receipt. // Failure to respond promptly may result in retries from Vonage. // Offload any heavy processing (DB writes, external API calls) // to a background job/queue if necessary (See Section 9). res.status(200).end(); }); // Vonage Status Webhook Handler (Delivery Receipts - DLRs) app.post(statusWebhookPath, (req, res) => { console.log(""--- Message Status Update Received ---""); console.log(""Timestamp:"", new Date().toISOString()); console.log(""Request Body:"", JSON.stringify(req.body, null, 2)); // **IMPORTANT FOR PRODUCTION:** Parse the request body here! // You need to extract the message UUID, status (e.g., 'delivered', 'failed'), // timestamp, error codes, etc., to track the status of messages you SENT. // This is crucial for reliability and understanding message delivery success. // Example fields to look for (check Vonage docs for exact structure): // const messageUuid = req.body.message_uuid; // const status = req.body.status; // const timestamp = req.body.timestamp; // console.log(`Status for ${messageUuid}: ${status} at ${timestamp}`); // Update your database or application state based on the status. res.status(200).end(); // Acknowledge receipt }); // --- Error Handling Middleware (Basic) --- app.use((err, req, res, next) => { console.error(""--- Unhandled Error ---""); console.error(err.stack); res.status(500).send('Something broke!'); }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`Inbound SMS Webhook available at http://localhost:${PORT}${inboundWebhookPath}`); console.log(`Status Webhook available at http://localhost:${PORT}${statusWebhookPath}`); console.log('Run ngrok to expose this server: ngrok http', PORT); });Explanation:
express(): Creates an Express application instance.app.use(express.json())&app.use(express.urlencoded(...)): Middleware to parse incoming request bodies. Vonage might send JSON or form data, so using both covers common cases.app.post('/webhooks/inbound', ...): Defines the route handler for POST requests made by Vonage to/webhooks/inboundwhen an SMS is received.console.log(req.body): Logs the data sent by Vonage (sender number, recipient number, message text, message ID, timestamp, etc.).res.status(200).end(): Crucial step. Sends an HTTP 200 OK status back to Vonage immediately to acknowledge receipt. If Vonage doesn't receive a 200 OK quickly, it will assume the delivery failed and may retry sending the webhook, leading to duplicate processing./webhooks/status: Handler for Delivery Receipts (DLRs), which inform you about the status (e.g.,delivered,failed) of messages you sent. Parsing the body is essential in production./health: A simple endpoint for health checks.app.listen(PORT, ...): Starts the server on the specified port (from.envor default 3000).3. Building a Complete API Layer (Optional Expansion)
The current setup uses a script (
send-sms.js) for sending and a webhook handler (server.js) for receiving. For more complex applications, you might expose an API endpoint for sending SMS instead of using a standalone script.Note: The following example demonstrates adding an API endpoint to
server.js. Remember that the// TODO:comments highlight essential steps for production readiness, not optional improvements. Also, if you integrate this intoserver.js, ensure the Vonage client is initialized only once for the entire application.Example API Endpoint for Sending:
You could add this to
server.js:// --- Add Vonage Client initialization near the top of server.js --- // Ensure this is done only ONCE if merging with the server. const { Vonage } = require('@vonage/server-sdk'); const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH, }, { debug: false }); // --- API Endpoint for Sending SMS --- app.post('/api/send-sms', async (req, res) => { // --- Authentication/Authorization (ESSENTIAL FOR PRODUCTION) --- // TODO: Implement proper API key check, JWT validation, session check, // or other auth mechanism suitable for your application. // Example placeholder: Check for a specific header // const apiKey = req.headers['x-api-key']; // if (!isValidApiKey(apiKey)) { // Replace isValidApiKey with your actual validation logic // return res.status(401).json({ error: 'Unauthorized' }); // } // --- Request Validation (ESSENTIAL FOR PRODUCTION) --- const { to, text } = req.body; if (!to || !text || typeof to !== 'string' || typeof text !== 'string') { return res.status(400).json({ error: 'Invalid request body. Required: ""to"" (string), ""text"" (string)' }); } // TODO: Add more robust validation: // - Check 'to' number format (E.164 recommended, use libphonenumber-js or similar) // - Check 'text' length limits (consider SMS segment limits) // - Sanitize input if used elsewhere (e.g., logging, database) const vonageNumber = process.env.VONAGE_NUMBER; if (!vonageNumber) { console.error(""VONAGE_NUMBER not configured.""); return res.status(500).json({ error: 'Server configuration error.' }); } try { console.log(`API sending SMS from ${vonageNumber} to ${to}`); const resp = await vonage.messages.send({ message_type: ""text"", text, to, from: vonageNumber, channel: ""sms"" }); console.log(""API SMS Sent Successfully. UUID:"", resp.message_uuid); res.status(200).json({ success: true, message_uuid: resp.message_uuid }); } catch (err) { console.error(""API Error sending SMS:""); if (err.response && err.response.data) { console.error(JSON.stringify(err.response.data, null, 2)); } else { console.error(err); } // Provide a generic error message to the client for security res.status(500).json({ success: false, error: 'Failed to send SMS.' }); } }); // --- Add error handling and server start from original server.js --- // ... (rest of server.js code: error handler, app.listen) ...Testing with cURL:
curl -X POST http://localhost:3000/api/send-sms \ -H ""Content-Type: application/json"" \ # -H ""X-API-Key: YOUR_SECRET_API_KEY"" # If you implemented auth -d '{ ""to"": ""RECIPIENT_E164_NUMBER"", ""text"": ""API Test Message!"" }'Response Example (Success):
{ ""success"": true, ""message_uuid"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"" }Response Example (Error):
{ ""success"": false, ""error"": ""Failed to send SMS."" }This demonstrates structuring the sending logic as a proper API endpoint. Remember that the placeholder authentication and validation comments (
// TODO:) represent critical steps needed for any real-world application.4. Integrating with Vonage
Now, let's configure our Vonage account and populate the
.envfile with the necessary credentials.Log in to Vonage Dashboard: Go to https://dashboard.vonage.com/ and log in.
Get API Key and Secret: On the main dashboard page, you'll find your API Key and API Secret (you might need to reveal it) usually under ""API settings"" or your account profile.
API keyand paste it as the value forVONAGE_API_KEYin your.envfile.API secretand paste it as the value forVONAGE_API_SECRETin your.envfile.(Note: While we use Application ID/Private Key for sending via Messages API, the Key/Secret might be needed for other SDK functions or account management).
Purchase a Vonage Number:
12015550123for a US number) and paste it as the value forVONAGE_NUMBERin your.envfile.MY_TEST_NUMBER.Create a Vonage Application: This application acts as a container for your configuration and security credentials (specifically the private key) needed for the Messages API.
Node Express SMS App).private.keyfile. Save this file securely in your project's root directory (the same place as your.envfile). Make sure the path in your.envfile (VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key) matches the location and name of this file. Ensureprivate.keyis listed in your.gitignorefile.ngrokrunning first to get the public URLs for these.Run
ngrok: Open a new terminal window in your project directory. Startngrokto expose your local server (running on port 3000, as defined in.envor the default inserver.js) to the internet.ngrokwill display output similar to this:Copy the
https://Forwarding URL. Keep this terminal window open while testing.Configure Application Webhooks:
ngrokHTTPS URL and append the path/webhooks/inbound. Example:https://xxxxxxxx.ngrok.io/webhooks/inboundngrokHTTPS URL and append the path/webhooks/status. Example:https://xxxxxxxx.ngrok.io/webhooks/statusVONAGE_APPLICATION_IDin your.envfile.Link Your Number to the Application:
Node Express SMS App) from the dropdown menu.Set Default SMS API (CRITICAL): The Vonage Node.js SDK's
vonage.messages.sendmethod relies on the account's default API setting being Messages API.Your
.envfile should now be fully populated with your credentials and configuration, and your Vonage account should be correctly set up to route incoming messages to your local application viangrok.5. Implementing Error Handling, Logging, and Retry Mechanisms
Our basic scripts include some error handling, but let's refine it.
Sending Errors (
send-sms.js):try...catchblock already handles API errors.err.response.dataprovides detailed error information from Vonage (e.g., insufficient funds, invalid number format, throttling).429 Too Many Requestsor5xxserver errors), you could implement a retry mechanism using libraries likeasync-retryaround thevonage.messages.sendcall.// Example using async-retry (install with: npm install async-retry) const retry = require('async-retry'); async function sendSmsWithRetry() { // Define message details here or pass them as arguments const messageDetails = { message_type: ""text"", text: ""Your message content"", // Replace with actual text to: process.env.MY_TEST_NUMBER, from: process.env.VONAGE_NUMBER, channel: ""sms"" }; await retry(async (bail, attemptNumber) => { // If Vonage returns specific error codes indicating a permanent failure, // use bail() to stop retrying. Example: Invalid credentials (401), Invalid number (400). // Check err.response.status or err.response.data.type for specific conditions. try { console.log(`Attempting to send SMS (attempt ${attemptNumber})...`); const resp = await vonage.messages.send(messageDetails); console.log(""SMS Sent Successfully!""); console.log(""Message UUID:"", resp.message_uuid); return resp; // Return result on success } catch (err) { console.warn(`Attempt ${attemptNumber} failed: ${err.message}`); if (err.response && (err.response.status === 401 || err.response.status === 400)) { console.error(""Permanent error detected, not retrying.""); bail(err); // Stop retrying for permanent errors return; // Important to exit after bail } // For other errors (like 5xx, 429, network issues), throw to trigger retry throw err; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial timeout (ms) onRetry: (error, attempt) => { console.warn(`Retrying SMS send (attempt ${attempt}) due to error: ${error.message}`); } }); } // In send-sms.js, replace the call to sendSms() with: // sendSmsWithRetry().catch(err => { // console.error(""Failed to send SMS after multiple retries:"", err); // process.exitCode = 1; // });Receiving Errors (
server.js):res.status(200).end()quickly.try...catchblock to handle unexpected errors during processing (e.g., issues parsingreq.body, database errors if added).catchblock.200 OKeven if processing fails (to prevent Vonage retries) or send a500 Internal Server Errorif the failure is critical and retrying might help (though Vonage might retry anyway). Sending200 OKis generally preferred unless you have specific retry logic tied to5xxerrors.// Inside app.post('/webhooks/inbound', ...) app.post(inboundWebhookPath, async (req, res) => { // Make handler async if using await inside try { console.log(""--- Inbound SMS Received ---""); console.log(""Timestamp:"", new Date().toISOString()); console.log(""Request Body:"", JSON.stringify(req.body, null, 2)); const { from, to, text, message_uuid } = req.body; console.log(`From: ${from}`); console.log(`To: ${to}`); console.log(`Message Text: ${text}`); console.log(`Message UUID: ${message_uuid}`); // --- Add your business logic here --- // Example: await saveMessageToDatabase(from, to, text, message_uuid); // Example: await triggerAnotherApiCall(text); // --- End business logic --- // If business logic succeeds, acknowledge receipt res.status(200).end(); } catch (error) { console.error(""--- Error processing inbound SMS ---""); console.error(""Timestamp:"", new Date().toISOString()); console.error(""Request Body:"", JSON.stringify(req.body, null, 2)); console.error(""Error Stack:"", error.stack); // Decide on response: // Option 1 (Recommended): Acknowledge receipt even on processing error // to prevent Vonage retries for potentially malformed but acknowledged data. // Log the error thoroughly for investigation. if (!res.headersSent) { // Check if response already sent res.status(200).end(); } // Option 2: Signal server error (Vonage might retry, potentially causing duplicates if the error is intermittent) // Use this cautiously, only if a retry is genuinely desired for this specific error. // if (!res.headersSent) { // Ensure headers haven't already been sent // res.status(500).send('Internal Server Error'); // } } }); // Note: res.status(200).end() is now inside the try/catch blocks.Logging:
console.logis suitable for development.info,warn,error,debug.6. Creating a Database Schema and Data Layer (Optional Expansion)
If you need to store message history or related data, you'll need a database.
Choose a Database: PostgreSQL, MongoDB, MySQL, etc.
Choose an ORM/Driver: Prisma, Sequelize, Mongoose,
pg,mysql2.Define Schema:
Implement Data Access:
npm install prisma --save-dev,npm install @prisma/client).npx prisma init.DATABASE_URLin.env.npx prisma db push(for development) ornpx prisma migrate dev(for migrations).server.js(or a dedicated data service layer):// server.js (add near top) const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); // Inside app.post('/webhooks/inbound', ...) try block: // Make the handler async: app.post(inboundWebhookPath, async (req, res) => { ... }); try { // ... previous logging ... const { from, to, text, message_uuid } = req.body; // NOTE: For production/high-volume, move this DB operation // to a background queue (See Section 9) to ensure the webhook // responds quickly. await prisma.smsMessage.create({ data: { vonageMsgId: message_uuid, direction: 'inbound', fromNumber: from, toNumber: to, body: text, status: 'received', // Initial status for inbound // Use Vonage timestamp if available and valid, otherwise fallback createdAt: req.body.timestamp && !isNaN(Date.parse(req.body.timestamp)) ? new Date(req.body.timestamp) : new Date(), }, }); console.log(`Inbound message ${message_uuid} saved to database.`); // Acknowledge receipt AFTER successful processing (if not queueing) if (!res.headersSent) { res.status(200).end(); } } catch (error) { console.error(""--- Error processing inbound SMS ---""); console.error(""Timestamp:"", new Date().toISOString()); console.error(""Request Body:"", JSON.stringify(req.body, null, 2)); console.error(""Error Stack:"", error.stack); console.error(""Database error during inbound processing:"", error); // Still acknowledge receipt to prevent Vonage retries for DB issues if (!res.headersSent) { res.status(200).end(); } } finally { // Optional: Add graceful shutdown logic for Prisma connection // process.on('SIGINT', async () => { await prisma.$disconnect(); process.exit(); }); } // res.status(200).end() moved inside try/catch/finally logicsend-sms.jsor the/api/send-smsendpoint to record outbound messages and update their status based on the Status Webhook (/webhooks/status). Remember to handle potential database errors gracefully in all data access points.