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-sdk
Node.js library. - dotenv: A module to load environment variables from a
.env
file 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 OK
Prerequisites:
- 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.
mkdir vonage-sms-campaign cd vonage-sms-campaign
-
Initialize Node.js Project: This creates a
package.json
file to manage project dependencies and scripts.npm init -y
-
Install Dependencies: We need Express for the web server, the Vonage SDK to interact with the API, and
dotenv
for managing environment variables.npm install express @vonage/server-sdk dotenv
-
Install Development Dependencies:
nodemon
is helpful during development as it automatically restarts the server when code changes are detected.npm install --save-dev nodemon
-
Create Project Structure: Create the main application file and files for environment variables and Git ignore rules.
touch index.js .env .gitignore
(On Windows, you might need
type nul > .env
andtype nul > .gitignore
in 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.gitignore
file:# Dependencies node_modules/ # Environment variables .env # Vonage Private Key private.key *.key # Logs logs *.log # OS generated files .DS_Store Thumbs.db
-
Set up
.env
File: This file will store your credentials and configuration. Add the following lines, leaving the values blank for now. We'll populate them later.# 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..env
combined with.gitignore
ensures secrets aren't accidentally exposed.dotenv
loads these intoprocess.env
at runtime.
- Why
-
Add Development Script: Open
package.json
and add adev
script within the""scripts""
section to run the server usingnodemon
.{ ""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.0
with the actual current major versions if desired, or letnpm install
manage them. Using specific versions is generally recommended for stability.)Now you can run
npm run dev
to 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.js
and add the following initial code:// 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
dotenv
first. - We import necessary modules (
express
,Vonage
,path
). - A configuration check ensures critical environment variables are present.
path.resolve
is used to ensure theVONAGE_PRIVATE_KEY_PATH
works correctly regardless of where the script is run from.- We initialize the
Vonage
SDK 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
SIGINT
handler allows for graceful shutdown (Ctrl+C).
- We load
- Explanation:
-
Implement SMS Sending Function: Add a function within
index.js
to handle the logic of sending an SMS to a single recipient. Place this function definition before the routes section (e.g., beforeapp.get('/')
).// 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
recipient
number andmessageText
. - It uses
vonage.messages.send
with the required parameters:to
: Recipient number (should be E.164 format, e.g.,+15551234567
).from
: Your Vonage number from the.env
file.channel
: Specified assms
.message_type
: Set totext
.text
: The actual message content.
- It uses
async/await
for cleaner asynchronous code. - A
try...catch
block 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.js
before theapp.listen
call:// 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
POST
requests 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.allSettled
is used to initiate sending SMS to all recipients concurrently. This is more performant than sending sequentially.allSettled
waits for all promises to either resolve or reject, making it ideal for collecting results from multiple independent operations.- We iterate through the
results
array provided byPromise.allSettled
to categorize successful and failed sends based on thestatus
(fulfilled
orrejected
) and thevalue
returned 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.curl -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_KEY
andVONAGE_API_SECRET
fields in your.env
file. - Virtual Number: Navigate to Numbers -> Your numbers. Copy the full E.164 formatted number you purchased and paste it into
VONAGE_NUMBER
in your.env
file.
- 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.key
file will be downloaded automatically. Save this file securely within your project directory (e.g., in the root). UpdateVONAGE_PRIVATE_KEY_PATH
in your.env
file to point to its location (e.g.,./private.key
). - Copy the Application ID displayed on the page and paste it into
VONAGE_APPLICATION_ID
in your.env
file. - 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 upngrok
and 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
.env
file. - 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):ngrok
creates a secure tunnel from the public internet to your local machine.- Open a new terminal window (keep your server running in the other).
- Run
ngrok
to forward to the port your Express app is using (default is 3000):ngrok http 3000
ngrok
will 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_URL
variable in your.env
file. - 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
ngrok
URL (e.g.,https://<random-string>.ngrok-free.app/webhooks/inbound
andhttps://<random-string>.ngrok-free.app/webhooks/status
). Save the application settings. - Restart your Node.js server (
npm run dev
) after updating the.env
file so it picks up theNGROK_URL
.
(Note on Alternatives: While
ngrok
is excellent for development, alternatives likelocaltunnel
exist. 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 currentsendSingleSms
function already includes basic error catching. For production, consider:- Specific Error Codes: Check
error.response.status
orerror.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-retry
for implementing strategies like exponential backoff. (Keep it simple for this guide - logging is the priority.)
- Specific Error Codes: Check
-
Logging: We're using
console.log
andconsole.error
. For production:- Use a dedicated logging library: Like
winston
orpino
. 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.js
beforeapp.listen
. Note that these handlers currently only log the incoming data. TheTODO
comments 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.// 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
POST
handlers are created for the paths configured in the Vonage Application (/webhooks/status
and/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_uuid
to 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 OK
status 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_uuid
and delivery status received via webhooks for each message sent. - Manage Opt-Outs: Persistently store numbers that have opted out via
STOP
requests.
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 theContacts
table (respectingopted_in
status) and log sent messages to theMessages
table, storing themessage_uuid
. - Modify the webhook handlers (
/webhooks/status
,/webhooks/inbound
) to update theMessages
table (status) andContacts
table (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.