code examples
code examples
Build Production-Ready SMS Marketing Campaigns with Node.js, Express, and Vonage
A comprehensive guide to building an SMS marketing application using Node.js, Express, and the Vonage Messages API, covering setup, sending SMS, handling webhooks, and database considerations.
Directly reaching customers via SMS is a powerful marketing strategy. Building a reliable system to manage and send SMS campaigns requires careful planning, robust error handling, and secure integration with communication APIs.
This guide provides a comprehensive walkthrough for building a foundational SMS marketing campaign application using Node.js, the Express framework, and the Vonage Messages API. We will cover everything from initial project setup to deployment and monitoring, enabling you to send targeted SMS messages efficiently and track their status. Note that while this guide aims for a production-ready foundation, certain critical aspects like database integration and webhook security are outlined conceptually and require full implementation by the developer for a truly secure and robust production system.
Project Overview and Goals
What We're Building:
We will construct a Node.js application using the Express framework that exposes an API endpoint. This endpoint will accept a list of recipient phone numbers and a message text, then utilize the Vonage Messages API to send an SMS message to each recipient. The application will also include webhook endpoints to receive delivery status updates from Vonage.
Problem Solved:
This system provides a foundational backend for SMS marketing campaigns, enabling businesses to:
- Send bulk SMS messages programmatically via an API.
- Integrate SMS sending capabilities into larger marketing platforms.
- Receive delivery status updates for tracking campaign effectiveness.
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 build the API and webhook handlers.
- Vonage Messages API: A powerful API for sending and receiving messages across various channels, including SMS. We'll use the
@vonage/server-sdkNode.js library. - dotenv: A module to load environment variables from a
.envfile intoprocess.env, keeping sensitive credentials out of the codebase. - ngrok: A tool to expose local servers to the internet, crucial for testing Vonage webhooks during development.
System Architecture:
The basic flow involves these components:
- Client (e.g., Postman, curl, Frontend App): Initiates a request to the Express API endpoint to send a campaign.
- Express API Server:
- Receives the request.
- Validates input (recipients, message).
- Iterates through recipients and calls the Vonage SDK to send SMS.
- Responds to the client.
- Listens for incoming webhooks from Vonage (Inbound Messages, Status Updates).
- Vonage Node.js SDK: Interfaces with the Vonage Messages API.
- Vonage Platform:
- Receives API requests from the SDK to send SMS.
- Sends SMS messages via carrier networks.
- Sends status updates (e.g., delivered, failed) back to the configured Status Webhook URL.
- Forwards replies sent to the Vonage number to the configured Inbound Webhook URL.
- ngrok (Development Only): Tunnels requests from Vonage webhooks to the local Express server.
sequenceDiagram
participant Client
participant ExpressApp as Express API Server
participant VonageSDK as Vonage Node SDK
participant VonagePlatform as Vonage Platform
participant UserPhone as User's Phone
Client->>+ExpressApp: POST /api/campaigns/send (recipients, message)
ExpressApp->>+VonageSDK: vonage.messages.send({to, from, text, ...})
VonageSDK->>+VonagePlatform: Send SMS API Request
VonagePlatform-->>-VonageSDK: message_uuid
VonageSDK-->>-ExpressApp: Success/Error Response
ExpressApp-->>-Client: Campaign Sending Initiated
VonagePlatform->>+UserPhone: Sends SMS
UserPhone-->>-VonagePlatform: SMS Delivered (or Failed)
VonagePlatform->>+ExpressApp: POST /webhooks/status (delivery receipt)
Note over ExpressApp: Process DLR
ExpressApp-->>-VonagePlatform: 200 OK
UserPhone->>+VonagePlatform: Sends Reply SMS (e.g., `STOP`)
VonagePlatform->>+ExpressApp: POST /webhooks/inbound (incoming message)
Note over ExpressApp: Process Inbound Message (e.g., Opt-out)
ExpressApp-->>-VonagePlatform: 200 OKPrerequisites:
- Node.js and npm (or yarn): Installed on your system. Download Node.js
- Vonage Account: Sign up for free at Vonage API Dashboard.
- Vonage Phone Number: Purchase an SMS-capable number from your Vonage dashboard (Numbers -> Buy numbers).
- ngrok: Installed and authenticated. Download ngrok. A free account is sufficient for this guide.
- Text Editor: Such as VS Code, Sublime Text, or Atom.
- Terminal/Command Prompt: For running commands.
Expected Outcome:
By the end of this guide, you will have a functional Node.js application capable of:
- Accepting API requests to send SMS messages to multiple recipients.
- Sending SMS messages via the Vonage Messages API.
- Receiving and logging delivery status updates from Vonage.
- Basic setup for receiving inbound SMS replies.
1. Setting up the Project
Let's start by creating the project directory, initializing Node.js, and installing necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
bashmkdir vonage-sms-campaign cd vonage-sms-campaign -
Initialize Node.js Project: This creates a
package.jsonfile to manage project dependencies and scripts.bashnpm init -y -
Install Dependencies: We need Express for the web server, the Vonage SDK to interact with the API, and
dotenvfor managing environment variables.bashnpm install express @vonage/server-sdk dotenv -
Install Development Dependencies:
nodemonis helpful during development as it automatically restarts the server when code changes are detected.bashnpm install --save-dev nodemon -
Create Project Structure: Create the main application file and files for environment variables and Git ignore rules.
bashtouch index.js .env .gitignore(On Windows, you might need
type nul > .envandtype nul > .gitignorein Command Prompt, or equivalent commands in PowerShell.) -
Configure
.gitignore: Prevent sensitive information and unnecessary files from being committed to version control. Add the following lines to your.gitignorefile:text# Dependencies node_modules/ # Environment variables .env # Vonage Private Key private.key *.key # Logs logs *.log # OS generated files .DS_Store Thumbs.db -
Set up
.envFile: This file will store your credentials and configuration. Add the following lines, leaving the values blank for now. We'll populate them later.dotenv# Vonage Credentials & Settings (Using Messages API Application) 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 # Or the full path to your key file VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Server Configuration PORT=3000 # Required during development for Webhooks NGROK_URL=YOUR_NGROK_FORWARDING_URL- Why
.env? Storing credentials directly in code is insecure..envcombined with.gitignoreensures secrets aren't accidentally exposed.dotenvloads these intoprocess.envat runtime.
- Why
-
Add Development Script: Open
package.jsonand add adevscript within the""scripts""section to run the server usingnodemon.json{ ""name"": ""vonage-sms-campaign"", ""version"": ""1.0.0"", ""description"": """", ""main"": ""index.js"", ""scripts"": { ""start"": ""node index.js"", ""dev"": ""nodemon index.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""@vonage/server-sdk"": ""^3.0.0"", ""dotenv"": ""^16.0.0"", ""express"": ""^4.0.0"" }, ""devDependencies"": { ""nodemon"": ""^2.0.0"" } }(Note: Replace version numbers like
^3.0.0with the actual current major versions if desired, or letnpm installmanage them. Using specific versions is generally recommended for stability.)Now you can run
npm run devto start the server in development mode.
2. Implementing Core Functionality (Sending SMS)
Let's write the basic Express server setup and the core logic for sending SMS messages using the Vonage SDK.
-
Basic Server Setup (
index.js): Openindex.jsand add the following initial code:javascript// index.js 'use strict'; // Load environment variables from .env file require('dotenv').config(); const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const path = require('path'); // Needed for resolving the private key path // --- Configuration Check --- // Ensure essential environment variables are set const requiredEnv = [ 'VONAGE_API_KEY', 'VONAGE_API_SECRET', 'VONAGE_APPLICATION_ID', 'VONAGE_PRIVATE_KEY_PATH', 'VONAGE_NUMBER', 'PORT' ]; const missingEnv = requiredEnv.filter(envVar => !process.env[envVar]); if (missingEnv.length > 0) { console.error(`Error: Missing required environment variables: ${missingEnv.join(', ')}`); console.error('Please check your .env file or environment configuration.'); process.exit(1); // Exit if configuration is incomplete } // Resolve the private key path relative to the project root const privateKeyPath = path.resolve(process.env.VONAGE_PRIVATE_KEY_PATH); // --- Initialize Vonage SDK --- // Using Application ID and Private Key for Messages API authentication const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, // Still useful for account context apiSecret: process.env.VONAGE_API_SECRET, // Still useful for account context applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyPath // Use the resolved absolute path }, { // Optional: Add debug flag for verbose SDK logging // debug: true }); // --- Initialize Express App --- const app = express(); const port = process.env.PORT || 3000; // Middleware to parse JSON request bodies app.use(express.json()); // Middleware to parse URL-encoded request bodies (needed for webhooks) app.use(express.urlencoded({ extended: true })); // --- Basic Routes (Placeholder) --- app.get('/', (req, res) => { res.send('SMS Campaign Server is running!'); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening on port ${port}`); console.log(`Vonage Application ID: ${process.env.VONAGE_APPLICATION_ID}`); console.log(`Using Vonage Number: ${process.env.VONAGE_NUMBER}`); // Remind about ngrok during development if (process.env.NODE_ENV !== 'production' && process.env.NGROK_URL) { console.log(`Webhook Base URL (via ngrok): ${process.env.NGROK_URL}`); } else if (process.env.NODE_ENV !== 'production') { console.warn('NGROK_URL not set in .env. Webhooks will not work locally without ngrok.'); } }); // --- Graceful Shutdown --- process.on('SIGINT', () => { console.log('\nGracefully shutting down from SIGINT (Ctrl+C)'); // Perform cleanup here if necessary process.exit(0); });- Explanation:
- We load
dotenvfirst. - We import necessary modules (
express,Vonage,path). - A configuration check ensures critical environment variables are present.
path.resolveis used to ensure theVONAGE_PRIVATE_KEY_PATHworks correctly regardless of where the script is run from.- We initialize the
VonageSDK using the Application ID and Private Key, which is standard for the Messages API. We include API Key/Secret as they can sometimes be useful for other SDK functions or context. - We create an Express app instance and configure JSON and URL-encoded body parsers. The URL-encoded parser is often needed for incoming webhooks from services like Vonage.
- A simple root route
/is added. - The server starts listening on the configured port.
- Basic logging confirms the server is running and shows key configuration details.
- A
SIGINThandler allows for graceful shutdown (Ctrl+C).
- We load
- Explanation:
-
Implement SMS Sending Function: Add a function within
index.jsto handle the logic of sending an SMS to a single recipient. Place this function definition before the routes section (e.g., beforeapp.get('/')).javascript// index.js (add this function definition) /** * Sends a single SMS message using the Vonage Messages API. * @param {string} recipient - The recipient's phone number in E.164 format. * @param {string} messageText - The text content of the SMS. * @returns {Promise<object>} - A promise that resolves with the Vonage API response or rejects with an error. */ async function sendSingleSms(recipient, messageText) { console.log(`Attempting to send SMS to: ${recipient}`); try { const response = await vonage.messages.send({ to: recipient, from: process.env.VONAGE_NUMBER, // Your Vonage virtual number channel: 'sms', message_type: 'text', text: messageText, }); console.log(`SMS submitted to Vonage for ${recipient}. Message UUID: ${response.message_uuid}`); return { success: true, recipient: recipient, message_uuid: response.message_uuid }; } catch (error) { console.error(`Error sending SMS to ${recipient}:`, error.response ? error.response.data : error.message); // Provide more context from Vonage error if available let errorMessage = 'Failed to send SMS.'; if (error.response && error.response.data) { errorMessage = `Vonage Error: ${error.response.data.title || 'Unknown error'} - ${error.response.data.detail || error.message}`; } else { errorMessage = error.message; } return { success: false, recipient: recipient, error: errorMessage }; } }- Explanation:
- The function takes the
recipientnumber andmessageText. - It uses
vonage.messages.sendwith the required parameters:to: Recipient number (should be E.164 format, e.g.,+15551234567).from: Your Vonage number from the.envfile.channel: Specified assms.message_type: Set totext.text: The actual message content.
- It uses
async/awaitfor cleaner asynchronous code. - A
try...catchblock handles potential errors during the API call. - It logs success or failure and returns a structured result object. We try to extract meaningful error details from the Vonage response if available.
- The function takes the
- Explanation:
3. Building an API Layer for Campaigns
Now, let's create the API endpoint that will receive campaign requests and use our sendSingleSms function.
-
Define the Campaign Sending Endpoint: Add the following route handler in
index.jsbefore theapp.listencall:javascript// index.js (add this route handler) // --- API Endpoint for Sending Campaigns --- app.post('/api/campaigns/send', async (req, res) => { const { recipients, message } = req.body; // --- Basic Input Validation --- if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ status: 'error', message: 'Invalid or missing `recipients` array in request body.' }); } if (!message || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ status: 'error', message: 'Invalid or missing `message` string in request body.' }); } console.log(`Received campaign request: ${recipients.length} recipients, message: ""${message.substring(0, 50)}...""`); // --- Process Recipients Asynchronously --- // Use Promise.allSettled to send messages concurrently and collect all results const sendPromises = recipients.map(recipient => { // Basic check for non-empty string type. // IMPORTANT: Robust E.164 validation (see Section 7) is crucial for production. if (typeof recipient !== 'string' || recipient.trim() === '') { console.warn(`Skipping invalid recipient entry: ${recipient}`); return Promise.resolve({ success: false, recipient: recipient, error: 'Invalid recipient format (basic check)' }); } return sendSingleSms(recipient.trim(), message); }); const results = await Promise.allSettled(sendPromises); // --- Aggregate Results --- const successfulSends = []; const failedSends = []; results.forEach(result => { if (result.status === 'fulfilled') { if (result.value.success) { successfulSends.push(result.value); } else { failedSends.push(result.value); } } else { // This catches unexpected errors in the sendSingleSms promise itself console.error(""Unexpected error during SMS send operation:"", result.reason); // Attempt to associate with a recipient if possible, otherwise generic error // This part might need refinement depending on how errors are propagated failedSends.push({ success: false, recipient: 'unknown', error: 'Processing error: ' + result.reason?.message }); } }); console.log(`Campaign processing complete. Success: ${successfulSends.length}, Failed: ${failedSends.length}`); // --- Respond to Client --- res.status(202).json({ // 202 Accepted: Request received, processing initiated status: 'processing', message: `Campaign processing initiated for ${recipients.length} recipients.`, results: { successful_count: successfulSends.length, failed_count: failedSends.length, // Optionally include detailed results, but be mindful of response size for large campaigns // successful_sends: successfulSends, // failed_sends: failedSends } }); });- Explanation:
- The endpoint listens for
POSTrequests at/api/campaigns/send. - It expects a JSON body with
recipients(an array of phone numbers) andmessage(a string). - Basic validation checks if the required fields are present and have the correct types. A note emphasizes that more robust validation (like E.164 format checks shown later) is needed.
Promise.allSettledis used to initiate sending SMS to all recipients concurrently. This is more performant than sending sequentially.allSettledwaits for all promises to either resolve or reject, making it ideal for collecting results from multiple independent operations.- We iterate through the
resultsarray provided byPromise.allSettledto categorize successful and failed sends based on thestatus(fulfilledorrejected) and thevaluereturned bysendSingleSms. - A summary response is sent back to the client with HTTP status
202 Accepted, indicating the request was received and processing has started (as sending many SMS messages can take time). The response includes counts of successful and failed attempts.
- The endpoint listens for
- Explanation:
-
Testing with
curl: Once the server is running (npm run dev), you can test this endpoint from another terminal window. Replace placeholders with actual values.bashcurl -X POST http://localhost:3000/api/campaigns/send \ -H ""Content-Type: application/json"" \ -d '{ ""recipients"": [""+15551112222"", ""+15553334444""], ""message"": ""Hello from our Vonage SMS campaign!"" }'You should see output in your server logs indicating the request was received and attempts were made to send SMS messages. You should receive the SMS on the test phone numbers if they are valid and verified (if required by Vonage sandbox rules).
4. Integrating with Vonage (Configuration Details)
Correctly configuring your Vonage account and application is crucial for the Messages API.
-
Get Vonage Credentials:
- API Key & Secret: Log in to the Vonage API Dashboard. Your Key and Secret are displayed prominently on the overview page. Copy these into the
VONAGE_API_KEYandVONAGE_API_SECRETfields in your.envfile. - Virtual Number: Navigate to Numbers -> Your numbers. Copy the full E.164 formatted number you purchased and paste it into
VONAGE_NUMBERin your.envfile.
- API Key & Secret: Log in to the Vonage API Dashboard. Your Key and Secret are displayed prominently on the overview page. Copy these into the
-
Create a Vonage Application: The Messages API uses Applications for authentication and webhook configuration.
- Go to Applications -> Create a new application.
- Give it a descriptive Name (e.g.,
""My SMS Campaign App""). - Click Generate public and private key. A
private.keyfile will be downloaded automatically. Save this file securely within your project directory (e.g., in the root). UpdateVONAGE_PRIVATE_KEY_PATHin your.envfile to point to its location (e.g.,./private.key). - Copy the Application ID displayed on the page and paste it into
VONAGE_APPLICATION_IDin your.envfile. - Scroll down to Capabilities.
- Toggle Messages ON. This will reveal fields for webhook URLs.
- Inbound URL: This is where Vonage sends incoming SMS replies sent to your Vonage number. Enter
YOUR_NGROK_URL/webhooks/inbound. We will set upngrokand this route later. Set the method toPOST. - Status URL: This is where Vonage sends delivery status updates (DLRs) for messages you send. Enter
YOUR_NGROK_URL/webhooks/status. Set the method toPOST.
- Inbound URL: This is where Vonage sends incoming SMS replies sent to your Vonage number. Enter
- Scroll down to the bottom and click Link next to the Vonage virtual number you want to use for this application. Select the number you added to your
.envfile. - Click Generate new application.
-
Set Default SMS API (Important): Ensure your Vonage account is configured to use the Messages API for SMS by default, as webhook formats differ between the legacy SMS API and the Messages API.
- Go to your Vonage Dashboard Settings.
- Find the API settings section, then locate SMS settings.
- Ensure that Default SMS Setting is set to Messages API.
- Click Save changes.
-
Configure and Run
ngrok(Development):ngrokcreates a secure tunnel from the public internet to your local machine.- Open a new terminal window (keep your server running in the other).
- Run
ngrokto forward to the port your Express app is using (default is 3000):bashngrok http 3000 ngrokwill display forwarding URLs (http and https). Copy the https forwarding URL (e.g.,https://<random-string>.ngrok-free.app).- Paste this URL into the
NGROK_URLvariable in your.envfile. - Go back to your Vonage Application settings (Applications -> Your App Name -> Edit) and update the Inbound URL and Status URL fields to use this exact
ngrokURL (e.g.,https://<random-string>.ngrok-free.app/webhooks/inboundandhttps://<random-string>.ngrok-free.app/webhooks/status). Save the application settings. - Restart your Node.js server (
npm run dev) after updating the.envfile so it picks up theNGROK_URL.
(Note on Alternatives: While
ngrokis excellent for development, alternatives likelocaltunnelexist. For more persistent testing or specific cloud environments, you might deploy to a staging server or use cloud-platform-specific tunneling services.)
5. Implementing Error Handling, Logging, and Webhooks
Robust applications need proper error handling, informative logging, and the ability to receive status updates via webhooks.
-
Enhanced Error Handling (in
sendSingleSms): Our currentsendSingleSmsfunction already includes basic error catching. For production, consider:- Specific Error Codes: Check
error.response.statusorerror.response.data.type(if available from Vonage) to handle specific issues differently (e.g., insufficient funds, invalid number format). - Retry Logic (Application Level): While Vonage handles some network retries, you might want application-level retries for transient errors (like temporary rate limiting). Use libraries like
async-retryfor implementing strategies like exponential backoff. (Keep it simple for this guide - logging is the priority.)
- Specific Error Codes: Check
-
Logging: We're using
console.logandconsole.error. For production:- Use a dedicated logging library: Like
winstonorpino. They offer log levels (debug, info, warn, error), structured logging (JSON format), and transport options (log to files, external services). - Log Key Events:
- Server start/stop.
- Incoming API requests (
/api/campaigns/send) with masked/limited data. - Each SMS send attempt (success/failure) with
message_uuid. - Incoming webhook requests (
/webhooks/inbound,/webhooks/status) with payload summaries. - Configuration errors (missing env vars).
- Unexpected errors in any part of the application.
- Use a dedicated logging library: Like
-
Implement Webhook Handlers: Add these route handlers in
index.jsbeforeapp.listen. Note that these handlers currently only log the incoming data. TheTODOcomments indicate where essential business logic, such as updating a database (Section 6) or handling opt-outs (Section 8.2 - Note: Section 8.2 is mentioned but not present in the original text, implying it might be part of a larger context or planned section. We'll keep the reference as is.), must be implemented for a functional production system.javascript// index.js (add webhook handlers) // --- Webhook Endpoint for Delivery Receipts (Status Updates) --- app.post('/webhooks/status', (req, res) => { const statusData = req.body; console.log('--- Received Status Webhook ---'); console.log('Timestamp:', statusData.timestamp); console.log('Message UUID:', statusData.message_uuid); console.log('Status:', statusData.status); console.log('To:', statusData.to); if (statusData.error) { console.error('Error Code:', statusData.error.code); console.error('Error Reason:', statusData.error.reason); } console.log('-----------------------------'); // TODO: Update message status in your database using message_uuid (See Section 6) // Example: await updateMessageStatusInDB(statusData.message_uuid, statusData.status, statusData.timestamp, statusData.error); // Vonage expects a 200 OK response to acknowledge receipt *quickly* res.status(200).send('OK'); }); // --- Webhook Endpoint for Inbound SMS Messages --- app.post('/webhooks/inbound', (req, res) => { const inboundData = req.body; console.log('--- Received Inbound SMS ---'); console.log('Timestamp:', inboundData.timestamp); console.log('From:', inboundData.from.number); // Sender's number console.log('To:', inboundData.to.number); // Your Vonage number console.log('Message UUID:', inboundData.message_uuid); console.log('Text:', inboundData.message.content.text); console.log('--------------------------'); // TODO: Process inbound message (e.g., handle STOP keywords for opt-outs - See Section 8.2) // Example: await handleOptOut(inboundData.from.number, inboundData.message.content.text); // Acknowledge receipt *quickly* res.status(200).send('OK'); });- Explanation:
- Two
POSThandlers are created for the paths configured in the Vonage Application (/webhooks/statusand/webhooks/inbound). - They log the received request body (
req.body). The structure of this body is defined by the Vonage Messages API webhook format. - Status Webhook: Logs key information like
message_uuid,status(e.g.,delivered,failed,submitted,rejected), timestamp, recipient number, and error details if the status is failed. You would typically use themessage_uuidto find the corresponding message in your database and update its status. - Inbound Webhook: Logs the sender's number (
from.number), your Vonage number (to.number), the message content (message.content.text), and other metadata. This is where you would implement logic to handle replies, especially opt-out keywords like`STOP`. - Crucially, both handlers send back a
200 OKstatus immediately. If Vonage doesn't receive a 200 OK quickly, it will assume the webhook failed and may retry, leading to duplicate processing. Business logic (like database updates or opt-out processing) should ideally happen asynchronously after sending the 200 OK (e.g., using a job queue), or be very fast.
- Two
- Explanation:
6. Creating a Database Schema (Conceptual)
While this guide uses in-memory processing, a production system needs a database to persist data.
Why a Database?
- Store Contact Lists: Manage subscribers, including opt-in/opt-out status.
- Track Campaigns: Record details about each campaign sent.
- Log Message Status: Store the
message_uuidand delivery status received via webhooks for each message sent. - Manage Opt-Outs: Persistently store numbers that have opted out via
STOPrequests.
Conceptual Schema (using SQL-like syntax):
-- Contacts Table
CREATE TABLE Contacts (
contact_id SERIAL PRIMARY KEY, -- Or UUID
phone_number VARCHAR(20) UNIQUE NOT NULL, -- E.164 format
first_name VARCHAR(100),
last_name VARCHAR(100),
opted_in BOOLEAN DEFAULT TRUE,
opt_in_timestamp TIMESTAMPTZ,
opt_out_timestamp TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Campaigns Table
CREATE TABLE Campaigns (
campaign_id SERIAL PRIMARY KEY, -- Or UUID
campaign_name VARCHAR(255) NOT NULL,
message_text TEXT NOT NULL,
created_by VARCHAR(100), -- User who created it
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
scheduled_at TIMESTAMPTZ, -- For future scheduling
status VARCHAR(20) DEFAULT 'draft' -- e.g., draft, sending, completed, failed
);
-- Messages Table (Individual SMS logs)
CREATE TABLE Messages (
message_log_id SERIAL PRIMARY KEY, -- Or UUID
vonage_message_uuid VARCHAR(100) UNIQUE, -- Crucial link to Vonage status updates
campaign_id INT REFERENCES Campaigns(campaign_id),
contact_id INT REFERENCES Contacts(contact_id),
recipient_number VARCHAR(20) NOT NULL, -- Denormalized for easier lookup if contact is deleted
status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, rejected, accepted
status_timestamp TIMESTAMPTZ,
error_code VARCHAR(50),
error_reason TEXT,
sent_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
price DECIMAL(10, 5), -- Optional: Store cost per message segment
currency VARCHAR(3) -- Optional: Store currency
);
-- Optional: Index frequently queried columns
CREATE INDEX idx_messages_uuid ON Messages(vonage_message_uuid);
CREATE INDEX idx_messages_status ON Messages(status);
CREATE INDEX idx_contacts_opted_in ON Contacts(opted_in);
CREATE INDEX idx_contacts_phone ON Contacts(phone_number);Implementation Notes:
- Choose a database system (e.g., PostgreSQL, MySQL, MongoDB).
- Use an ORM (like Sequelize, TypeORM for SQL) or a database driver (like
pg,mysql2,mongodb) in your Node.js application to interact with the database. - Modify the API endpoint (
/api/campaigns/send) to fetch recipients from theContactstable (respectingopted_instatus) and log sent messages to theMessagestable, storing themessage_uuid. - Modify the webhook handlers (
/webhooks/status,/webhooks/inbound) to update theMessagestable (status) andContactstable (opt-out status) based on incoming data.
This database structure provides a solid foundation for tracking campaigns, managing contacts, and handling message statuses effectively in a production environment. Remember to implement proper indexing and connection management for performance.
Frequently Asked Questions
How to send bulk SMS messages using Node.js?
You can send bulk SMS messages by creating a Node.js application with Express.js that uses the Vonage Messages API. This involves setting up an API endpoint to handle recipient numbers and message text, then using the Vonage SDK to send messages to each recipient. The application should also include webhook endpoints to receive delivery status updates.
What is the Vonage Messages API used for?
The Vonage Messages API is a service that allows you to send and receive messages across multiple channels, including SMS. It's used in this project to programmatically send SMS messages as part of a marketing campaign. The guide uses the '@vonage/server-sdk' Node.js library to interact with this API.
Why use dotenv in a Node.js project?
Dotenv is used to securely manage environment variables, which helps keep sensitive information like API keys and secrets out of your codebase. It loads variables from a '.env' file into 'process.env', accessible within your application at runtime.
When should I use ngrok with Vonage?
Ngrok is primarily used during development to create a secure tunnel from the public internet to your local server, allowing Vonage webhooks to reach your application for testing purposes. For production, you would typically deploy your application to a publicly accessible server.
Can I track SMS delivery status with Vonage?
Yes, you can track delivery status using Vonage's webhooks. Set up a 'Status URL' in your Vonage application settings. Vonage will send delivery receipts to this URL, allowing you to monitor the success or failure of each message sent.
How to set up a Vonage Messages API application?
Create a new application in your Vonage dashboard, generate public and private keys (store the private key securely), and copy the Application ID. Enable the 'Messages' capability, configure Inbound and Status URLs for webhooks, link your Vonage virtual number, and generate the application.
What is the purpose of the private key in Vonage?
The private key is used for authentication with the Vonage Messages API and should be kept securely within your project. It ensures only authorized requests can be made using your Vonage account and application.
How to handle inbound SMS replies in my application?
Set up an 'Inbound URL' in your Vonage application settings and create a corresponding route handler in your Express app. Vonage will forward incoming SMS messages to this URL, allowing you to process replies from recipients.
What database schema is recommended for SMS campaigns?
The article suggests a schema with tables for Contacts (stores recipient information and opt-in/out status), Campaigns (stores details of each campaign), and Messages (logs individual SMS messages, including status and Vonage message UUID).
Why is a database important for production SMS campaigns?
A database provides persistent storage for contact lists, campaign details, and message delivery status. This information is essential for tracking campaign effectiveness, managing subscribers (including opt-outs), and maintaining data consistency.
How to integrate the Vonage SDK into a Node.js project?
Install the '@vonage/server-sdk' package using npm or yarn. Then, initialize the Vonage object with your API key, secret, Application ID, and the path to your private key file.
What is the role of Express.js in this project?
Express.js is a Node.js web application framework that's used to create the API endpoints for sending campaigns and receiving webhooks from Vonage. It handles routing, middleware, and request/response management.