code examples
code examples
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.
Send SMS with Vonage Messages API: Complete RedwoodJS Guide
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
System Architecture:
+-------------+ +---------------------+ +-----------------+ +-------------+
| Client | ----> | RedwoodJS API Func | ----> | Vonage Node SDK | ----> | Vonage API |
| (e.g., Web) | | (/api/sendSms) | | (@vonage/server)| | (Messages) |
+-------------+ +---------------------+ +-----------------+ +-------------+
| Loads Credentials |
| from .env / env |
+-----------------+Prerequisites:
- 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/clioryarn 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:
bashyarn create redwood-app ./vonage-sms-appThis command scaffolds a new RedwoodJS project in the
vonage-sms-appdirectory. -
Navigate to Project Directory:
bashcd vonage-sms-app -
Install Vonage SDK Dependencies: We need the Vonage Server SDK. RedwoodJS includes
dotenvby default for environment variable management.bashyarn workspace api add @vonage/server-sdkWhy
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.
- Create a
.envfile in the root of your project (vonage-sms-app/.env). - Add the following variables:
dotenv# .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., +15551234567VONAGE_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.
- Create a
-
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:text# .gitignore # Environment Variables .env .env.* !.env.example !.env.defaults # Vonage Private Key private.key *.keyWhy? 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.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}`) } }- 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? Thevonage.messages.sendmethod returns a Promise, makingasync/awaita 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...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.
- Why Base64 or Path? This setup allows using a simple file path (
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:
bashyarn 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:javascript// 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 }), } } }- 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
toandmessageexist and are strings. We also add a basic check for the E.164 format on thetonumber. 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
messageUuidfrom Vonage in the success response is helpful for tracking.
- Why check
-
Test Your SMS Integration Locally: Start the development server:
bashyarn rw devOpen 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.bashcurl -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):
json{ "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):
json{ "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:
- Navigate to "Applications" in the left sidebar.
- 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_IDin your environment variables. - Private Key: If creating a new application, click "Generate public and private key". A
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). - 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) forVONAGE_FROM_NUMBERin 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
References:
- Vonage 10DLC Overview
- Vonage 10DLC Brand Registration Guide
- A2P Messaging Options in the U.S. and Canada
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:
- Network Issues: For transient network errors between your server and Vonage, a simple retry strategy can help. You could wrap the
vonage.messages.sendcall in a library likeasync-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).
- Network Issues: For transient network errors between your server and Vonage, a simple retry strategy can help. You could wrap the
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
sendSmsAPI handler to create anSmsLogentry after successfully submitting to Vonage:Trade-off: Usingjavascript// 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:
- We implemented basic validation in the API handler (
sendSms.js) forto(format) andmessage(existence). - Further Validation: Consider using a library like
libphonenumber-jsfor 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.
- We implemented basic validation in the API handler (
-
Credential Security:
- NEVER commit
.envorprivate.keyto 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.envfiles within the deployed application code.
- NEVER commit
-
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-flexiblewith 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.
- Our current endpoint is open. In a real application, you'd protect it using RedwoodJS's built-in auth (
-
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
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:
- 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 (
fromNumber):- 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
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: