How to Send SMS with Vonage Messages API in RedwoodJS (2025 Guide)
Step-by-step tutorial: Send SMS messages using Vonage Messages API with RedwoodJS v8.x and Node.js. Includes A2P 10DLC setup, authentication, error handling, and production deployment code examples.
Learn how to send SMS messages programmatically using the Vonage Messages API with RedwoodJS and Node.js. This comprehensive tutorial covers everything from initial setup to production deployment, including A2P 10DLC registration for US compliance.
What You'll Learn: Sending SMS with RedwoodJS and Vonage
This guide walks you through building a production-ready RedwoodJS application that sends SMS messages via the Vonage Messages API. Whether you need to send OTP codes, appointment reminders, order notifications, or marketing messages, you'll learn how to implement SMS functionality using RedwoodJS v8.x, Node.js 20/22 LTS, and the Vonage Server SDK with proper authentication, error handling, and security best practices.
Project Overview and Use Cases
What We'll Build:
A RedwoodJS serverless API function that accepts a phone number and message text, then sends an SMS using the Vonage Messages API.
Real-World Use Cases:
This SMS integration enables critical business communications including:
Two-Factor Authentication (2FA): Send one-time password codes for secure login
Appointment Reminders: Automated notifications for bookings and meetings
Order Status Updates: Keep customers informed about shipping and delivery
Alert Notifications: Time-sensitive alerts for account activity or system events
Marketing Campaigns: Promotional messages with proper opt-in consent
Technologies Used:
RedwoodJS: Full-stack JavaScript framework with integrated API and web layers, providing excellent developer experience and serverless-ready architecture
Node.js: JavaScript runtime (requires Node.js 20.x or 22.x LTS for RedwoodJS v8.x)
Vonage Messages API: Enterprise-grade messaging platform with multi-channel support (SMS, WhatsApp, MMS, Viber)
@vonage/server-sdk: Official Vonage Node.js SDK for seamless API integration
dotenv: Secure environment variable management for API credentials
Node.js: LTS version installed. Recommended: Node.js 20.x or 22.x (as of 2025). Node.js 18.x reaches end-of-life on April 30, 2025. Check your version with node -v.
Yarn: Package manager (Check with yarn -v). RedwoodJS uses Yarn by default.
RedwoodJS: This guide is compatible with RedwoodJS v8.x (latest as of 2025). RedwoodJS requires Node.js 20.x or higher.
Vonage API Account: Sign up at Vonage.com. You'll need your Application ID and Private Key.
A2P 10DLC Registration (Required for US Production SMS): For production A2P SMS messaging in the US, you must complete 10DLC Brand and Campaign registration through the Vonage Dashboard. This is mandatory for all A2P SMS traffic from US long code numbers as of 2025. Trial accounts may have limited sending capabilities until registration is complete.
Vonage Phone Number: Purchase or use an existing Vonage virtual number capable of sending SMS.
Test Phone Number: A mobile number where you can receive test SMS messages. (Note: If using a Vonage trial account, this number must be added to your account's whitelist).
RedwoodJS CLI: Install globally if you haven't already: npm install -g @redwoodjs/cli or yarn global add @redwoodjs/cli.
Estimated Setup Time: 30-45 minutes for complete implementation
Expected Outcome:
A functional RedwoodJS API endpoint (/api/sendSms) that, when called with a recipient number and message, sends an SMS via Vonage.
1. How to Set Up Your RedwoodJS SMS Project
Let's create a new RedwoodJS project and install the Vonage SDK dependencies.
Create RedwoodJS Project:
Open your terminal and run:
Bash
yarn create redwood-app ./vonage-sms-app
This command scaffolds a new RedwoodJS project in the vonage-sms-app directory.
Navigate to Project Directory:
Bash
cd vonage-sms-app
Install Vonage SDK Dependencies:
We need the Vonage Server SDK. RedwoodJS includes dotenv by default for environment variable management.
Bash
yarn workspace api add @vonage/server-sdk
Why yarn workspace api add? RedwoodJS uses Yarn workspaces. This command specifically adds the dependency to the api side of your project, where it will be used.
Configure Environment Variables (for Local Development):
Vonage requires credentials to authenticate API requests. We'll store these securely in an environment file for local development.
Create a .env file in the root of your project (vonage-sms-app/.env).
Add the following variables:
.env
# .env (For Local Development Only - Do Not Commit!)
VONAGE_APPLICATION_ID="YOUR_VONAGE_APP_ID"
VONAGE_PRIVATE_KEY_PATH="./private.key" # Relative path from project root
VONAGE_FROM_NUMBER="YOUR_VONAGE_PHONE_NUMBER" # In E.164 format, e.g., +15551234567
VONAGE_APPLICATION_ID: Find this on your Vonage Application's page in the Vonage API Dashboard.
VONAGE_PRIVATE_KEY_PATH: This is the path to the private key file you downloaded when creating your Vonage Application. We'll place this file in the project root shortly. This variable is primarily used for local development.
VONAGE_FROM_NUMBER: The Vonage virtual number you'll send SMS messages from. Ensure it's in E.164 format.
Add Private Key File (for Local Development):
Locate the private.key file you downloaded from the Vonage dashboard when creating your application. Copy this file into the root directory of your RedwoodJS project (vonage-sms-app/). The path in your .env file (./private.key) should now correctly point to it.
Configure .gitignore:
Ensure your .env file and private.key are never committed to version control. Open the .gitignore file in your project root and add/verify these lines:
Why? Committing secrets like API credentials or private keys is a major security risk.
2. Implementing the Vonage SMS Client in RedwoodJS
We'll create a reusable library function to handle the Vonage SDK initialization and the core SMS sending logic. This client will handle reading the private key from a file path (local dev) or a Base64 encoded environment variable (deployment).
Create Vonage Client Library:
Create a new file: api/src/lib/vonageClient.js
Implement the SMS Client:
Add the following code to api/src/lib/vonageClient.js:
JavaScript
// api/src/lib/vonageClient.jsimport { Vonage } from'@vonage/server-sdk'import { logger } from'src/lib/logger'import fs from'fs'// Import Node.js filesystem module to read the key locallyimport path from'path'// To resolve the key path correctly// Ensure required environment variables are setif (!process.env.VONAGE_APPLICATION_ID) {
thrownewError('VONAGE_APPLICATION_ID environment variable not set.')
}
if (!process.env.VONAGE_FROM_NUMBER) {
thrownewError('VONAGE_FROM_NUMBER environment variable not set.')
}
// Determine how to load the private key: Base64 env var (prod/deploy) or file path (local dev)let privateKeyValue
if (process.env.VONAGE_PRIVATE_KEY_BASE64) {
logger.debug('Loading Vonage private key from VONAGE_PRIVATE_KEY_BASE64 env var.')
try {
privateKeyValue = Buffer.from(
process.env.VONAGE_PRIVATE_KEY_BASE64,
'base64'
).toString('utf8')
} catch (error) {
logger.error(
{ error },
'Failed to decode VONAGE_PRIVATE_KEY_BASE64. Ensure it is a valid Base64 encoded string.'
)
thrownewError('Invalid Base64 private key configuration.')
}
} elseif (process.env.VONAGE_PRIVATE_KEY_PATH) {
logger.debug(
`Loading Vonage private key from path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`
)
try {
// Resolve path relative to the project root (where .env is usually located)// __dirname here refers to api/src/lib, so go up 3 levels for project rootconst keyPath = path.resolve(
__dirname,
'../../../', // Adjust if your lib folder structure changes
process.env.VONAGE_PRIVATE_KEY_PATH
)
privateKeyValue = fs.readFileSync(keyPath, 'utf8')
} catch (error) {
logger.error(
{ error, path: process.env.VONAGE_PRIVATE_KEY_PATH },
`Failed to read private key file. Ensure VONAGE_PRIVATE_KEY_PATH is correct relative to project root and file exists.`
)
thrownewError(
`Could not read Vonage private key file at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}`
)
}
} else {
thrownewError(
'Missing Vonage private key configuration. Set either VONAGE_PRIVATE_KEY_BASE64 (for deployment) or VONAGE_PRIVATE_KEY_PATH (for local dev) environment variable.'
)
}
// Initialize Vonage SDK// Note: The SDK expects the *content* of the private key.const vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: privateKeyValue, // Pass the key content here
})
/**
* Sends an SMS message using the Vonage Messages API.
* @param {string} to - The recipient phone number in E.164 format (e.g., +15551234567).
* @param {string} text - The message content.
* @returns {Promise<object>} - A promise that resolves with the Vonage API response on success.
* @throws {Error} - Throws an error if sending fails.
*/exportconstsendSms = async (to, text) => {
constfrom = process.env.VONAGE_FROM_NUMBER
logger.debug(`Attempting to send SMS from ${from} to ${to}`)
try {
const resp = await vonage.messages.send({
message_type: 'text', // Type of message being sentto: to, // Recipient numberfrom: from, // Sender number (your Vonage number)channel: 'sms', // Channel to send viatext: text, // The message body
})
logger.info({ messageUuid: resp.message_uuid, to }, 'SMS submitted successfully to Vonage')
return resp // Contains message_uuid on success
} catch (error) {
logger.error({ error, to, from }, 'Failed to send SMS via Vonage')
// Rethrow a more specific error or handle different Vonage errorsif (error.response && error.response.data) {
// Log detailed Vonage error if available
logger.error({ vonageError: error.response.data }, 'Vonage API error details');
thrownewError(`Vonage API Error: ${error.response.data.title || 'Unknown error'} - ${error.response.data.detail || error.message}`);
}
thrownewError(`Failed to send SMS: ${error.message}`)
}
}
Why Base64 or Path? This setup allows using a simple file path (VONAGE_PRIVATE_KEY_PATH) for local development (easier to manage) and a Base64 encoded string (VONAGE_PRIVATE_KEY_BASE64) for deployment environments where uploading files is difficult but setting environment variables (even long ones) is standard. Base64 encoding ensures the multi-line key content is safely stored in a single environment variable string.
Why path.resolve? Ensures the file path is correctly interpreted relative to the project root, regardless of where the script is run from within the project structure.
Why async/await? The vonage.messages.send method returns a Promise, making async/await a clean way to handle the asynchronous operation.
Why logger? RedwoodJS provides a built-in logger (pino). We use it to log information about the sending attempt and any errors, which is crucial for debugging.
Error Handling: The try...catch block handles potential errors during the API call. We log the error and rethrow it to be handled by the caller (the API function). We also attempt to log more specific details if Vonage provides them in the error response.
3. Building the RedwoodJS API Function for SMS
Now, let's create the RedwoodJS serverless function that will expose our SMS sending capability as an HTTP endpoint.
Generate RedwoodJS Function:
Use the RedwoodJS CLI to generate a new function:
Bash
yarn rw g function sendSms
This creates api/src/functions/sendSms.js and sets up the necessary routing.
Implement the API Handler:
Replace the contents of api/src/functions/sendSms.js with the following:
JavaScript
// api/src/functions/sendSms.jsimport { logger } from'src/lib/logger'import { sendSms as sendVonageSms } from'src/lib/vonageClient'// Renamed import/**
* The handler function is your serverless function's execution environment.
* RedwoodJS provides useful properties for handling request and response:
*
* @param {Object} event - Contains incoming request data (e.g., query params, headers, body)
* @param {Object} context - Contains additional context about the request (e.g., Lambda Function name)
* @returns {Object} - An object with statusCode, headers, and body properties for the response
*/exportconsthandler = async (event, context) => {
// Log the incoming event for debugging (optional, consider removing sensitive data in prod)// logger.debug({ event }, 'Received event in sendSms function')// Ensure it's a POST requestif (event.httpMethod !== 'POST') {
return {
statusCode: 405, // Method Not Allowedheaders: { 'Allow': 'POST', 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Method not allowed. Use POST.' }),
}
}
let requestBody
try {
requestBody = JSON.parse(event.body)
} catch (error) {
logger.error({ error, body: event.body }, 'Failed to parse request body')
return {
statusCode: 400, // Bad Requestheaders: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid JSON request body.' }),
}
}
const { to, message } = requestBody
// Basic Input Validationif (!to || typeof to !== 'string' || !to.trim()) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Missing or invalid \'to\' field (string, E.164 format required).' }),
}
}
// Basic E.164 format check (starts with '+', followed by digits)if (!/^\+\d+$/.test(to)) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Invalid \'to\' field format. Use E.164 format (e.g., +15551234567).' }),
}
}
if (!message || typeof message !== 'string' || !message.trim()) {
return {
statusCode: 400,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Missing or invalid \'message\' field (string required).' }),
}
}
logger.info(`Processing SMS request for recipient: ${to}`)
try {
const result = awaitsendVonageSms(to.trim(), message.trim()) // Use the imported function
logger.info({ messageUuid: result.message_uuid }, 'Successfully processed sendSms request')
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
message: 'SMS submitted successfully.',
messageUuid: result.message_uuid, // Include Vonage message ID in response
}),
}
} catch (error) {
logger.error({ error, recipient: to }, 'Error processing sendSms request')
// Determine appropriate status code based on error type if possibleconst statusCode = error.message.includes('Vonage API Error: Authentication failed') ? 401 : 500;
return {
statusCode: statusCode, // Internal Server Error or specific auth errorheaders: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: false,
error: `Failed to send SMS: ${error.message}`, // Provide error message
}),
}
}
}
Why check event.httpMethod? We ensure only POST requests are accepted for sending data.
Why JSON.parse(event.body)? The request body typically arrives as a JSON string; we need to parse it into a JavaScript object.
Why Input Validation? We check if to and message exist and are strings. We also add a basic check for the E.164 format on the to number. This prevents basic errors and improves security.
Why sendVonageSms? We call the reusable function created in the previous step, keeping the handler focused on request/response logic.
Response Structure: We return standardized JSON responses with appropriate HTTP status codes (200 for success, 400/405/500 for errors) and clear messages. Including the messageUuid from Vonage in the success response is helpful for tracking.
Test Your SMS Integration Locally:
Start the development server:
Bash
yarn rw dev
Open a separate terminal or use a tool like Postman or curl to send a POST request to the function's endpoint (usually http://localhost:8911/sendSms).
Using curl to Test SMS Sending:
Replace +1555YOURNUMBER with your test phone number. Remember, if you are using a Vonage trial account, this number must be whitelisted in your Vonage dashboard. Adjust the message as needed.
Bash
curl -X POST http://localhost:8911/sendSms \
-H "Content-Type: application/json" \
-d '{
"to": "+1555YOURNUMBER",
"message": "Hello from RedwoodJS and Vonage! (Local Test)"
}'
Either "Create a new application" or select your existing one.
Application ID: This is displayed prominently on your application's page. Use this value for VONAGE_APPLICATION_ID in your environment variables.
Private Key: If creating a new application, click "Generate public and private key". A private.key file will be downloaded. If you've lost your key for an existing application, you may need to generate a new public/private key pair (this will invalidate the old one) and download the new private.key. For local development, place this file in your project root as configured by VONAGE_PRIVATE_KEY_PATH. For deployment, you'll encode its content (see Deployment section).
Capabilities: Ensure the "Messages" capability is enabled for your application. You don't need to configure Inbound/Status URLs for sending SMS, only for receiving or getting delivery receipts.
Configure Vonage Phone Number (VONAGE_FROM_NUMBER):
Navigate to "Numbers" > "Your numbers".
Copy the Vonage virtual number you want to send messages from. Ensure it's listed with SMS capability.
Use this number (in E.164 format, e.g., +15551234567) for VONAGE_FROM_NUMBER in your environment variables.
Set Default SMS API Setting:
Navigate to "Settings".
Under "API settings" > "SMS settings", check the "Default SMS API" setting. While our code explicitly uses the Messages API SDK methods (vonage.messages.send), setting this default to Messages API aligns your account settings and is generally recommended by Vonage when primarily using the Messages API. Save changes if needed.
4.1. A2P 10DLC Registration Guide (Required for US Production SMS)
For production A2P SMS messaging in the US, you must complete 10DLC (10-Digit Long Code) registration. This is mandatory as of 2025 for all Application-to-Person SMS traffic from US long code numbers.
Why 10DLC Registration is Required:
Carrier Compliance: US mobile carriers require all A2P traffic to be registered to prevent spam and improve message deliverability.
Higher Throughput: Registered 10DLC numbers can send up to 30 messages per second (with higher rates available for verified brands), compared to severely throttled or blocked unregistered traffic.
Message Filtering: Unregistered A2P traffic may be filtered or blocked entirely by carriers.
Improved Deliverability: Registered 10DLC numbers are more likely to reach recipients successfully.
10DLC Registration Steps:
Brand Registration:
Navigate to "10DLC" or "Campaign Registry" in the Vonage Dashboard.
Register your business or organization as a Brand. You'll need:
Legal business name and type (LLC, Corporation, Non-Profit, etc.)
EIN (Employer Identification Number) or tax ID
Business address and contact information
Website URL (required for most business types)
Brand registration is processed through The Campaign Registry (TCR), a third-party company trusted by US carriers.
Brand Authentication+ (October 2024): New and existing public profit brands go through an enhanced verification process for improved trust scores.
Approval typically takes 1-2 business days.
Campaign Registration:
After Brand approval, register one or more Campaigns for your specific use cases.
Select a campaign use case that matches your messaging purpose:
Customer Care
Account Notifications
2FA (Two-Factor Authentication)
Marketing
Mixed/Other
Provide sample message content that accurately represents what you'll send.
Campaign registration typically takes 1-5 business days for approval.
Link Numbers to Campaign:
Once your Campaign is approved, link your US long code number(s) to the Campaign in the Vonage Dashboard.
Only linked numbers will benefit from approved throughput limits.
10DLC Throughput Limits (2025):
10DLC throughput varies based on your Brand trust score and Campaign type. Standard rates:
Unregistered Traffic: Severely throttled or blocked
Low Trust Score: 0.25-2 messages per second
Standard Brands: Up to 30 messages per second
Verified Brands (Brand Authentication+): Higher throughput available with additional verification
10DLC Fees (2025):
Brand Registration: One-time fee ($4) plus annual renewal ($1.50)
Campaign Registration: One-time fee (~$15 per campaign)
Monthly Pass-Through Fees: Carrier fees ($1.50-$8.00/month per number, varies by carrier)
Unregistered Traffic: $0.004 per SMS surcharge
Compliance Guidelines:
Obtain proper opt-in consent before sending messages
Include opt-out instructions (e.g., "Reply STOP to unsubscribe")
Use your registered Brand name in messages
Send only message content that matches your registered Campaign use case
Follow CTIA Messaging Principles and Best Practices
Comply with TCPA (Telephone Consumer Protection Act) regulations
5. Error Handling and Logging Best Practices for SMS
We've already built-in basic error handling and logging.
Error Handling Strategy: The vonageClient.js catches errors from the SDK, logs them with details, and throws a new, potentially more informative error. The API handler (sendSms.js) catches errors from the client function, logs them, and returns an appropriate HTTP error response (e.g., 500, 401) with the error message.
Logging: RedwoodJS's logger (pino) is used in both the client library and the API handler to log informational messages (like successful submission, including messageUuid) and errors (including recipient number and error details). Check the console where yarn rw dev is running. In production, configure log draining to a service like Papertrail, Datadog, or CloudWatch.
Common Vonage API Errors to Handle:
Authentication failed: Check VONAGE_APPLICATION_ID and the private key content (from VONAGE_PRIVATE_KEY_BASE64 or VONAGE_PRIVATE_KEY_PATH).
Non-Whitelisted Destination: If using a trial account, ensure the to number is added to the allowed list in the Vonage dashboard ("Sandbox & Test Numbers").
Invalid Parameters: Check to format (E.164), from format, ensure message is not empty.
Insufficient funds: Your Vonage account needs credit.
Invalid sender address: The from number might be incorrect or not SMS-enabled.
Throughput capacity exceeded: Sending too many messages too quickly (consider queues/throttling for high volume).
10DLC Registration Required (2025): For US long code numbers, ensure 10DLC Brand and Campaign registration is complete. Unregistered numbers may experience blocked or filtered messages.
Campaign Suspended (2025): Your 10DLC campaign may have been suspended due to compliance violations. Check the Vonage Dashboard and TCR portal for details.
Throughput Limit Exceeded (10DLC): Sending faster than your approved 10DLC throughput limit. Implement rate limiting based on your campaign's approved messages per second.
Retry Mechanisms:
Network Issues: For transient network errors between your server and Vonage, a simple retry strategy can help. You could wrap the vonage.messages.send call in a library like async-retry.
JavaScript
// Example using async-retry (install with: yarn workspace api add async-retry)// --- In api/src/lib/vonageClient.js ---// import retry from 'async-retry';//// // Replace the existing try...catch block inside sendSms with this:// try {// const resp = await retry(async (bail, attempt) => {// logger.debug(`Attempt ${attempt} to send SMS via Vonage...`);// try {// return await vonage.messages.send({// message_type: 'text',// to: to,// from: from,// channel: 'sms',// text: text,// });// } catch (error) {// // Don't retry on certain errors (e.g., authentication, bad request)// if (error.response && (error.response.status === 401 || error.response.status === 400)) {// logger.warn({ status: error.response.status }, 'Non-retriable error received from Vonage. Bailing.');// bail(error); // Stop retrying// return; // Needed because bail doesn't return// }// // For other errors (like network issues, 5xx), throw to trigger retry// logger.warn({ error }, `Retriable error encountered on attempt ${attempt}.`);// throw error;// }// }, {// retries: 3, // Number of retries (total attempts = retries + 1)// minTimeout: 500, // Initial delay in ms before first retry// factor: 2, // Exponential backoff factor (delay = minTimeout * factor^(attempt-1))// onRetry: (error, attempt) => {// logger.warn(`Retrying Vonage API call (attempt ${attempt}) due to error: ${error.message}`);// }// });//// logger.info({ messageUuid: resp.message_uuid, to }, 'SMS submitted successfully to Vonage after retries');// return resp;// } catch (error) {// logger.error({ error, to, from }, 'Failed to send SMS via Vonage after all retries');// // Rethrow specific error details if available// if (error.response && error.response.data) {// logger.error({ vonageError: error.response.data }, 'Vonage API error details');// throw new Error(`Vonage API Error: ${error.response.data.title || 'Unknown error'} - ${error.response.data.detail || error.message}`);// }// throw new Error(`Failed to send SMS: ${error.message}`);// }
Vonage Retries: Vonage itself may retry sending the message to the carrier if initial attempts fail. This is separate from retrying the API call from your application.
Recommendation: For this basic guide, focus on robust error logging. Implement application-level retries only if you specifically encounter frequent transient network errors or 5xx responses from Vonage. Over-retrying can sometimes worsen issues (e.g., hitting rate limits).
6. Database Integration for SMS Tracking (Optional)
For the core functionality of sending an SMS, no database schema is strictly required by this application itself.
However, in a real-world scenario, you might want to store:
A history of sent messages (recipient, message content, timestamp, Vonage messageUuid).
The status of the message (e.g., submitted, delivered, failed - requires setting up Delivery Receipts, see Vonage docs).
If needed, you would use RedwoodJS's Prisma integration:
Define Schema: Add a model to api/db/schema.prisma:
Prisma
// api/db/schema.prisma
model SmsLog {
id String @id @default(cuid())
recipient String
message String
status String @default("submitted") // e.g., submitted, delivered, failed
vonageUuid String? @unique // Store the Vonage message ID
sentAt DateTime @default(now())
// Add relation to User if applicable
// userId String?
// user User? @relation(fields: [userId], references: [id])
}
Run Migrations:
Bash
yarn rw prisma migrate dev
Log to Database: Modify the sendSms API handler to create an SmsLog entry after successfully submitting to Vonage:
JavaScript
// api/src/functions/sendSms.jsimport { db } from'src/lib/db'// Import Prisma client// ... inside handler, within the successful try block ...try {
const result = awaitsendVonageSms(to.trim(), message.trim())
// --- Log to database ---// Using .catch() makes this 'fire-and-forget'. The function won't wait// for the DB write to complete before responding. This improves response time// slightly but risks losing the log if the function terminates unexpectedly.
db.smsLog.create({
data: {
recipient: to.trim(),
message: message.trim(),
vonageUuid: result.message_uuid,
status: 'submitted', // Initial status
}
}).catch(dbError => {
// Log the DB error, but don't fail the overall SMS request
logger.error({ error: dbError }, "Failed to save SMS log to database");
});
// If reliability of logging is critical, use 'await' instead:// try {// await db.smsLog.create({ data: { ... } });// } catch (dbError) {// logger.error({ error: dbError }, "Failed to save SMS log to database");// // Decide if this should cause the main function to return an error// }// --- End log to database ---
logger.info({ messageUuid: result.message_uuid }, 'Successfully processed sendSms request')
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
success: true,
message: 'SMS submitted successfully.',
messageUuid: result.message_uuid,
}),
}
} catch (error) { /* ... existing error handling ... */ }
Trade-off: Using await db.smsLog.create(...) ensures the log is successfully written before the 200 OK response is sent, making logging more reliable. The "fire-and-forget" approach (db.smsLog.create(...).catch(...)) is slightly faster for the user but carries a small risk of the log write failing silently if the serverless function terminates quickly. Choose based on your application's reliability requirements.
This database integration is optional for basic sending but recommended for tracking and auditing.
7. Security Best Practices for SMS APIs
Security is paramount when dealing with APIs and user data.
Input Validation & Sanitization:
We implemented basic validation in the API handler (sendSms.js) for to (format) and message (existence).
Further Validation: Consider using a library like libphonenumber-js for more robust E.164 validation if needed: yarn workspace api add libphonenumber-js.
Sanitization: While not strictly necessary for the SMS content itself (Vonage handles encoding), ensure any data interpolated into the message from user input is sanitized to prevent injection if messages were constructed dynamically based on other inputs. RedwoodJS typically doesn't auto-escape in the API layer like it might in the web layer.
Credential Security:
NEVER commit .env or private.key to Git (use .gitignore).
Use environment variables provided by your deployment host for production secrets (like VONAGE_PRIVATE_KEY_BASE64). Do not store production secrets in .env files within the deployed application code.
Rate Limiting:
Prevent abuse by limiting how many requests a single user or IP can make in a given time frame.
RedwoodJS doesn't have built-in API rate limiting. Implement it using:
API Gateway: Most deployment platforms (Vercel, Netlify, AWS API Gateway) offer rate limiting configurations. This is often the easiest approach.
Middleware: Add custom middleware using libraries like rate-limiter-flexible with a store like Redis (requires adding Redis to your stack).
Authentication/Authorization (for the API Endpoint):
Our current endpoint is open. In a real application, you'd protect it using RedwoodJS's built-in auth (yarn rw setup auth <provider>) or other mechanisms (e.g., API keys for server-to-server communication). Ensure only authorized users/services can trigger SMS sending.
Least Privilege (Vonage API Key - Not Used Here, but Relevant):
If using Vonage API Key/Secret for other APIs, generate keys with the minimum required permissions. Our current method uses Application ID/Private Key which is scoped to the application's capabilities.
8. Handling Special Cases and International SMS
E.164 Format: Always ensure the to number is in E.164 format (+ followed by country code and number, no spaces or dashes, e.g., +14155552671, +442071838750). Our basic validation checks for the starting +. Use libphonenumber-js for robust parsing/validation if needed.
Character Limits & Encoding:
Standard SMS messages are limited (160 chars for GSM-7 encoding, 70 for UCS-2 if non-standard characters are used).
Vonage handles concatenation for longer messages (sending them as multiple segments), but this increases cost. Be mindful of message length.
Inform users about potential costs associated with long messages if applicable.
Sender ID (from Number):
Using your purchased Vonage number is the most reliable method globally, especially required in countries like the US & Canada.
Alphanumeric Sender IDs (e.g., "MyBrand") are supported in some countries but may have restrictions and are often for one-way messaging. Check Vonage documentation for country-specific regulations. Our setup uses the VONAGE_FROM_NUMBER.
International Sending: Vonage supports international SMS, but ensure your account is enabled for the destination countries and be aware of varying costs and regulations.
Opt-Out Handling: Implement mechanisms for users to opt-out of receiving SMS (e.g., replying "STOP"). This often requires setting up inbound message webhooks (outside the scope of this guide) to process replies. Comply with regulations like TCPA (US).
9. Performance Optimizations
For sending individual or low-volume SMS messages triggered by user actions or specific events, the performance of the current setup (direct API call within the serverless function) is generally sufficient. Response times will depend on the Vonage API latency.
Asynchronous Processing: If the API endpoint needs to respond very quickly without waiting for Vonage's confirmation, or if you need to send bulk messages, consider moving the sendVonageSms call to a background job queue (e.g., using services like AWS SQS, Google Cloud Tasks, or libraries like BullMQ with Redis). The API handler would just enqueue the job and return immediately.
SDK Initialization: The Vonage SDK is initialized once per function invocation (cold start) or reused on warm starts. This overhead is typically minimal for serverless functions.
Bulk Sending: The Vonage Messages API doesn't have a specific "bulk send" endpoint like some older APIs might. Sending multiple messages involves making multiple calls to the send endpoint. If sending high volumes, implement throttling and potentially use a job queue to manage the rate of API calls to avoid hitting Vonage throughput limits.
For this guide's scope, direct API calls are appropriate. Evaluate the need for queues based on your specific application's volume and latency requirements.
Set up a new RedwoodJS project, install the Vonage Server SDK, configure environment variables with your Vonage credentials, create a Vonage client library to handle SDK initialization and SMS sending logic, and create a RedwoodJS API function to expose the SMS sending functionality over HTTP. Test the setup by sending a POST request to the function's endpoint with the recipient's number and message. You'll need a Vonage API account and virtual number for sending SMS messages from your application via the Vonage Messages API.
The Vonage Messages API allows developers to send messages across various channels, including SMS, from their applications. It offers reliability, scalability, and a user-friendly SDK. In this RedwoodJS guide, it's used to send SMS notifications, alerts, or general messages programmatically. You'll need Vonage API credentials and an SMS-enabled Vonage Virtual Number.
Vonage is chosen for its reliability, scalability, and easy-to-use Node.js SDK. The Vonage Messages API provides a powerful and flexible way to integrate SMS messaging into Node.js and RedwoodJS applications, simplifying the process of sending alerts, notifications, and other text messages. The provided guide uses the official Vonage Node.js SDK @vonage/server-sdk.
Consider a job queue for high-volume SMS sending or when your API endpoint needs to respond very quickly without waiting for Vonage's confirmation. For low-volume or individual messages triggered by user actions, a direct API call within the serverless function is usually sufficient, whereas a job queue becomes more beneficial for bulk messages or near real-time API response requirements. The provided example implements retries for transient errors, which is sometimes sufficient without introducing a queue.
Vonage supports international SMS. Ensure your account is enabled for the destination countries and be aware of international messaging costs and specific country regulations. The "to" number must be in E.164 format, which starts with '+' followed by country code and number. The provided implementation does basic E.164 format validation, but third party libraries are available for more robust validation if required.
Never commit your .env file or private.key to version control. Use environment variables provided by your deployment platform for production credentials. The Vonage guide provides a comprehensive .gitignore setup to prevent accidental commits of these sensitive files. For local development, a private.key file is loaded, and for deployment, a VONAGE_PRIVATE_KEY_BASE64 environment variable is used.
VONAGE_PRIVATE_KEY_PATH specifies the path to your Vonage private key file for local development. It's used by the Vonage client library to initialize the Vonage SDK. The path is resolved using path.resolve ensuring the key is found, even if called from different locations within the project structure. Ensure this file is not committed to version control.
The provided example implements error handling at both the client and API handler level. The vonageClient.js catches errors from the SDK, logs them with specific details from Vonage when available, and rethrows them up to the API handler. The sendSms.js API handler then catches and returns an appropriate HTTP error response, enabling more robust error reporting and improved debugging.
The sendSms function is a RedwoodJS API handler responsible for receiving HTTP requests to send SMS messages. It parses the request body, validates the input, calls the Vonage client library to send the SMS, and returns a JSON response indicating success or failure, including logging for debugging. The provided implementation includes validation for to and message parameters, specifically checking for the presence of the + at the start of the to number. Further validation of the number could be performed if necessary.
Vonage automatically concatenates long messages into multiple segments, but be mindful of character limits and associated costs. Standard SMS messages are limited to 160 characters for GSM-7 encoding or 70 for UCS-2, with longer messages split into segments incurring additional costs. It is advisable to inform the users about the potential costs associated with sending longer messages.
RedwoodJS uses Yarn workspaces to manage dependencies. yarn workspace api add @vonage/server-sdk installs the Vonage SDK specifically for the API side of your project, where it's needed. This keeps the web side lean and improves dependency management.
The RedwoodJS logger (pino) logs informational messages, including the Vonage message ID (messageUuid), and any errors encountered during the SMS sending process. This information is crucial for debugging and monitoring. In production, configure log draining to a service like Papertrail, Datadog, or CloudWatch.
Transient network errors can be handled using a retry mechanism. The provided Vonage and RedwoodJS example demonstrates how to integrate an external retry library like async-retry around the Vonage messages.send call, ensuring messages are sent even if there are transient network problems. Vonage may also implement their own internal retries when communicating with carriers.
The RedwoodJS Prisma schema can be extended to include SmsLog which can store a history of sent SMS messages, including recipient, message content, status, timestamp, and Vonage ID. The provided example demonstrates how to insert data into this schema after a message has been successfully submitted to Vonage. Two implementations are provided: one waits for the database write to complete, whereas the other does not. You'll need to consider which is most suitable for your requirements.
Learn how to send SMS messages programmatically using the Vonage Messages API with RedwoodJS and Node.js. This comprehensive tutorial covers everything from initial setup to production deployment, including A2P 10DLC registration for US compliance.
What You'll Learn: Sending SMS with RedwoodJS and Vonage
This guide walks you through building a production-ready RedwoodJS application that sends SMS messages via the Vonage Messages API. Whether you need to send OTP codes, appointment reminders, order notifications, or marketing messages, you'll learn how to implement SMS functionality using RedwoodJS v8.x, Node.js 20/22 LTS, and the Vonage Server SDK with proper authentication, error handling, and security best practices.
Project Overview and Use Cases
What We'll Build: A RedwoodJS serverless API function that accepts a phone number and message text, then sends an SMS using the Vonage Messages API.
Real-World Use Cases: This SMS integration enables critical business communications including:
Technologies Used:
@vonage/server-sdk: Official Vonage Node.js SDK for seamless API integrationSystem Architecture:
Prerequisites:
node -v.yarn -v). RedwoodJS uses Yarn by default.npm install -g @redwoodjs/clioryarn global add @redwoodjs/cli.Expected Outcome: A functional RedwoodJS API endpoint (
/api/sendSms) that, when called with a recipient number and message, sends an SMS via Vonage.1. How to Set Up Your RedwoodJS SMS Project
Let's create a new RedwoodJS project and install the Vonage SDK dependencies.
Create RedwoodJS Project: Open your terminal and run:
This command scaffolds a new RedwoodJS project in the
vonage-sms-appdirectory.Navigate to Project Directory:
cd vonage-sms-appInstall Vonage SDK Dependencies: We need the Vonage Server SDK. RedwoodJS includes
dotenvby default for environment variable management.Why
yarn workspace api add? RedwoodJS uses Yarn workspaces. This command specifically adds the dependency to theapiside of your project, where it will be used.Configure Environment Variables (for Local Development): Vonage requires credentials to authenticate API requests. We'll store these securely in an environment file for local development.
.envfile in the root of your project (vonage-sms-app/.env).VONAGE_APPLICATION_ID: Find this on your Vonage Application's page in the Vonage API Dashboard.VONAGE_PRIVATE_KEY_PATH: This is the path to the private key file you downloaded when creating your Vonage Application. We'll place this file in the project root shortly. This variable is primarily used for local development.VONAGE_FROM_NUMBER: The Vonage virtual number you'll send SMS messages from. Ensure it's in E.164 format.Add Private Key File (for Local Development): Locate the
private.keyfile you downloaded from the Vonage dashboard when creating your application. Copy this file into the root directory of your RedwoodJS project (vonage-sms-app/). The path in your.envfile (./private.key) should now correctly point to it.Configure
.gitignore: Ensure your.envfile andprivate.keyare never committed to version control. Open the.gitignorefile in your project root and add/verify these lines:Why? Committing secrets like API credentials or private keys is a major security risk.
2. Implementing the Vonage SMS Client in RedwoodJS
We'll create a reusable library function to handle the Vonage SDK initialization and the core SMS sending logic. This client will handle reading the private key from a file path (local dev) or a Base64 encoded environment variable (deployment).
Create Vonage Client Library: Create a new file:
api/src/lib/vonageClient.jsImplement the SMS Client: Add the following code to
api/src/lib/vonageClient.js:// api/src/lib/vonageClient.js import { Vonage } from '@vonage/server-sdk' import { logger } from 'src/lib/logger' import fs from 'fs' // Import Node.js filesystem module to read the key locally import path from 'path' // To resolve the key path correctly // Ensure required environment variables are set if (!process.env.VONAGE_APPLICATION_ID) { throw new Error('VONAGE_APPLICATION_ID environment variable not set.') } if (!process.env.VONAGE_FROM_NUMBER) { throw new Error('VONAGE_FROM_NUMBER environment variable not set.') } // Determine how to load the private key: Base64 env var (prod/deploy) or file path (local dev) let privateKeyValue if (process.env.VONAGE_PRIVATE_KEY_BASE64) { logger.debug('Loading Vonage private key from VONAGE_PRIVATE_KEY_BASE64 env var.') try { privateKeyValue = Buffer.from( process.env.VONAGE_PRIVATE_KEY_BASE64, 'base64' ).toString('utf8') } catch (error) { logger.error( { error }, 'Failed to decode VONAGE_PRIVATE_KEY_BASE64. Ensure it is a valid Base64 encoded string.' ) throw new Error('Invalid Base64 private key configuration.') } } else if (process.env.VONAGE_PRIVATE_KEY_PATH) { logger.debug( `Loading Vonage private key from path: ${process.env.VONAGE_PRIVATE_KEY_PATH}` ) try { // Resolve path relative to the project root (where .env is usually located) // __dirname here refers to api/src/lib, so go up 3 levels for project root const keyPath = path.resolve( __dirname, '../../../', // Adjust if your lib folder structure changes process.env.VONAGE_PRIVATE_KEY_PATH ) privateKeyValue = fs.readFileSync(keyPath, 'utf8') } catch (error) { logger.error( { error, path: process.env.VONAGE_PRIVATE_KEY_PATH }, `Failed to read private key file. Ensure VONAGE_PRIVATE_KEY_PATH is correct relative to project root and file exists.` ) throw new Error( `Could not read Vonage private key file at path: ${process.env.VONAGE_PRIVATE_KEY_PATH}` ) } } else { throw new Error( 'Missing Vonage private key configuration. Set either VONAGE_PRIVATE_KEY_BASE64 (for deployment) or VONAGE_PRIVATE_KEY_PATH (for local dev) environment variable.' ) } // Initialize Vonage SDK // Note: The SDK expects the *content* of the private key. const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKeyValue, // Pass the key content here }) /** * Sends an SMS message using the Vonage Messages API. * @param {string} to - The recipient phone number in E.164 format (e.g., +15551234567). * @param {string} text - The message content. * @returns {Promise<object>} - A promise that resolves with the Vonage API response on success. * @throws {Error} - Throws an error if sending fails. */ export const sendSms = async (to, text) => { const from = process.env.VONAGE_FROM_NUMBER logger.debug(`Attempting to send SMS from ${from} to ${to}`) try { const resp = await vonage.messages.send({ message_type: 'text', // Type of message being sent to: to, // Recipient number from: from, // Sender number (your Vonage number) channel: 'sms', // Channel to send via text: text, // The message body }) logger.info({ messageUuid: resp.message_uuid, to }, 'SMS submitted successfully to Vonage') return resp // Contains message_uuid on success } catch (error) { logger.error({ error, to, from }, 'Failed to send SMS via Vonage') // Rethrow a more specific error or handle different Vonage errors if (error.response && error.response.data) { // Log detailed Vonage error if available logger.error({ vonageError: error.response.data }, 'Vonage API error details'); throw new Error(`Vonage API Error: ${error.response.data.title || 'Unknown error'} - ${error.response.data.detail || error.message}`); } throw new Error(`Failed to send SMS: ${error.message}`) } }VONAGE_PRIVATE_KEY_PATH) for local development (easier to manage) and a Base64 encoded string (VONAGE_PRIVATE_KEY_BASE64) for deployment environments where uploading files is difficult but setting environment variables (even long ones) is standard. Base64 encoding ensures the multi-line key content is safely stored in a single environment variable string.path.resolve? Ensures the file path is correctly interpreted relative to the project root, regardless of where the script is run from within the project structure.async/await? Thevonage.messages.sendmethod returns a Promise, makingasync/awaita clean way to handle the asynchronous operation.logger? RedwoodJS provides a built-in logger (pino). We use it to log information about the sending attempt and any errors, which is crucial for debugging.try...catchblock handles potential errors during the API call. We log the error and rethrow it to be handled by the caller (the API function). We also attempt to log more specific details if Vonage provides them in the error response.3. Building the RedwoodJS API Function for SMS
Now, let's create the RedwoodJS serverless function that will expose our SMS sending capability as an HTTP endpoint.
Generate RedwoodJS Function: Use the RedwoodJS CLI to generate a new function:
yarn rw g function sendSmsThis creates
api/src/functions/sendSms.jsand sets up the necessary routing.Implement the API Handler: Replace the contents of
api/src/functions/sendSms.jswith the following:// api/src/functions/sendSms.js import { logger } from 'src/lib/logger' import { sendSms as sendVonageSms } from 'src/lib/vonageClient' // Renamed import /** * The handler function is your serverless function's execution environment. * RedwoodJS provides useful properties for handling request and response: * * @param {Object} event - Contains incoming request data (e.g., query params, headers, body) * @param {Object} context - Contains additional context about the request (e.g., Lambda Function name) * @returns {Object} - An object with statusCode, headers, and body properties for the response */ export const handler = async (event, context) => { // Log the incoming event for debugging (optional, consider removing sensitive data in prod) // logger.debug({ event }, 'Received event in sendSms function') // Ensure it's a POST request if (event.httpMethod !== 'POST') { return { statusCode: 405, // Method Not Allowed headers: { 'Allow': 'POST', 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Method not allowed. Use POST.' }), } } let requestBody try { requestBody = JSON.parse(event.body) } catch (error) { logger.error({ error, body: event.body }, 'Failed to parse request body') return { statusCode: 400, // Bad Request headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Invalid JSON request body.' }), } } const { to, message } = requestBody // Basic Input Validation if (!to || typeof to !== 'string' || !to.trim()) { return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Missing or invalid \'to\' field (string, E.164 format required).' }), } } // Basic E.164 format check (starts with '+', followed by digits) if (!/^\+\d+$/.test(to)) { return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Invalid \'to\' field format. Use E.164 format (e.g., +15551234567).' }), } } if (!message || typeof message !== 'string' || !message.trim()) { return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Missing or invalid \'message\' field (string required).' }), } } logger.info(`Processing SMS request for recipient: ${to}`) try { const result = await sendVonageSms(to.trim(), message.trim()) // Use the imported function logger.info({ messageUuid: result.message_uuid }, 'Successfully processed sendSms request') return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, message: 'SMS submitted successfully.', messageUuid: result.message_uuid, // Include Vonage message ID in response }), } } catch (error) { logger.error({ error, recipient: to }, 'Error processing sendSms request') // Determine appropriate status code based on error type if possible const statusCode = error.message.includes('Vonage API Error: Authentication failed') ? 401 : 500; return { statusCode: statusCode, // Internal Server Error or specific auth error headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: false, error: `Failed to send SMS: ${error.message}`, // Provide error message }), } } }event.httpMethod? We ensure only POST requests are accepted for sending data.JSON.parse(event.body)? The request body typically arrives as a JSON string; we need to parse it into a JavaScript object.toandmessageexist and are strings. We also add a basic check for the E.164 format on thetonumber. This prevents basic errors and improves security.sendVonageSms? We call the reusable function created in the previous step, keeping the handler focused on request/response logic.messageUuidfrom Vonage in the success response is helpful for tracking.Test Your SMS Integration Locally: Start the development server:
Open a separate terminal or use a tool like Postman or
curlto send a POST request to the function's endpoint (usuallyhttp://localhost:8911/sendSms).Using
curlto Test SMS Sending: Replace+1555YOURNUMBERwith your test phone number. Remember, if you are using a Vonage trial account, this number must be whitelisted in your Vonage dashboard. Adjust the message as needed.curl -X POST http://localhost:8911/sendSms \ -H "Content-Type: application/json" \ -d '{ "to": "+1555YOURNUMBER", "message": "Hello from RedwoodJS and Vonage! (Local Test)" }'Expected Response (Success):
{ "success": true, "message": "SMS submitted successfully.", "messageUuid": "some-unique-vonage-message-id" }You should receive the SMS on your test phone shortly after. Check the terminal running
yarn rw devfor logs.Expected Response (Error - e.g., invalid number format):
{ "error": "Invalid 'to' field format. Use E.164 format (e.g., +15551234567)." }4. Vonage API Dashboard Setup and Configuration
Let's walk through finding your Vonage credentials and configuring your account.
Log in to Vonage API Dashboard: https://dashboard.nexmo.com/
Locate Application ID & Private Key:
VONAGE_APPLICATION_IDin your environment variables.private.keyfile will be downloaded. If you've lost your key for an existing application, you may need to generate a new public/private key pair (this will invalidate the old one) and download the newprivate.key. For local development, place this file in your project root as configured byVONAGE_PRIVATE_KEY_PATH. For deployment, you'll encode its content (see Deployment section).Configure Vonage Phone Number (
VONAGE_FROM_NUMBER):+15551234567) forVONAGE_FROM_NUMBERin your environment variables.Set Default SMS API Setting:
vonage.messages.send), setting this default to Messages API aligns your account settings and is generally recommended by Vonage when primarily using the Messages API. Save changes if needed.4.1. A2P 10DLC Registration Guide (Required for US Production SMS)
For production A2P SMS messaging in the US, you must complete 10DLC (10-Digit Long Code) registration. This is mandatory as of 2025 for all Application-to-Person SMS traffic from US long code numbers.
Why 10DLC Registration is Required:
10DLC Registration Steps:
Brand Registration:
Campaign Registration:
Link Numbers to Campaign:
10DLC Throughput Limits (2025):
10DLC throughput varies based on your Brand trust score and Campaign type. Standard rates:
10DLC Fees (2025):
$4) plus annual renewal ($1.50)Compliance Guidelines:
References:
5. Error Handling and Logging Best Practices for SMS
We've already built-in basic error handling and logging.
Error Handling Strategy: The
vonageClient.jscatches errors from the SDK, logs them with details, and throws a new, potentially more informative error. The API handler (sendSms.js) catches errors from the client function, logs them, and returns an appropriate HTTP error response (e.g., 500, 401) with the error message.Logging: RedwoodJS's logger (
pino) is used in both the client library and the API handler to log informational messages (like successful submission, includingmessageUuid) and errors (including recipient number and error details). Check the console whereyarn rw devis running. In production, configure log draining to a service like Papertrail, Datadog, or CloudWatch.Common Vonage API Errors to Handle:
Authentication failed: CheckVONAGE_APPLICATION_IDand the private key content (fromVONAGE_PRIVATE_KEY_BASE64orVONAGE_PRIVATE_KEY_PATH).Non-Whitelisted Destination: If using a trial account, ensure thetonumber is added to the allowed list in the Vonage dashboard ("Sandbox & Test Numbers").Invalid Parameters: Checktoformat (E.164),fromformat, ensuremessageis not empty.Insufficient funds: Your Vonage account needs credit.Invalid sender address: Thefromnumber might be incorrect or not SMS-enabled.Throughput capacity exceeded: Sending too many messages too quickly (consider queues/throttling for high volume).10DLC Registration Required(2025): For US long code numbers, ensure 10DLC Brand and Campaign registration is complete. Unregistered numbers may experience blocked or filtered messages.Campaign Suspended(2025): Your 10DLC campaign may have been suspended due to compliance violations. Check the Vonage Dashboard and TCR portal for details.Throughput Limit Exceeded(10DLC): Sending faster than your approved 10DLC throughput limit. Implement rate limiting based on your campaign's approved messages per second.Retry Mechanisms:
vonage.messages.sendcall in a library likeasync-retry.// Example using async-retry (install with: yarn workspace api add async-retry) // --- In api/src/lib/vonageClient.js --- // import retry from 'async-retry'; // // // Replace the existing try...catch block inside sendSms with this: // try { // const resp = await retry(async (bail, attempt) => { // logger.debug(`Attempt ${attempt} to send SMS via Vonage...`); // try { // return await vonage.messages.send({ // message_type: 'text', // to: to, // from: from, // channel: 'sms', // text: text, // }); // } catch (error) { // // Don't retry on certain errors (e.g., authentication, bad request) // if (error.response && (error.response.status === 401 || error.response.status === 400)) { // logger.warn({ status: error.response.status }, 'Non-retriable error received from Vonage. Bailing.'); // bail(error); // Stop retrying // return; // Needed because bail doesn't return // } // // For other errors (like network issues, 5xx), throw to trigger retry // logger.warn({ error }, `Retriable error encountered on attempt ${attempt}.`); // throw error; // } // }, { // retries: 3, // Number of retries (total attempts = retries + 1) // minTimeout: 500, // Initial delay in ms before first retry // factor: 2, // Exponential backoff factor (delay = minTimeout * factor^(attempt-1)) // onRetry: (error, attempt) => { // logger.warn(`Retrying Vonage API call (attempt ${attempt}) due to error: ${error.message}`); // } // }); // // logger.info({ messageUuid: resp.message_uuid, to }, 'SMS submitted successfully to Vonage after retries'); // return resp; // } catch (error) { // logger.error({ error, to, from }, 'Failed to send SMS via Vonage after all retries'); // // Rethrow specific error details if available // if (error.response && error.response.data) { // logger.error({ vonageError: error.response.data }, 'Vonage API error details'); // throw new Error(`Vonage API Error: ${error.response.data.title || 'Unknown error'} - ${error.response.data.detail || error.message}`); // } // throw new Error(`Failed to send SMS: ${error.message}`); // }6. Database Integration for SMS Tracking (Optional)
For the core functionality of sending an SMS, no database schema is strictly required by this application itself.
However, in a real-world scenario, you might want to store:
messageUuid).If needed, you would use RedwoodJS's Prisma integration:
api/db/schema.prisma:sendSmsAPI handler to create anSmsLogentry after successfully submitting to Vonage:// api/src/functions/sendSms.js import { db } from 'src/lib/db' // Import Prisma client // ... inside handler, within the successful try block ... try { const result = await sendVonageSms(to.trim(), message.trim()) // --- Log to database --- // Using .catch() makes this 'fire-and-forget'. The function won't wait // for the DB write to complete before responding. This improves response time // slightly but risks losing the log if the function terminates unexpectedly. db.smsLog.create({ data: { recipient: to.trim(), message: message.trim(), vonageUuid: result.message_uuid, status: 'submitted', // Initial status } }).catch(dbError => { // Log the DB error, but don't fail the overall SMS request logger.error({ error: dbError }, "Failed to save SMS log to database"); }); // If reliability of logging is critical, use 'await' instead: // try { // await db.smsLog.create({ data: { ... } }); // } catch (dbError) { // logger.error({ error: dbError }, "Failed to save SMS log to database"); // // Decide if this should cause the main function to return an error // } // --- End log to database --- logger.info({ messageUuid: result.message_uuid }, 'Successfully processed sendSms request') return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, message: 'SMS submitted successfully.', messageUuid: result.message_uuid, }), } } catch (error) { /* ... existing error handling ... */ }await db.smsLog.create(...)ensures the log is successfully written before the200 OKresponse is sent, making logging more reliable. The "fire-and-forget" approach (db.smsLog.create(...).catch(...)) is slightly faster for the user but carries a small risk of the log write failing silently if the serverless function terminates quickly. Choose based on your application's reliability requirements.This database integration is optional for basic sending but recommended for tracking and auditing.
7. Security Best Practices for SMS APIs
Security is paramount when dealing with APIs and user data.
Input Validation & Sanitization:
sendSms.js) forto(format) andmessage(existence).libphonenumber-jsfor more robust E.164 validation if needed:yarn workspace api add libphonenumber-js.Credential Security:
.envorprivate.keyto Git (use.gitignore).VONAGE_PRIVATE_KEY_BASE64). Do not store production secrets in.envfiles within the deployed application code.Rate Limiting:
rate-limiter-flexiblewith a store like Redis (requires adding Redis to your stack).Authentication/Authorization (for the API Endpoint):
yarn rw setup auth <provider>) or other mechanisms (e.g., API keys for server-to-server communication). Ensure only authorized users/services can trigger SMS sending.Least Privilege (Vonage API Key - Not Used Here, but Relevant):
8. Handling Special Cases and International SMS
E.164 Format: Always ensure the
tonumber is in E.164 format (+followed by country code and number, no spaces or dashes, e.g.,+14155552671,+442071838750). Our basic validation checks for the starting+. Uselibphonenumber-jsfor robust parsing/validation if needed.Character Limits & Encoding:
Sender ID (
fromNumber):VONAGE_FROM_NUMBER.International Sending: Vonage supports international SMS, but ensure your account is enabled for the destination countries and be aware of varying costs and regulations.
Opt-Out Handling: Implement mechanisms for users to opt-out of receiving SMS (e.g., replying "STOP"). This often requires setting up inbound message webhooks (outside the scope of this guide) to process replies. Comply with regulations like TCPA (US).
9. Performance Optimizations
For sending individual or low-volume SMS messages triggered by user actions or specific events, the performance of the current setup (direct API call within the serverless function) is generally sufficient. Response times will depend on the Vonage API latency.
Asynchronous Processing: If the API endpoint needs to respond very quickly without waiting for Vonage's confirmation, or if you need to send bulk messages, consider moving the
sendVonageSmscall to a background job queue (e.g., using services like AWS SQS, Google Cloud Tasks, or libraries like BullMQ with Redis). The API handler would just enqueue the job and return immediately.SDK Initialization: The Vonage SDK is initialized once per function invocation (cold start) or reused on warm starts. This overhead is typically minimal for serverless functions.
Bulk Sending: The Vonage Messages API doesn't have a specific "bulk send" endpoint like some older APIs might. Sending multiple messages involves making multiple calls to the
sendendpoint. If sending high volumes, implement throttling and potentially use a job queue to manage the rate of API calls to avoid hitting Vonage throughput limits.For this guide's scope, direct API calls are appropriate. Evaluate the need for queues based on your specific application's volume and latency requirements.
Related Resources
Learn more about SMS integration with RedwoodJS: