Frequently Asked Questions
Use Fastify's post route to create a webhook endpoint (e.g., /webhooks/inbound-sms) that listens for incoming POST requests from Vonage. Make sure to register the @fastify/formbody plugin to parse the incoming x-www-form-urlencoded data sent by the Vonage webhook. Acknowledge successful receipt by responding with a 200 OK status.
The Vonage Messages API provides a unified interface for sending and receiving messages across various channels, including SMS. It handles webhook triggers for incoming messages and allows for streamlined communication within applications.
Webhooks provide a real-time, event-driven mechanism for delivering incoming SMS messages to your application. This eliminates the need for inefficient polling and allows your application to react to messages instantly.
Always verify webhook signatures in production to ensure the requests originate from Vonage and haven't been tampered with. This is a crucial security practice to prevent unauthorized access to your application data and logic.
Yes, you can use other Node.js frameworks like Express.js, NestJS, or Koa. The core logic of setting up a webhook endpoint remains the same, but you'll adapt the code to your framework's routing and request handling mechanisms.
Run ngrok http <your_port>, where <your_port> is your application's port (e.g., 3000). Copy the HTTPS forwarding URL generated by ngrok and paste it as your Inbound URL in the Vonage application dashboard. Remember, this is temporary, and you need a stable URL in production.
The .env file stores environment variables, such as API keys and secrets, separate from your codebase. This enhances security and allows for easy configuration across different environments (development, staging, production). Never commit this file to version control.
Vonage typically reassembles long, concatenated SMS messages before sending them to your webhook. The message text should appear as a single string in the text field. Be aware of potential edge cases related to carrier concatenation issues, though rare.
The @vonage/server-sdk simplifies interactions with Vonage APIs, including webhook signature verification. It provides a convenient way to verify signatures and interact with other Vonage services.
A 200 OK response signals to Vonage that your application successfully received the webhook. Without it, Vonage might retry the webhook, leading to duplicate processing of the same message.
Use ngrok to create a publicly accessible URL for your local server. Configure your Vonage application to send webhooks to this ngrok URL. After starting your server and ngrok, send an SMS to your Vonage number, and check your application logs for confirmation.
A table with columns for message_id (unique), sender_msisdn, recipient_number, message_text, received_at timestamp, and optionally the full raw_payload as JSON, is recommended. Ensure proper indexing for efficient querying.
Implement thorough error handling with try...catch blocks. Log errors with context using fastify.log.error. If an error necessitates Vonage retrying the webhook, respond with a 5xx status code; otherwise, acknowledge with a 200 OK even if logging an error that was handled internally.
Use asynchronous processing with message queues (e.g., RabbitMQ) and background workers when your webhook logic involves time-consuming operations like external API calls or complex database interactions. This prevents Vonage webhook timeouts and maintains endpoint responsiveness.
Learn how to receive and process inbound SMS messages using Vonage Messages API, Node.js, and Fastify. This comprehensive tutorial covers webhook setup, JWT signature verification, and production deployment – everything you need to build secure two-way SMS messaging applications.
By the end of this guide, you'll have a production-ready webhook endpoint that securely receives SMS messages sent to your Vonage virtual number, validates their authenticity using JWT tokens, and processes them reliably. This foundation enables you to build chatbots, automated customer support systems, notification handlers, and interactive SMS applications.
What you'll learn: Fastify project setup, Vonage dashboard configuration, webhook security with signature verification, error handling strategies, database integration patterns, and production deployment best practices.
Source: Vonage Messages API Documentation | Fastify Official Documentation
Prerequisites: What You Need Before Starting
Before building your inbound SMS webhook, ensure you have:
Estimated costs: Vonage charges per SMS message received and sent. Inbound SMS typically costs $0.0050-$0.0075 per message (varies by country). Check Vonage pricing for your region.
How Do You Set Up a Fastify Project for Vonage SMS Webhooks?
Fastify offers 5-10% better performance than Express with lower memory overhead – ideal for high-traffic webhook endpoints. Its built-in JSON schema validation and Pino logging make it production-ready out of the box.
Initialize your Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal and create a new directory for your project.
Initialize Node.js Project: Create a
package.jsonfile to manage dependencies and project metadata.(The
-yflag accepts default settings.)Install Dependencies: Install Fastify for the web server, the Vonage SDK for signature verification,
dotenvfor environment variables, and@fastify/formbodyto parsex-www-form-urlencodeddata from webhooks.Create Project Structure: Create the basic files and folders.
index.js: Main application file where your Fastify server code lives.env: Stores sensitive credentials (API keys, secrets). Never commit this file to Git..env-example: Template file showing required environment variables. Commit this file..gitignore: Specifies files and directories that Git should ignoreConfigure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing dependencies and secrets.Define Environment Variables (
.env-exampleand.env): Populate.env-examplewith the variables needed. You'll get these values from the Vonage Dashboard.Now copy
.env-exampleto.envand fill in the actual values in.envas you obtain them.VONAGE_API_KEY,VONAGE_API_SECRET: Found on the main page of your Vonage API Dashboard.VONAGE_APPLICATION_ID: Generated when you create a Vonage Application (covered later).VONAGE_SIGNATURE_SECRET: Found in your Vonage API Settings when you enable signed webhooks (recommended, covered later).PORT: Local port your Fastify server listens on (defaults to 3000).LOG_LEVEL: Controls logging verbosity (e.g.,info,debug,warn,error). The.envvalue overrides the code default.Why
.env? Storing secrets in environment variables is a standard security practice. It separates configuration from code, making it easier to manage different environments (development, staging, production) and preventing accidental exposure of sensitive data in version control.How Do You Implement the Inbound SMS Webhook Endpoint?
This is the core of the application – handling the incoming POST request from Vonage when someone sends an SMS to your virtual number.
Load Environment Variables: At the top of
index.js, load the variables from your.envfile.Initialize Fastify: Import Fastify, create an instance with logging enabled, and register the form body parser.
logger: { level: ... }? Fastify's built-in Pino logger is efficient and provides structured logging, crucial for debugging and monitoring. Setting the level via.envallows environment-specific verbosity.@fastify/formbody? Vonage webhooks send data asx-www-form-urlencoded. This plugin automatically parses it intorequest.body.Define the Inbound Webhook Route: Create a POST route that listens for incoming messages from Vonage. Use
/webhooks/inbound-smsas the path.async (request, reply)? Usingasync/awaitmakes handling asynchronous operations (like database calls you might add later) cleaner.request.body? This object contains the parsed data sent by Vonage, thanks to@fastify/formbody. The exact properties (msisdn,to,text,messageId, etc.) depend on the Vonage API used (Messages API in this case) and the channel. Refer to the Vonage Messages API webhook reference for details.fastify.log.info? Using the instance logger (fastify.log) ensures logs are structured and include request context if configured.reply.status(200).send()? This is crucial. It tells Vonage you successfully received the webhook. Without this, Vonage might consider the delivery failed and retry, leading to duplicate processing.Start the Server: Add the code to start the Fastify server, listening on the configured port.
host: '0.0.0.0'? This makes the server accessible from outside the local machine (necessary for ngrok and deployment).parseInt(port, 10)? Environment variables are strings;listenexpects a number.try...catchandprocess.exit(1)? This ensures graceful error handling during server startup. If the server fails to start (e.g., port already in use), it logs the error and exits cleanly.How Do You Build a Complete API Layer?
For this use case (receiving inbound webhooks), the Fastify route
/webhooks/inbound-smsis the API endpoint that Vonage interacts with. This guide doesn't build a separate API for other clients to call.However, if you extend this application, you might add routes like:
/api/messages: Retrieve stored messages (requires database integration)./api/send-sms: Trigger outbound SMS messages via the Vonage API (requires using the@vonage/server-sdk)./health: A simple endpoint for health checks.Authentication (e.g., API keys, JWT) is crucial for any new endpoints you expose, but the webhook endpoint itself relies on Vonage's mechanism (ideally signed webhooks, see Security section) for authenticity.
How Do You Configure Vonage to Send Webhooks to Your Fastify App?
Configure Vonage to send inbound SMS messages to your running application.
Start Your Local Server: Run your Fastify application.
You should see output indicating the server is listening (e.g.,
{"level":30,"time":...,"pid":...,"hostname":"...","msg":"Server listening at http://127.0.0.1:3000"}).Expose Local Server with ngrok: Open a new terminal window (leave the server running). Start ngrok to forward a public URL to your local port 3000.
ngrok displays output like this:
Copy the
https://<unique-code>.ngrok-free.appURL. This is your temporary public base URL for webhooks during development. Note: This URL changes every time you restart ngrok (unless you have a paid plan with static domains). A stable, permanent URL is required for production (see Deployment section).Configure Vonage Application:
Fastify Inbound SMS Handler).private.keyfile downloads. Save this file securely. Note: This private key is primarily used for generating JWTs for authenticating outbound API calls (e.g., sending SMS) and is not needed for verifying inbound webhook signatures using the signature secret as described in this guide.https://<unique-code>.ngrok-free.app/webhooks/inbound-sms/webhooks/status) if needed:https://<unique-code>.ngrok-free.app/webhooks/inbound-sms(or/webhooks/status).envfile forVONAGE_APPLICATION_ID.Link Vonage Number:
Configure API Settings (Webhook Signatures – Recommended):
.envfile forVONAGE_SIGNATURE_SECRET.Ensure Messages API is Default for SMS (If Sending Later):
Update
.env: Make sure your.envfile now contains the actualVONAGE_API_KEY,VONAGE_API_SECRET,VONAGE_APPLICATION_ID, andVONAGE_SIGNATURE_SECRET. Restart your Node.js server (Ctrl+Cthennode index.js) to load the new values.How Do You Implement Error Handling and Logging?
Robust error handling ensures your application handles unexpected issues gracefully without crashing or losing data.
Structured Logging: Fastify's default logger (Pino) is already structured (JSON). Log meaningful information, especially within
catchblocks.Webhook Acknowledgement: As stressed before, always send a
200 OKunless you specifically want Vonage to retry (e.g., temporary internal failure). Log errors before sending the200 OKif the failure doesn't require a retry.Retry Mechanisms (Vonage Side): Vonage has its own retry mechanism for webhooks that fail (non-2xx response or timeout). You don't typically need to implement retry logic within your webhook handler for the initial reception, but ensure you acknowledge receipt promptly. Retries might be needed if your handler calls other external services.
Log Analysis: In production, use log management tools (like Datadog, Logz.io, ELK stack) to ingest, search, and analyze your structured logs for troubleshooting. Filter by
messageIdormsisdnto track specific message flows.How Do You Create a Database Schema for SMS Messages?
Storing messages is a common requirement. While not implemented in this core guide, here's how to approach it:
Choose a Database: PostgreSQL, MongoDB, MySQL, etc.
Choose an ORM/Driver: Prisma (recommended for type safety and migrations), TypeORM, Sequelize, or native drivers (
pg,mysql2,mongodb).Define Schema: Create a table/collection for messages.
Example (SQL-like pseudo-schema):
Entity Relationship Diagram (ERD): For this simple case, it's just one table. More complex apps might link messages to users, conversations, etc.
Implement Data Access: Create functions (e.g.,
saveInboundMessage(messageData)) using your chosen ORM/driver to insert data into the database within your webhook handler.Migrations: Use the migration tool provided by your ORM (e.g.,
prisma migrate dev) to manage schema changes safely.How Do You Add Security Features to Your Webhook?
Security is paramount for public-facing webhooks.
Webhook Signature Verification (Implement): This is the most crucial security measure for webhooks. Vonage uses JSON Web Token (JWT) Bearer Authorization with HMAC-SHA256 signatures to authenticate webhook requests.
Critical Security Requirements:
payload_hashfield in the JWT claims.Source: Vonage Webhook Signature Security | Validating Inbound Messages
npm install @vonage/server-sdk).WebhookSignatureand initialize necessary components.verifySignaturemethod within your webhook handler.HTTPS: Always use HTTPS for your webhook URLs.
ngrokprovides this automatically for development. In production, your hosting platform or reverse proxy (like Nginx) should handle SSL/TLS termination.Input Sanitization: If you process the
textcontent further (e.g., display it in a web UI, use it in database queries), sanitize it to prevent Cross-Site Scripting (XSS) or SQL Injection attacks. Libraries likeDOMPurify(for HTML context) or parameterized queries (for databases) are essential. For simply logging, sanitization is less critical but still good practice if logs might be viewed in sensitive contexts.Rate Limiting: Protect your endpoint from abuse or accidental loops by implementing rate limiting.
Adjust
maxandtimeWindowbased on expected legitimate traffic from Vonage's IP ranges.Disable Unnecessary HTTP Methods: Your webhook only needs POST. While Fastify defaults to 404 for undefined routes/methods, explicitly ensuring only POST is handled for
/webhooks/inbound-smsis good practice (which ourfastify.postdefinition inherently does).How Do You Handle Special Cases in SMS Processing?
textfield in the webhook payload should be decoded correctly by Vonage into a standard string format (usually UTF-8).concat-ref,concat-total,concat-partwithin themessageobject in some API versions). Your application usually receives the completetextafter reassembly by Vonage, but be aware that carrier reassembly issues can occasionally occur (though rare).timestampfield) are typically in UTC (ISO 8601 format). Store dates in UTC in your database and convert to the user's local time zone only when displaying or processing based on local time.How Do You Optimize Fastify Performance for High Traffic?
For a simple inbound webhook logger, performance is unlikely to be an issue with Fastify. However, if your handler performs complex operations:
200 OK) immediately and perform the heavy lifting asynchronously. Use a message queue (like RabbitMQ, Redis Streams, Kafka, BullMQ) and background workers. This prevents Vonage webhook timeouts and makes your endpoint more resilient.msisdn), implement caching using Redis or Memcached to reduce database load. Use a library like@fastify/caching.vonage_message_id,sender_msisdn) are indexed appropriately.k6,artillery, orautocannonto simulate high webhook volume and identify bottlenecks before going to production. Test your asynchronous processing pipeline as well.How Do You Add Monitoring and Observability?
Health Checks: Add a simple health check endpoint.
Configure your monitoring system (e.g., Kubernetes liveness/readiness probes, external uptime checker) to hit this endpoint.
Metrics: Use a library like
fastify-metricsto expose application metrics (request latency, counts per route, error rates) in a Prometheus-compatible format. Use Prometheus and Grafana (or a cloud provider's monitoring service like CloudWatch, Datadog) to scrape and visualize these metrics. Create dashboards showing inbound message rate, error rate by status code, processing latency (especially if using async processing).Error Tracking: Integrate with services like Sentry (
@sentry/node,@sentry/fastify) or Datadog APM to capture and aggregate errors automatically, providing stack traces, request context, and environment details. Configure alerts for high error rates or specific critical error types (like signature validation failures).Logging: As emphasized, structured logging (JSON) is key. Ensure logs are shipped to a centralized logging platform (e.g., ELK Stack, Splunk, Datadog Logs, CloudWatch Logs) for aggregation, searching, and analysis. Correlate logs using unique request IDs or the
messageId.What Are Common Troubleshooting Issues and Solutions?
node index.jsserver running without startup errors? Check server logs for crashes./webhooks/inbound-sms) exactly matches your Fastify route definition (fastify.post('/webhooks/inbound-sms', ...)). Case-sensitive!200 OKresponse from your handler within Vonage's timeout window (usually a few seconds)? Any other status code (4xx, 5xx) or a timeout will likely cause Vonage to retry. Check your server logs for errors occurring before thereply.status(200).send()is called. Check for slow synchronous operations blocking the response.VONAGE_SIGNATURE_SECRETin.envexactly matches the secret in Vonage dashboard (no extra spaces/characters).fastify-raw-bodyplugin and passrequest.rawBodytoverifySignature().x-vonage-signatureheader exists and contains valid JWT token format.Frequently Asked Questions
How do I receive inbound SMS with Vonage using Fastify?
Set up a Fastify POST endpoint to receive webhook requests from Vonage when SMS messages arrive. Install @vonage/server-sdk and @fastify/formbody, configure your webhook URL in the Vonage dashboard, link your virtual number to your application, and implement signature verification for security. The webhook receives the message data as form-encoded payload.
What version of Fastify works with Vonage webhooks?
Both Fastify v4 and v5 work with Vonage webhooks. Fastify v5 (latest) requires Node.js 20+ and offers 5–10% performance improvements. Fastify v4 supports Node.js 14+ but reaches end-of-life on June 30, 2025. Use Fastify v5 for new projects to ensure long-term support and better performance.
How do I verify Vonage webhook signatures with JWT?
Use the
verifySignature()method from @vonage/server-sdk. Extract the Authorization header, pass the raw request body and signature secret to the verification function. The JWT contains signature claims with payload hash, timestamp, and expiration (5 minutes). Always verify signatures in production to prevent unauthorized webhook requests and ensure message integrity.Why is my Vonage webhook not receiving messages?
Check five common issues: (1) ngrok or tunnel is running with correct HTTPS URL in Vonage dashboard, (2) your Fastify server is running without errors, (3) virtual number is linked to your application in Vonage dashboard, (4) API settings switched to "Messages API" (not SMS API), and (5) webhook URL path exactly matches your route definition (case-sensitive).
Can I use Fastify v4 with Vonage webhooks?
Yes, Fastify v4 works with Vonage webhooks and supports Node.js 14+. However, Fastify v4 reaches end-of-life on June 30, 2025. Plan to migrate to Fastify v5 before this date to receive security updates and new features. The migration is straightforward – primarily requires updating to Node.js 20+ and adjusting a few API changes.
How do I test Vonage webhooks locally?
Use ngrok to create a public HTTPS tunnel to your local Fastify server. Run
ngrok http 3000, copy the HTTPS forwarding URL, paste it into your Vonage application's inbound message webhook URL field (add/webhooks/inbound-smspath), and send a test SMS to your Vonage number. Check your server logs for the webhook request.What is the minimum secret length for Vonage webhook security?
Use at least 32 bits (4 bytes) for your signature secret to prevent brute-force attacks. Generate secrets using cryptographically secure random generators like
crypto.randomBytes(32).toString('hex')in Node.js. Store secrets in environment variables, never commit them to version control, and rotate secrets periodically following security best practices.How do I handle JWT token expiration errors?
Vonage JWT signatures expire 5 minutes after issuance. If verification fails with expiration errors, check your server's time synchronization using Network Time Protocol (NTP). Clock skew between your server and Vonage's servers causes premature expiration. Use
ntpdateor similar tools to sync your server time, and monitor time drift in production environments.Should I respond to webhooks synchronously or asynchronously?
Respond with 200 OK immediately (synchronously) and process SMS messages asynchronously using a job queue. Vonage expects responses within a few seconds – timeouts trigger retries. For complex processing (database writes, external API calls, business logic), acknowledge receipt immediately, then process in background workers. This prevents duplicate webhook deliveries and improves reliability.
How do I prevent duplicate webhook processing?
Check for duplicate
messageIdvalues before processing. Store processed message IDs in your database with a unique constraint, or use Redis with TTL for temporary deduplication. Vonage may retry webhooks if your server doesn't respond with 200 OK quickly enough, so idempotent processing prevents duplicate actions (duplicate database records, duplicate API calls, duplicate user notifications).What database schema should I use for inbound SMS messages?
Store essential fields:
messageId(unique identifier),from(sender phone number in E.164),to(your Vonage number),text(message content),timestamp(message receive time),channel(SMS/MMS/etc), andprocessedAt(processing timestamp). Add indexes onmessageId(unique),from, andtimestampfor efficient queries. Consider partitioning by date for high-volume applications.How do I optimize Fastify performance for high SMS traffic?
Enable clustering to use all CPU cores (Node.js cluster module or PM2), implement connection pooling for databases, use Redis for caching and rate limiting, enable Fastify's schema validation for faster request parsing, compress responses with @fastify/compress, and monitor with health checks and metrics. Deploy behind a load balancer and scale horizontally for very high traffic.
Related Resources