This guide provides a step-by-step walkthrough for building a Node.js application using the Fastify framework to receive real-time delivery status updates for SMS messages sent via the Vonage Messages API. Knowing the final delivery status is crucial for understanding message reachability and ensuring reliable communication.
We will cover setting up the project, configuring Vonage, implementing the webhook handler in Fastify, sending a test message, and verifying the status updates. We'll also touch upon essential aspects like error handling, security, and deployment.
Project Overview and Goals
Goal: To create a reliable webhook endpoint using Fastify that listens for and processes SMS delivery status updates sent by the Vonage Messages API.
Problem Solved: When you send an SMS message using an API, the initial success response only confirms that the message was accepted by the platform (Vonage) for sending. It doesn't guarantee delivery to the recipient's handset. This application solves that by providing real-time feedback from the carrier network about the message's final status (e.g., delivered, failed, rejected).
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Fastify: A high-performance, low-overhead web framework for Node.js, chosen for its speed and developer experience.
- Vonage Messages API: A unified API for sending and receiving messages across various channels, including SMS. We use this API for sending messages and receiving status updates.
@vonage/server-sdk
: The official Vonage Node.js SDK for interacting with the Vonage APIs.ngrok
(for local development): A tool to expose local servers to the public internet, necessary for Vonage webhooks to reach your development machine.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.
System Architecture:
+-----------------+ +-----------------+ +-----------------+ +-----------------+ +--------------------+
| Your Fastify App| ---> | Vonage Messages | ---> | Carrier Network | ---> | Vonage Platform | ---> | Your Fastify App |
| (Send Request) | | API (Sends SMS) | | (Delivers SMS) | | (Receives DLR) | | (Webhook Endpoint) |
+-----------------+ +-----------------+ +-----------------+ +-----------------+ +--------------------+
Step 1 Step 2 Step 3 Step 4 Step 5 (Status Update)
Outcome: By the end of this guide, you will have a running Fastify application capable of:
- Sending an SMS message via the Vonage Messages API.
- Receiving
POST
requests from Vonage on a specific webhook endpoint (/webhooks/status
). - Logging the delivery status information contained in the webhook payload.
Prerequisites:
- Node.js: Installed (LTS version recommended). Check with
node -v
. - npm or yarn: Included with Node.js. Check with
npm -v
oryarn -v
. - Vonage API Account: Sign up for a free account if you don't have one. You'll get free credit to start.
- Vonage Phone Number: Purchase an SMS-capable number from the Vonage Dashboard (Numbers -> Buy Numbers).
ngrok
: Installed and authenticated (a free account is sufficient). Download from ngrok.com. Note:ngrok
provides a temporary public URL for local development. For production, you will need a stable, permanent public URL for your server.- Basic Terminal/Command Line Knowledge: Creating directories, running commands.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.
mkdir fastify-vonage-status cd fastify-vonage-status
-
Initialize Node.js Project: This creates a
package.json
file to manage dependencies and project metadata.npm init -y
(Alternatively, use
yarn init -y
if you prefer yarn) -
Install Dependencies: We need Fastify, the Vonage SDK, and
dotenv
for managing environment variables. We also install@fastify/formbody
as good practice, although Messages API status webhooks typically use JSON.npm install fastify @vonage/server-sdk dotenv @fastify/formbody
(Alternatively, use
yarn add fastify @vonage/server-sdk dotenv @fastify/formbody
) -
Create Project Files: Create the main application file and files for environment variables and git ignore rules.
touch index.js .env .gitignore
-
Configure
.gitignore
: Prevent sensitive files and unnecessary directories from being committed to version control. Add the following to your.gitignore
file:# Dependencies node_modules/ # Environment Variables .env* !.env.example # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Vonage Private Key (if stored directly, better to use path) private.key # OS generated files .DS_Store Thumbs.db
-
Set Up Environment Variables (
.env
): Create a file named.env
in the root of your project. This file will store your sensitive credentials and configuration. Never commit this file to Git.# .env # Vonage API Credentials (Found on Vonage Dashboard homepage) VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Application Credentials (Generated when creating a Vonage Application) VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID # Path to the private key file downloaded during application setup VONAGE_PRIVATE_KEY_PATH=./private.key # Example: /Users/youruser/.vonage/private.key # Vonage Number (Purchased from Vonage Dashboard) VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Your Personal Phone Number (For testing SMS sending) # Use E.164 format (e.g., 15551234567) YOUR_PHONE_NUMBER=YOUR_PERSONAL_PHONE_NUMBER # Server Port PORT=3000
- How to get these values:
VONAGE_API_KEY
&VONAGE_API_SECRET
: Found at the top of your Vonage API Dashboard.VONAGE_APPLICATION_ID
&VONAGE_PRIVATE_KEY_PATH
: Obtained in the next section when creating a Vonage Application. Theprivate.key
file will be downloaded. Important: Ideally, store this file outside your project directory for better security (e.g., in~/.vonage/private.key
) and update theVONAGE_PRIVATE_KEY_PATH
in your.env
file accordingly. Storing it in the project root is possible but less secure, even with.gitignore
.VONAGE_NUMBER
: The virtual phone number you purchased in the Vonage Dashboard under 'Numbers'.YOUR_PHONE_NUMBER
: Your actual mobile number to receive test messages.PORT
: The port your Fastify server will run on locally (e.g.,3000
).
- How to get these values:
2. Configuring Vonage for Messages API and Webhooks
To receive delivery status updates, you need a Vonage Application configured correctly for the Messages API.
-
Ensure Messages API is Default:
- Navigate to your Vonage API Dashboard.
- Go to Settings in the left-hand menu.
- Scroll down to API keys > SMS settings.
- Ensure the toggle is set to Messages API. If it's set to
SMS API
, switch it to Messages API. - Click Save changes. This is crucial for using the correct webhook format.
-
Create a Vonage Application:
- In the Vonage Dashboard, navigate to Applications -> Create a new application.
- Enter an Application name (e.g.,
Fastify SMS Status App
). - Click Generate public and private key. This will automatically download a
private.key
file. Save this file securely according to the recommendation in the previous section (ideally outside the project folder) and note its path for the.env
file. Vonage does not store this private key. - Note the Application ID displayed on the page. Add it to your
.env
file asVONAGE_APPLICATION_ID
. - Enable Capabilities -> Messages.
- You will see fields for Inbound URL and Status URL. We need a public URL for these, which we'll get from
ngrok
in the next step. Leave them blank for now, but keep this page open or be ready to edit the application later.
-
Expose Local Server with
ngrok
:- Open a new terminal window (keep the first one for running the app later).
- Run
ngrok
to forward traffic to the port your Fastify app will run on (defined asPORT
in.env
, default3000
).
ngrok http 3000
ngrok
will display output including aForwarding
URL usinghttps
. Copy thehttps
URL (e.g.,https://random-subdomain.ngrok-free.app
). This is your public URL for local testing.
-
Configure Webhook URLs in Vonage Application:
- Go back to your Vonage Application settings page (or find it under Applications -> Your Application Name -> Edit).
- In the Messages capability section:
- Paste your
ngrok
https
URL into the Status URL field and append/webhooks/status
. Example:https://random-subdomain.ngrok-free.app/webhooks/status
- Optionally, do the same for the Inbound URL appending
/webhooks/inbound
(for receiving messages, not covered in detail here). Example:https://random-subdomain.ngrok-free.app/webhooks/inbound
- Paste your
- Scroll down and click Generate new application or Save changes.
-
Link Your Vonage Number:
- On the application details page, scroll down to the Linked numbers section.
- Find the Vonage number you purchased earlier (make sure it matches
VONAGE_NUMBER
in your.env
). - Click the Link button next to the number.
Your Vonage account and application are now configured to send SMS status updates to your ngrok
URL, which forwards them to your local machine.
3. Implementing the Fastify Webhook Handler
Now, let's write the Fastify code to receive and process these status updates.
Edit your index.js
file with the following code:
// index.js
'use strict';
// 1. Import Dependencies
require('dotenv').config(); // Load .env variables into process.env
const Fastify = require('fastify');
const formbody = require('@fastify/formbody');
const { Vonage } = require('@vonage/server-sdk');
// 2. Initialize Fastify
const fastify = Fastify({
logger: true, // Enable built-in Pino logger (good for development)
});
// Register formbody plugin in case Vonage sends form-encoded data (though status should be JSON)
fastify.register(formbody);
// 3. Initialize Vonage Client (for sending SMS later)
// Note: Uses Application ID and Private Key for authentication with Messages API
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// --- Webhook Endpoint for Delivery Status ---
// 4. Define the Status Webhook Route
// Vonage sends POST requests to the Status URL you configured
fastify.post('/webhooks/status', async (request, reply) => {
fastify.log.info(`Status Webhook Received. Payload:`);
fastify.log.info(request.body); // Log the entire payload (which is the Vonage status object)
const params = request.body;
// Basic validation - Check for essential fields
if (!params.message_uuid || !params.status) {
fastify.log.warn('Received incomplete status data.');
// Still reply 200 OK so Vonage doesn't retry an invalid request
return reply.code(200).send({ message: 'Incomplete data received' });
}
// Process the status
const messageId = params.message_uuid;
const status = params.status;
const timestamp = params.timestamp;
const to = params.to?.number || params.to; // Handle potential object/string format
const from = params.from?.number || params.from; // Handle potential object/string format
fastify.log.info(`---> Message ID: ${messageId}`);
fastify.log.info(` Status: ${status}`);
fastify.log.info(` Timestamp: ${timestamp}`);
fastify.log.info(` To: ${to}`);
fastify.log.info(` From: ${from}`);
if (params.error) {
fastify.log.error(` Error Code: ${params.error.code}`);
fastify.log.error(` Error Reason: ${params.error.reason}`);
}
// TODO: Add your business logic here
// - Update a database record with the status
// - Trigger notifications
// - Add to an analytics system
// 5. Send 200 OK Response
// IMPORTANT: Vonage expects a 200 OK response to acknowledge receipt.
// If it doesn't receive one, it will retry sending the webhook.
reply.code(200).send();
});
// --- Route for Sending a Test SMS ---
// 6. Define a simple route to trigger sending an SMS
// Access this via GET http://localhost:3000/send-test-sms in your browser or curl
fastify.get('/send-test-sms', async (request, reply) => {
const fromNumber = process.env.VONAGE_NUMBER;
const toNumber = process.env.YOUR_PHONE_NUMBER;
const textMessage = `Hello from Fastify and Vonage! Test at ${new Date().toLocaleTimeString()}`;
if (!fromNumber || !toNumber) {
fastify.log.error('VONAGE_NUMBER or YOUR_PHONE_NUMBER not set in .env');
return reply.code(500).send({ error: 'Server configuration missing.' });
}
fastify.log.info(`Sending SMS from ${fromNumber} to ${toNumber}`);
try {
const resp = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: toNumber,
from: fromNumber,
text: textMessage,
});
fastify.log.info(`SMS Submitted: Message UUID: ${resp.message_uuid}`);
reply.send({ success: true, message_uuid: resp.message_uuid });
} catch (err) {
fastify.log.error('Error sending SMS:', err);
reply.code(500).send({ success: false, error: 'Failed to send SMS', details: err.message });
}
});
// --- Start the Server ---
// 7. Define Server Start Function
const start = async () => {
try {
const port = process.env.PORT || 3000;
// Listen on all available network interfaces, important for Docker/deployment
await fastify.listen({ port: port, host: '0.0.0.0' });
fastify.log.info(`Server listening on port ${port}. Waiting for webhooks at /webhooks/status`);
fastify.log.info(`Send test SMS via: GET http://localhost:${port}/send-test-sms`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
// 8. Run the Server
start();
Code Explanation:
- Imports: Loads necessary modules (
dotenv
,fastify
,@fastify/formbody
,@vonage/server-sdk
). - Fastify Init: Creates a Fastify instance with logging enabled (
logger: true
). - Vonage Init: Initializes the Vonage SDK using the Application ID (
VONAGE_APPLICATION_ID
) and Private Key path (VONAGE_PRIVATE_KEY_PATH
) from.env
. This authentication method is standard for the Messages API. - Status Webhook Route (
/webhooks/status
):- Defines a
POST
handler matching the URL configured in the Vonage Application. - Logs the entire incoming request body (
request.body
) for debugging. Fastify automatically parsesJSON
payloads.@fastify/formbody
is registered to handle form-encoded data if needed, although status webhooks useJSON
. - Performs basic validation checking for required fields (
message_uuid
,status
). - Extracts key information from the payload. Note the potential
.number
access forto
andfrom
, as the format can vary slightly. - Logs the extracted details.
- Includes a
TODO
placeholder where you would add your application-specific logic (database updates, etc.).
- Defines a
- 200 OK Response: Sends an empty
200 OK
response back to Vonage. This is critical to prevent Vonage from retrying the webhook delivery. - Test SMS Route (
/send-test-sms
):- Provides a simple
GET
endpoint for convenience. Accessing this URL triggers the sending of an SMS message using the credentials from.env
. - Uses
vonage.messages.send()
specifyingchannel: 'sms'
,message_type: 'text'
, and the requiredto
,from
, andtext
. - Logs the
message_uuid
returned by Vonage upon successful submission.
- Provides a simple
- Server Start Function: Encapsulates the server start logic in an
async
function. Usesfastify.listen
withhost: '0.0.0.0'
to make the server accessible externally (necessary forngrok
and deployments). - Run Server: Calls the
start
function to launch the application.
4. Verification and Testing
Let's run the application and test the workflow.
-
Ensure
ngrok
is Running: Verify that yourngrok http 3000
process is still active in its terminal window and that thehttps
Forwarding URL matches the one configured in your Vonage Application's Status URL. -
Start the Fastify Server: In your primary terminal window (in the
fastify-vonage-status
directory), run:node index.js
You should see log output indicating the server is listening on port
3000
. -
Send a Test SMS: Open your web browser or use a tool like
curl
to access the test endpoint:# Using curl curl http://localhost:3000/send-test-sms # Or open in your browser: # http://localhost:3000/send-test-sms
- Check the terminal running the Fastify server. You should see logs indicating the SMS was submitted and its
message_uuid
. - You should receive the SMS on the phone number specified in
YOUR_PHONE_NUMBER
.
- Check the terminal running the Fastify server. You should see logs indicating the SMS was submitted and its
-
Observe the Status Webhook:
- Wait a few seconds (delivery times vary).
- Watch the terminal running the Fastify server (
node index.js
). You should see new log entries starting withStatus Webhook Received. Payload:
. - Examine the logged JSON payload directly following that message. This is the Vonage status object. Look for the
status
field (e.g.,submitted
,delivered
,accepted
). The final status is usuallydelivered
. - Verify the
message_uuid
in the webhook payload matches the one logged when you sent the SMS.
Example Vonage Status Payload (logged by
fastify.log.info(request.body)
):{ ""message_uuid"": ""aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"", ""to"": { ""type"": ""sms"", ""number"": ""15551234567"" }, ""from"": { ""type"": ""sms"", ""number"": ""18885550000"" }, ""timestamp"": ""2024-04-20T18:00:00.000Z"", ""status"": ""delivered"", ""usage"": { ""currency"": ""USD"", ""price"": ""0.0075"" }, ""client_ref"": ""optional-client-ref-if-sent"" }
(Note: You will also see surrounding log lines from Fastify/Pino showing log level, time, etc., but the above JSON is the core
request.body
content) -
Check
ngrok
Interface (Optional): Openhttp://localhost:4040
(or the address shown in thengrok
terminal) in your browser. This web interface shows requests forwarded byngrok
. You can inspect the exactPOST
request sent by Vonage to/webhooks/status
and the200 OK
response sent by your Fastify app.
If you see the status webhook logs in your Fastify terminal, congratulations! You have successfully implemented SMS delivery status handling.
5. Error Handling and Logging
- Fastify Logging: We enabled Fastify's built-in Pino logger (
logger: true
). For production, you might configure log levels (level: 'info'
) and potentially transport logs to a dedicated logging service. - Vonage SDK Errors: The
/send-test-sms
route includes atry...catch
block to handle errors during the sending process (e.g., invalid credentials, network issues). - Webhook Payload Errors: The webhook handler includes basic checks for essential fields (
message_uuid
,status
). Robust error handling would involve more thorough validation (e.g., using JSON Schema validation with Fastify) depending on how critical the data is. - Vonage Retries: Remember, Vonage will retry sending the webhook if it doesn't receive a
2xx
response (like our200 OK
). Ensure your handler always returns200 OK
quickly, even if processing fails internally (log the error and return 200). Handle the processing failure asynchronously if needed.
6. Troubleshooting and Caveats
- Webhook Not Received:
- Verify
ngrok
is running and thehttps
URL is correct in the Vonage Application Status URL setting. - Check your Fastify server is running and listening on the correct port (e.g.,
3000
). - Ensure no firewalls are blocking incoming connections to the port
ngrok
is forwarding to. - Check the Vonage Dashboard Logs (Logs -> Messages) for errors related to webhook delivery attempts.
- Verify
- Incorrect API / Webhook Format:
- Crucially, double-check that Messages API is selected under Settings -> API keys -> SMS settings in the Vonage Dashboard. Using the legacy
SMS API
setting results in a different webhook format and configuration method (via the main Settings page, not the Application).
- Crucially, double-check that Messages API is selected under Settings -> API keys -> SMS settings in the Vonage Dashboard. Using the legacy
- Authentication Errors (Sending):
- Ensure
VONAGE_APPLICATION_ID
andVONAGE_PRIVATE_KEY_PATH
in your.env
are correct and the private key file exists at that path and is readable by the application.
- Ensure
- Parsing Issues:
- The Messages API status webhook should send
application/json
. Fastify handles this automatically. If you suspect issues, logrequest.headers['content-type']
.
- The Messages API status webhook should send
- Delayed or Missing Receipts:
- Delivery receipts (DLRs) depend on downstream carriers providing the information back to Vonage. Delays can occur.
- Not all carriers or regions provide reliable DLRs. Check Vonage's country-specific feature documentation if deliverability in a specific region is critical. Status might remain
submitted
oraccepted
.
- Rate Limiting: If your endpoint receives high traffic, consider adding rate limiting using plugins like
fastify-rate-limit
to prevent abuse.
7. Security Considerations
- Webhook Security: Verifying the authenticity of Vonage webhooks can be complex. While Vonage uses Signed Webhooks (JWT) for inbound messages, the documentation for status messages doesn't explicitly detail a standard signature verification method that applies universally.
- Basic Check (IP Filtering): You could potentially filter requests based on expected Vonage IP ranges, but these can change and are not a robust solution. Check Vonage documentation for current IP ranges if pursuing this.
- Obscurity (Hard-to-Guess URL): Use a long, unpredictable, randomly generated path for your webhook endpoint (e.g.,
/webhooks/status/a7f3b9z2k1x0
). While not true security (security through obscurity), it makes accidental discovery or simple scans less likely. Update this path in both your Fastify route and the Vonage Application settings. - Focus on Sending Security: Ensure your sending credentials (
VONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
) are kept highly secure.
- Secret Management: Use
.env
for local development only. In production, use secure secret management solutions provided by your hosting platform (e.g., AWS Secrets Manager, Google Secret Manager, Heroku Config Vars, Docker secrets). - Input Validation: Sanitize and validate any data extracted from the webhook payload before using it in database queries or other sensitive operations, although the risk is lower as the data originates from Vonage.
8. Deployment
- Obtain a Public URL: Replace
ngrok
with a permanent public URL for your deployed application (e.g., from Heroku, Render, AWS EC2 with a domain, etc.). - Update Vonage Status URL: Go back to your Vonage Application settings and update the Status URL to your production webhook endpoint (e.g.,
https://your-app-domain.com/webhooks/status
). Remember to use your secure, hard-to-guess path if implemented. - Environment Variables: Configure your production environment variables (
VONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
, etc.) securely using your deployment platform's tools. Do not commit your.env
file orprivate.key
directly into your repository if it's public. Consider storing the private key content securely as an environment variable itself, or using a secure file storage mechanism accessible to your production environment. - Deployment Strategy: Choose a suitable deployment method (e.g., Platform as a Service like Heroku/Render, containerization with Docker on services like AWS ECS/Fargate or Google Cloud Run, traditional VM).
- Process Management: Use a process manager like
pm2
in production to handle Node.js application lifecycle (restarts, clustering). - Logging: Configure production logging to capture output effectively (e.g., sending logs to a centralized service like Datadog, Logtail, or CloudWatch).
Conclusion
You have now successfully built a Node.js application using Fastify to receive and log SMS delivery status updates from the Vonage Messages API. This provides crucial visibility into message deliverability, enabling you to build more robust and reliable communication features.
From here, you can extend the application by:
- Storing the status updates in a database alongside the original message details.
- Building dashboards to visualize delivery rates.
- Triggering alerts or alternative actions for failed messages.
- Implementing the
/webhooks/inbound
endpoint to receive SMS replies.
Remember to consult the official Vonage Messages API documentation for detailed information on status codes and payload formats.