Send SMS and Receive Delivery Status Callbacks with Node.js, Express, and Vonage
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage API and reliably receive delivery status updates (Delivery Receipts or DLRs) through webhooks. We'll cover everything from project setup to handling DLRs, ensuring you have a production-ready foundation.
You'll learn how to configure Vonage, send messages programmatically, set up a webhook endpoint to receive status updates, and handle potential issues. By the end, you'll have a functional application capable of sending SMS and tracking its delivery status.
Key Technologies:
- Node.js: A JavaScript runtime for building server-side applications.
- Express: A minimal and flexible Node.js web application framework.
- Vonage Node.js SDK: Simplifies interaction with the Vonage APIs.
- Vonage API Account: Required for sending SMS and configuring webhooks.
- ngrok (for development): Exposes your local server to the internet for webhook testing.
System Architecture:
+--------------------+ Sends SMS Request +------------------+ Sends SMS +-----------------+
| Your Node.js App |----------------------------->| Vonage SMS API |--------------------->| Carrier Network |
| (Express Server | | | | |
| - Send Logic | +--------+---------+ +--------+--------+
| - DLR Handler) | | |
+---------^----------+ | Receives DLR from Carrier | Sends DLR
| | v
| Processes DLR | +-----------------+
| received at | | Vonage Platform |
| /webhooks/dlr v | (Webhook Cfg) |
+--------------------+ Forwards Request +--------------------+ Sends DLR Webhook +--------+--------+
| Localhost:3000 |<---------------------------| ngrok Tunnel |<------------------------------|
| (Listens for DLR) | | (Public HTTPS URL) |
+--------------------+ +--------------------+
Prerequisites:
- Node.js and npm: Installed on your system (Download Node.js).
- Vonage API Account: Sign up for free (Vonage Dashboard). Note your API Key and API Secret.
- Vonage Virtual Number: Purchase an SMS-capable number from the Vonage Dashboard.
- ngrok: Installed and authenticated (Download ngrok).
1. Setting up the Project
Let's create the project structure and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project_ then navigate into it.
mkdir vonage-sms-dlr-guide cd vonage-sms-dlr-guide
-
Initialize Node.js Project: Initialize the project using npm. The
-y
flag accepts default settings.npm init -y
This creates a
package.json
file. -
Install Dependencies: We need the Vonage Server SDK for interacting with the API_ Express for our web server_ and
dotenv
to manage environment variables securely.npm install @vonage/server-sdk express dotenv --save
-
Create Project Files: Create the main files for our application logic and environment variables.
# For macOS/Linux/Git Bash: touch index.js server.js .env .gitignore
(Note for Windows CMD users: If
touch
is not available_ you can usetype nul > filename
for each file or create them manually in your editor.)index.js
: Will contain the code to send SMS messages.server.js
: Will contain the Express server code to receive DLR webhooks..env
: Will store sensitive credentials like API keys and phone numbers..gitignore
: Specifies files that Git should ignore (like.env
andnode_modules
).
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing them to version control.# .gitignore node_modules/ .env
-
Configure Environment Variables (
.env
): Open the.env
file and add your Vonage credentials and configuration. Replace the placeholder values with your actual data.# .env # Vonage Credentials (Find these in your Vonage Dashboard) VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Vonage Number (Must be SMS-capable and purchased in your account) VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Recipient Number (The number you want to send the test SMS to) TO_NUMBER=RECIPIENT_PHONE_NUMBER # Server Configuration PORT=3000 # Port your local server will listen on
VONAGE_API_KEY
,VONAGE_API_SECRET
: Found on the main page of your Vonage API Dashboard.VONAGE_NUMBER
: One of your virtual numbers purchased from Vonage (Vonage Dashboard > Numbers > Your Numbers). Ensure it's SMS-capable. Use E.164 format (e.g.,14155550100
).TO_NUMBER
: The destination phone number for your test SMS, also in E.164 format.PORT
: The local port ngrok will expose and your Express server will listen on.3000
is a common choice.
2. Implementing Core Functionality: Sending SMS
Now, let's write the code in index.js
to send an SMS message using the Vonage SDK.
-
Edit
index.js
: Openindex.js
and add the following code:// index.js require('dotenv').config(); // Load environment variables from .env file const { Vonage } = require('@vonage/server-sdk'); // --- Configuration --- const vonageApiKey = process.env.VONAGE_API_KEY; const vonageApiSecret = process.env.VONAGE_API_SECRET; const fromNumber = process.env.VONAGE_NUMBER; const toNumber = process.env.TO_NUMBER; // Basic validation if (!vonageApiKey || !vonageApiSecret || !fromNumber || !toNumber) { console.error( 'Error: Missing required environment variables. Check your .env file.' ); process.exit(1); // Exit if configuration is incomplete } // --- Initialize Vonage Client --- // For SMS API, authentication via API Key and Secret is standard. const vonage = new Vonage({ apiKey: vonageApiKey, apiSecret: vonageApiSecret, }); // --- Send SMS Function --- async function sendSms() { const text = `Hello from Vonage! This is a test message sent on ${new Date().toLocaleTimeString()}.`; console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}...`); try { // Use the vonage.sms.send method for the dedicated SMS API endpoint const resp = await vonage.sms.send({ to: toNumber, from: fromNumber, text: text, }); // The SMS API response structure is different from Messages API // It returns an array of message responses if (resp.messages && resp.messages.length > 0) { const message = resp.messages[0]; if (message.status === '0') { console.log('Message sent successfully!'); console.log(`Message ID: ${message['message-id']}`); console.log(`Remaining balance: ${message['remaining-balance']}`); } else { console.error(`Message failed with error: ${message['error-text']}`); } } else { console.error('Unexpected response format from Vonage API:', resp); } } catch (err) { console.error('Error sending SMS:'); // Check for specific Vonage error structures if available if (err.response && err.response.data) { console.error(JSON.stringify(err.response.data, null, 2)); } else { console.error(err); } } } // --- Execute --- sendSms();
-
Explanation:
require('dotenv').config();
: Loads the variables defined in your.env
file intoprocess.env
.- Configuration: Retrieves the API key, secret, and phone numbers from environment variables (
process.env.VONAGE_API_KEY
, etc.). Includes basic validation. - Initialize Vonage Client: Creates an instance of the
Vonage
client using your API Key and Secret. sendSms
Function:- Defines the message text.
- Calls
vonage.sms.send()
. This method is specifically designed for the Vonage SMS API. - It takes an object with
to
,from
, andtext
properties. - Uses
async/await
for cleaner handling of the promise returned by the SDK. - Response Handling: The SMS API response includes a
messages
array. We check thestatus
of the first message ('0'
indicates success) and log themessage-id
which is crucial for correlating with Delivery Receipts later. We also log anyerror-text
if the status is not'0'
.
- Error Handling: Includes a
try...catch
block to gracefully handle network issues or API errors during the send process. It attempts to log detailed error information if available from the Vonage SDK error object. - Execution: Calls the
sendSms()
function to initiate the process.
-
Test Sending: Make sure you've saved your
.env
file with the correct credentials and numbers. Run the script from your terminal:node index.js
You should see output indicating the message was sent successfully and receive an SMS on the
TO_NUMBER
phone. Keep the loggedMessage ID
handy – you'll see it again when the DLR comes back.
3. Building the API Layer: Webhook for Delivery Receipts
Now, we'll create the Express server in server.js
to listen for incoming POST requests from Vonage containing the DLRs.
-
Edit
server.js
: Openserver.js
and add the following code:// server.js require('dotenv').config(); // Load environment variables const express = require('express'); const app = express(); // --- Configuration --- const port = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Middleware --- // Enable Express to parse JSON request bodies app.use(express.json()); // Enable Express to parse URL-encoded request bodies (common for webhooks) app.use(express.urlencoded({ extended: true })); // --- Webhook Endpoint for Delivery Receipts --- // Vonage sends DLRs via POST by default, but handling GET can be useful for initial browser checks/pings app.all('/webhooks/dlr', (req, res) => { console.log('--- Delivery Receipt Received ---'); // Vonage might send DLR data in query params (GET) or body (POST) const params = { ...req.query, ...req.body }; console.log('Timestamp:', new Date().toISOString()); console.log('Method:', req.method); console.log('Payload:', JSON.stringify(params, null, 2)); // Log the entire payload // --- Process the DLR --- // Key fields to look for in a Vonage DLR: // - messageId: The ID of the original outbound message // - msisdn: The recipient phone number // - status: The delivery status (e.g., 'delivered', 'failed', 'expired', 'buffered') // - err-code: Error code if status is 'failed' or 'rejected' // - price: Cost of the message segment // - scts: Status change timestamp from the carrier // - message-timestamp: When the original message was sent if (params.messageId && params.status) { console.log(`Status for message ${params.messageId}: ${params.status}`); // TODO: Add logic here to update your database or trigger other actions based on the status // Example: findOrderByMessageId(params.messageId).updateStatus(params.status); } else { console.warn('Received incomplete DLR data.'); } // --- Respond to Vonage --- // IMPORTANT: Always send a 2xx status code back to Vonage quickly. // Failure to do so will cause Vonage to retry sending the webhook, // potentially leading to duplicate processing. // 200 OK or 204 No Content are appropriate. res.status(200).send('OK'); // Alternatively: res.status(204).end(); }); // --- Basic Health Check Endpoint --- app.get('/health', (req, res) => { console.log('Health check endpoint hit.'); res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening for webhooks at http://localhost:${port}`); console.log(`Webhook endpoint: /webhooks/dlr`); });
-
Explanation:
- Middleware:
express.json()
andexpress.urlencoded({ extended: true })
are crucial for parsing incoming request bodies. Vonage DLRs typically arrive asapplication/x-www-form-urlencoded
, but supporting JSON is good practice. - Webhook Endpoint (
/webhooks/dlr
):- Uses
app.all()
to handle both GET and POST requests on this path, though Vonage primarily uses POST for DLRs. - Logs that a request was received.
- Combines
req.query
andreq.body
into a singleparams
object to handle data regardless of the HTTP method used by Vonage (though POST body is standard for DLRs). - Logs the received payload for debugging.
- Crucially identifies key DLR fields:
messageId
,status
,msisdn
,err-code
. - Includes a
// TODO:
comment indicating where you would add your application-specific logic (e.g., updating a database record with the delivery status). - Responds with
res.status(200).send('OK');
: This is vital. You must acknowledge receipt of the webhook to Vonage with a2xx
status code. Failing to respond or responding with an error code will cause Vonage to retry delivering the webhook.
- Uses
- Health Check (
/health
): A simple GET endpoint useful for verifying the server is running and accessible. - Start Server: Starts the Express application, listening on the configured
PORT
.
- Middleware:
4. Integrating with Vonage: Configuring Webhooks
To receive DLRs, you need to tell Vonage where to send them. This involves exposing your local server using ngrok (during development) and configuring the webhook URL in the Vonage Dashboard.
-
Start Your Local Server: Open a terminal window in your project directory and run:
node server.js
You should see
Server listening for webhooks at http://localhost:3000
. -
Expose Local Server with ngrok: Open a second terminal window. Run ngrok to create a public URL tunnel to your local port
3000
(or whichever port your server is using, matching thePORT
in your.env
).ngrok http 3000
ngrok will display output similar to this:
Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us-east-1) Web Interface http://127.0.0.1:4040 Forwarding http://<random_string>.ngrok-free.app -> http://localhost:3000 Forwarding https://<random_string>.ngrok-free.app -> http://localhost:3000 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
Copy the
https://<random_string>.ngrok-free.app
URL. This is your public base URL. -
Configure Vonage SMS Settings:
- Go to your Vonage API Dashboard.
- Navigate to Settings in the left-hand menu.
- Scroll down to the SMS Settings section.
- Default SMS Setting: Ensure SMS API is selected as the default API for sending/receiving SMS. This setting determines the format of webhooks you receive. If it's set to Messages API, the DLR webhook format will be different. Click Save changes if you modify this.
- Delivery receipts (DLR) webhooks:
- In the Webhook URL for Delivery Receipt field, paste your ngrok HTTPS URL and append the path to your webhook endpoint:
https://<random_string>.ngrok-free.app/webhooks/dlr
- Set the HTTP Method to POST.
- Click Save changes.
- In the Webhook URL for Delivery Receipt field, paste your ngrok HTTPS URL and append the path to your webhook endpoint:
-
Verification:
- Your local
server.js
should be running. - ngrok should be running and tunneling to port
3000
. - Vonage settings should point the DLR webhook to your ngrok URL.
- Your local
5. Implementing Error Handling and Logging
Robust error handling and logging are crucial for production systems.
-
SMS Sending Errors (
index.js
):- The
try...catch
block inindex.js
already provides basic error handling for the API call itself. - It logs the specific error message from Vonage (
message['error-text']
) if the API indicates a failure (status !== '0'
). - For production, consider using a dedicated logging library like Winston or Pino for structured logging (e.g., JSON format) and different log levels (info, warn, error).
- The
-
Webhook Handling Errors (
server.js
):- Respond Quickly: The most critical aspect is responding
200 OK
or204 No Content
to Vonage before undertaking potentially slow or fallible processing. This prevents Vonage retries due to timeouts. The example below demonstrates this "respond first" pattern. - Log Everything: The current code logs the entire incoming payload. This is essential for debugging. In production, you might filter sensitive data before logging.
- Graceful Failures: Wrap your DLR processing logic (the
// TODO:
section) in its owntry...catch
block. If your internal processing fails (e.g., database error), log the error comprehensively, but ensure the 2xx response has already been sent to Vonage. You'll need separate monitoring/alerting for failures in your internal processing logic.
// Example of safer webhook processing in server.js app.all('/webhooks/dlr', (req, res) => { console.log('--- Delivery Receipt Received ---'); const params = { ...req.query, ...req.body }; console.log('Payload:', JSON.stringify(params, null, 2)); // Respond to Vonage immediately *before* heavy or potentially failing processing // This acknowledges receipt and prevents Vonage retries for this delivery attempt. res.status(200).send('OK'); // Now, attempt to process the DLR asynchronously or synchronously (if very fast) try { if (params.messageId && params.status) { console.log(`Processing status for message ${params.messageId}: ${params.status}`); // --- Your application logic here --- // This is where you'd update your database, push to a queue, etc. // Example: await updateDatabaseRecord(params.messageId, params.status); // If this logic fails, it should be logged below, but Vonage won't retry // because the 200 OK was already sent. // ----------------------------------- } else { // Log incomplete data, but still don't cause a Vonage retry. console.warn('Received incomplete DLR data.'); } } catch (processingError) { // Log the internal processing error thoroughly. console.error('Error processing DLR after response sent:', processingError); // TODO: Implement alerting or error tracking here (e.g., Sentry, Datadog) // This alerts you to failures in your *own* system's handling of the DLR. } });
- Respond Quickly: The most critical aspect is responding
-
Vonage Retries: Be aware that if Vonage doesn't receive a
2xx
response from your webhook endpoint within a reasonable timeframe (usually a few seconds), it will retry sending the DLR. Your webhook handler should be idempotent – meaning processing the same DLR multiple times should not cause adverse effects (e.g., use themessageId
to ensure you only update a status once, or use database transactions appropriately). The "respond first" pattern helps avoid retries due to processing time but doesn't inherently make the processing logic idempotent.
6. Database Schema and Data Layer (Conceptual)
While this guide doesn't implement a full database layer, here's a conceptual schema for storing message and delivery status information:
Entity Relationship Diagram (Conceptual):
+-------------------+ +---------------------------+
| Users | | Messages |
|-------------------| |---------------------------|
| user_id (PK) | | internal_message_id (PK) | --> Auto-incrementing DB ID
| name | | user_id (FK) |
| phone_number | | vonage_message_id (UQ, IX)| --> Vonage message-id (Unique, Indexed)
| ... | | to_number |
+-------------------+ | from_number |
| | body |
| | status | --> (e.g., 'submitted', 'delivered', 'failed', 'expired')
| | submitted_at |
+------------------| last_status_update_at |
| error_code | --> DLR err-code
| price | --> DLR price
| ... |
+---------------------------+
Data Access:
- When sending an SMS (
index.js
), you would insert a new record into theMessages
table with an initial status like'submitted'
, storing thevonage_message_id
returned by the API. You'd get back theinternal_message_id
. - When the DLR webhook (
server.js
) receives a status update, you would use themessageId
from the payload (which corresponds tovonage_message_id
in your table) to find the relevant record in yourMessages
table and update thestatus
,last_status_update_at
,error_code
, andprice
fields accordingly.
Tools:
- ORM: Libraries like Sequelize, TypeORM, or Prisma help manage database interactions in Node.js.
- Migrations: Tools like
sequelize-cli
or Prisma Migrate help manage database schema changes over time.
7. Adding Security Features
Securing your application, especially the webhook endpoint, is crucial.
-
Secure Credential Storage: Already addressed by using
.env
and.gitignore
. Never commit secrets to version control. Use environment variable management provided by your hosting platform for production (VONAGE_API_KEY
,VONAGE_API_SECRET
, etc.). -
Webhook Security:
- HTTPS: Always use HTTPS for your webhook URLs (ngrok provides this, ensure your production deployment does too).
- Signature Verification (Context): Vonage can sign webhook requests for certain APIs/configurations using JWT or signature secrets, allowing you to verify the request genuinely came from Vonage. However, standard SMS API Delivery Receipts (as used in this guide) generally do not support JWT signature verification. This feature is typically available for other APIs like the Messages API, Voice API, or Verify API v2. Always check the latest Vonage documentation on Signed Webhooks and the specific API reference to confirm availability for your use case. If available for your configuration, implement verification using your Vonage Signature Secret.
- IP Whitelisting (Less Flexible): You could restrict incoming traffic to your webhook endpoint to only allow requests from Vonage's known IP ranges. However, these ranges can change, making this approach brittle and requiring maintenance. (Vonage IP Addresses).
- Obscurity (Not True Security): Using a long, unpredictable path for your webhook (e.g.,
/webhooks/dlr/aBcD3fG7hJkL9mN1oPqR
) offers minimal protection but can deter casual scanners.
-
Rate Limiting: Protect your webhook endpoint from abuse or potential retry storms by implementing rate limiting. The
express-rate-limit
package is a popular choice.npm install express-rate-limit --save
// Add to server.js near the top const rateLimit = require('express-rate-limit'); // Apply rate limiting to the webhook endpoint const dlrLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 500, // Limit each IP to 500 requests per windowMs (adjust as needed) message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the limiter specifically to the DLR route *before* the handler app.use('/webhooks/dlr', dlrLimiter); // --- Webhook Endpoint for Delivery Receipts --- app.all('/webhooks/dlr', (req, res) => { // ... your handler logic ... }); // ... rest of your server.js code ...
-
Input Validation: While the DLR payload comes from Vonage, it's still good practice to validate expected fields and types before processing, though less critical than user-submitted data.
8. Handling Special Cases Relevant to SMS DLRs
Consider these real-world scenarios:
- DLR Status Codes: Vonage uses various status codes. Handle the most common ones explicitly:
delivered
: Message successfully reached the handset.failed
: Message could not be delivered (checkerr-code
for reason).expired
: Message delivery timed out (often handset off or out of coverage).buffered
: Message is waiting on the carrier network (temporary state).rejected
: Message was rejected by Vonage or the carrier (often due to spam filters, invalid number, or permissions).accepted
: Message accepted by the downstream carrier, delivery pending.unknown
: The final status couldn't be determined.- Refer to the Vonage API Reference for Delivery Receipts for the full list and
err-code
meanings.
- DLR Delays: Delivery receipts are not always instantaneous. They depend on the downstream carrier network reporting back. Delays can range from seconds to minutes, or sometimes longer. Your application should tolerate these delays.
- Missing DLRs: Not all carriers or regions reliably support or return DLRs. Do not design your core application logic to depend solely on receiving a
delivered
status. Sometimes, a message might be delivered without a DLR ever arriving. - Out-of-Order DLRs: While rare, it's theoretically possible (due to network conditions or retries) to receive DLRs for the same
messageId
out of sequence (e.g.,buffered
thendelivered
). Ensure your status update logic handles this gracefully (e.g., only update if the new status is considered 'more final' than the current one, perhaps using timestamps). - Multi-part Messages: Longer SMS messages are split into multiple parts. The SMS API typically returns multiple message objects in the initial send response, each with its own
message-id
. You might receive separate DLRs for each part. You may need logic to determine the overall delivery status based on the statuses of all parts.
9. Implementing Performance Optimizations
For high-volume applications, optimize your webhook handler.
- Respond Quickly: As emphasized in Error Handling, respond
2xx
immediately using the "respond first" pattern. - Asynchronous Processing: Offload database updates or other intensive tasks triggered by the DLR to a background job queue (e.g., BullMQ with Redis, RabbitMQ). The webhook handler simply adds a job to the queue and responds
2xx
. - Database Indexing: Ensure your
Messages
table (or equivalent) has an index on thevonage_message_id
column (as shown in the conceptual schema) for fast lookups when processing DLRs. - Webhook Server Scaling: If handling a very high volume of DLRs, deploy your
server.js
application behind a load balancer and run multiple instances (e.g., using PM2 cluster mode or container orchestration like Kubernetes). Ensure your processing logic (especially database updates) can handle concurrent operations correctly (idempotency, transactions). - Caching: If frequently querying message statuses, consider caching status information (e.g., in Redis) to reduce database load, with appropriate cache invalidation when a new DLR arrives.
10. Adding Monitoring, Observability, and Analytics
Understand how your SMS sending and DLR processing are performing.
- Health Checks: The
/health
endpoint inserver.js
is a basic start. Production health checks might involve checking database connectivity or queue status. - Logging: Use structured logging (JSON) and send logs to a centralized logging platform (e.g., Datadog Logs, AWS CloudWatch Logs, ELK stack). This enables searching and analysis. Log errors from both sending (
index.js
) and DLR processing (server.js
). - Metrics: Track key performance indicators (KPIs):
- Number of SMS sent (success/failure).
- API latency for
vonage.sms.send()
. - Number of DLRs received per status (
delivered
,failed
, etc.). - Webhook endpoint latency (
/webhooks/dlr
). - Webhook endpoint HTTP status codes (monitor for non-2xx, although ideally should be 0 if responding correctly).
- Internal DLR processing error rate (errors caught in the
try...catch
block inserver.js
). - Queue depth (if using asynchronous processing).
- Use libraries like
prom-client
for Prometheus metrics or platform-specific monitoring agents (Datadog Agent, etc.).
- Error Tracking: Integrate an error tracking service (e.g., Sentry, Datadog APM) to capture and alert on exceptions in both
index.js
andserver.js
, especially the internal DLR processing errors. - Dashboards: Create dashboards in your monitoring tool (e.g., Grafana, Datadog) visualizing the metrics above to get an at-a-glance view of system health and SMS delivery performance.
- Alerting: Set up alerts based on metrics and logs:
- High SMS sending failure rate.
- High webhook latency.
- Significant drop in the rate of received DLRs (could indicate a configuration issue).
- Increase in internal DLR processing errors.
11. Troubleshooting and Caveats
Common issues and things to be aware of:
- Webhook Not Firing:
- Check ngrok: Is it running? Is the URL correct? Any errors in the ngrok console? Can you access the ngrok URL in a browser (it might show an error, but should respond)?
- Check Server: Is your
node server.js
running? Any startup errors? Check the console logs for incoming requests and processing logs. - Check Vonage Settings: Is the webhook URL correct in the Vonage Dashboard settings? Is the HTTP method set to POST? Is the "Default SMS Setting" configured for "SMS API" (as webhook formats differ for Messages API)?
- Send a Test SMS: Trigger the process by sending an SMS using
node index.js
. Watch both theserver.js
console and the ngrok console (http://127.0.0.1:4040
) for incoming requests to/webhooks/dlr
.
- Incorrect DLR Format: If the DLR payload looks different from expected (e.g., JSON format instead of URL-encoded, different field names), double-check the "Default SMS Setting" in your Vonage Dashboard. Ensure it's set to "SMS API" for the format described in this guide.
- Credentials Error: Ensure
VONAGE_API_KEY
andVONAGE_API_SECRET
in your.env
file are correct and have no extra spaces. - Invalid
from
Number: EnsureVONAGE_NUMBER
is a valid, SMS-capable number purchased in your Vonage account and formatted correctly (E.164). - Throttling/Blocking: Check if the recipient number is blocked or if there are carrier restrictions. Check your Vonage account balance.
- Idempotency Issues: If you see duplicate processing, ensure your webhook handler is idempotent and that you are responding
2xx
quickly. - Firewall Issues: If deploying to production, ensure any firewalls allow incoming traffic on the webhook port from Vonage's IP ranges (though signature verification or obscurity is often preferred over IP whitelisting).