code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Implementing Plivo SMS Delivery Status Callbacks in Next.js

A guide on building a Next.js API route webhook to securely receive and process Plivo SMS delivery status callbacks, including setup, validation, and deployment.

Reliably tracking the delivery status of SMS messages is crucial for applications that depend on timely communication. When you send an SMS via Plivo, the initial API response only confirms that Plivo accepted the message, not that it reached the recipient's handset. To get real-time updates on message delivery (e.g., delivered, failed, undelivered), you need to implement a webhook endpoint that Plivo can call back.

This guide provides a step-by-step walkthrough for building a robust webhook endpoint using Next.js API Routes to receive and process Plivo's message status callbacks. We will cover project setup, secure handling of Plivo requests, data persistence (optional), error handling, deployment, and verification.

Project Goal: Build a Next.js application with a dedicated API endpoint that securely receives message status updates from Plivo, validates the requests, and optionally stores the status information.

Technologies Used:

  • Next.js: A React framework providing server-side rendering, static site generation, and simplified API route creation. Chosen for its developer experience and ease of deploying serverless functions.
  • Plivo: A cloud communications platform providing SMS and Voice APIs. We'll use its SMS API and webhook features.
  • Plivo Node SDK: Simplifies interaction with the Plivo API, particularly for validating webhook signatures.
  • TypeScript: (Recommended) Adds static typing for improved code quality and maintainability.
  • (Optional) Prisma: A modern database toolkit for Node.js and TypeScript, used here for optionally storing status updates.
  • (Optional) ngrok: A tool to expose local servers to the internet, essential for testing webhooks during development.

System Architecture:

text
+-----------------+      Sends SMS      +------------+      Sends Status      +---------------------+
| Your Application| -----------------> | Plivo API  | -----------------> | Next.js API Route   |
| (e.g., Next.js) |                    +------------+      (Webhook)       | (/api/plivo/status) |
+-----------------+                                                        +----------+----------+
                                                                                      | Processes &
                                                                                      | Validates Status
                                                                                      v
                                                                           +---------------------+
                                                                           | (Optional) Database |
                                                                           | (e.g., PostgreSQL)  |
                                                                           +---------------------+

Prerequisites:

  • Node.js (LTS version recommended) and npm/yarn/pnpm installed.
  • A Plivo account with Auth ID and Auth Token. (Free trial available).
  • A code editor (e.g., VS Code).
  • Basic understanding of Next.js, APIs, and asynchronous JavaScript/TypeScript.
  • (Optional, for local testing) ngrok installed globally (npm install -g ngrok) or available via npx.
  • (Optional, for database persistence) Docker or a running PostgreSQL instance for Prisma.

1. Setting up the Project

Let's initialize a new Next.js project and install necessary dependencies.

  1. Create a Next.js App: Open your terminal and run:

    bash
    npx create-next-app@latest plivo-nextjs-callbacks --typescript --eslint --tailwind --src-dir --app --import-alias '@/*'
    • We use TypeScript (--typescript) for better type safety.
    • --app enables the App Router, which is standard for new Next.js projects and where we'll build our API route.
    • Other flags set up common tools like ESLint and Tailwind CSS (optional but common).
  2. Navigate to Project Directory:

    bash
    cd plivo-nextjs-callbacks
  3. Install Plivo SDK:

    bash
    npm install plivo-node

    This package provides helper functions, most importantly for validating incoming webhook requests from Plivo.

  4. (Optional) Install Prisma for Database Persistence: If you want to store the delivery statuses:

    bash
    npm install prisma --save-dev
    npm install @prisma/client

    Initialize Prisma:

    bash
    npx prisma init --datasource-provider postgresql

    This creates a prisma directory with a schema.prisma file and a .env file for your database connection string.

  5. Configure Environment Variables: Create a file named .env.local in the root of your project. Never commit this file to version control. Add your Plivo credentials and (if using Prisma) your database URL:

    dotenv
    # .env.local
    
    # Plivo Credentials
    # Find these in your Plivo Console: https://console.plivo.com/dashboard/
    PLIVO_AUTH_ID="YOUR_PLIVO_AUTH_ID"
    PLIVO_AUTH_TOKEN="YOUR_PLIVO_AUTH_TOKEN"
    
    # (Optional) Database URL for Prisma
    # Example for local PostgreSQL using default user/password 'postgres'/'postgres'
    # Replace with your actual database connection string
    DATABASE_URL="postgresql://postgres:postgres@localhost:5432/plivo_callbacks?schema=public"
    
    # Base URL for your application (needed for signature validation)
    # For local dev with ngrok, this will be your ngrok URL (e.g., https://random-subdomain.ngrok-free.app)
    # For production, this will be your deployed URL (e.g., https://your-app.vercel.app)
    APP_BASE_URL="http://localhost:3000" # Replace later with ngrok/production URL
    • Purpose: Storing sensitive credentials like API keys and database URLs outside your codebase is crucial for security. .env.local is used by Next.js for local development environment variables.
    • APP_BASE_URL: This is critical for Plivo signature validation, as the validation function needs the exact URL Plivo is sending the request to. We'll update this later for local testing and production.

2. Implementing the Callback API Route

We'll create a Next.js API Route to handle incoming POST requests from Plivo.

  1. Create the API Route File: Inside the src/app/api/ directory, create the following folder structure and file: src/app/api/plivo/status/route.ts

  2. Implement the API Logic: Paste the following code into src/app/api/plivo/status/route.ts:

    typescript
    // src/app/api/plivo/status/route.ts
    
    import { NextRequest, NextResponse } from 'next/server';
    import * as plivo from 'plivo-node';
    // Optional: Import Prisma client if storing data
    // import { PrismaClient } from '@prisma/client';
    
    // Optional: Initialize Prisma Client.
    // Note: For serverless environments, initializing the client directly in the module scope
    // can lead to connection pool exhaustion. Consider using a cached helper function
    // as recommended in Prisma's documentation for serverless environments.
    // See: https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments
    // const prisma = new PrismaClient();
    
    export async function POST(request: NextRequest) {
      console.log('Received request on /api/plivo/status');
    
      // --- 1. Get Required Headers and Base URL ---
      const signature = request.headers.get('X-Plivo-Signature-V3');
      const nonce = request.headers.get('X-Plivo-Signature-V3-Nonce');
      const appBaseUrl = process.env.APP_BASE_URL;
      const plivoAuthId = process.env.PLIVO_AUTH_ID;
      const plivoAuthToken = process.env.PLIVO_AUTH_TOKEN;
    
      // --- 2. Input Validation ---
      if (!signature || !nonce) {
        console.error('Missing Plivo signature headers');
        return NextResponse.json({ error: 'Missing signature headers' }, { status: 400 });
      }
    
      if (!plivoAuthId || !plivoAuthToken) {
        console.error('Plivo Auth ID or Token not configured in environment variables');
        return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
      }
    
      if (!appBaseUrl) {
          console.error('APP_BASE_URL not configured in environment variables');
          return NextResponse.json({ error: 'Server configuration error: APP_BASE_URL missing' }, { status: 500 });
      }
    
      // Construct the full URL Plivo is sending the request to
      const requestUrl = appBaseUrl + '/api/plivo/status';
    
      try {
        // --- 3. Get Request Body (Plivo sends form data) ---
        const formData = await request.formData();
        const bodyParams: Record<string, string> = {};
        formData.forEach((value, key) => {
          if (typeof value === 'string') {
            bodyParams[key] = value;
          }
        });
        console.log('Received body params:', bodyParams);
    
    
        // --- 4. Validate Plivo Signature ---
        // Why: Ensures the request genuinely came from Plivo and wasn't tampered with.
        const isValid = plivo.validateV3Signature(
          requestUrl, // The FULL URL Plivo sends the request to
          nonce,
          signature,
          plivoAuthToken // Use Auth Token for validation
        );
    
        if (!isValid) {
          console.error('Invalid Plivo signature');
          return NextResponse.json({ error: 'Invalid signature' }, { status: 403 }); // Use 403 Forbidden
        }
    
        console.log('Plivo signature validated successfully.');
    
        // --- 5. Process the Status Update ---
        const messageUuid = bodyParams.MessageUUID;
        const status = bodyParams.Status;
        const errorCode = bodyParams.ErrorCode; // Present if status is 'failed' or 'undelivered'
        const fromNumber = bodyParams.From; // Sender ID or source number
        const toNumber = bodyParams.To; // Destination number
    
        console.log(`Processing status for MessageUUID: ${messageUuid}`);
        console.log(`Status: ${status}, ErrorCode: ${errorCode || 'N/A'}`);
    
        // --- (Optional) 6. Store Status in Database ---
        /*
        if (messageUuid && status) {
          try {
            // Ensure prisma client is initialized if using this block
            // const prisma = new PrismaClient(); // Or use cached instance
            const updatedStatus = await prisma.messageStatus.upsert({
              where: { messageUuid: messageUuid },
              update: {
                status: status,
                errorCode: errorCode,
                updatedAt: new Date(),
              },
              create: {
                messageUuid: messageUuid,
                status: status,
                errorCode: errorCode,
                fromNumber: fromNumber, // Store additional context if needed
                toNumber: toNumber,     // Store additional context if needed
                receivedAt: new Date(),
                updatedAt: new Date(),
              },
            });
            console.log(`Successfully saved status for ${messageUuid}: ${status}`);
          } catch (dbError) {
            console.error(`Database error saving status for ${messageUuid}:`, dbError);
            // Decide if this should be a 500 error, or if logging is sufficient.
            // For now, we log but still return 200 to Plivo (e.g., to prevent Plivo retries
            // for potentially transient DB issues, while ensuring the failure is logged for investigation).
            // Consider queuing the update for retry as an alternative strategy.
          }
        } else {
           console.warn('Missing MessageUUID or Status in callback data.');
        }
        */
    
        // --- 7. Respond to Plivo ---
        // Why: Plivo expects a 2xx response to acknowledge receipt.
        // Failure to respond correctly will cause Plivo to retry the callback.
        return NextResponse.json({ message: 'Status received successfully' }, { status: 200 });
    
      } catch (error) {
        console.error('Error processing Plivo callback:', error);
        // Generic error response
        return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
      }
    }
    
    // Add a simple GET handler for basic testing/health check if desired
    export async function GET() {
        return NextResponse.json({ message: 'Plivo status endpoint is active. Use POST for callbacks.' });
    }

    Code Explanation:

    1. Headers & URL: Retrieves the necessary X-Plivo-Signature-V3 and X-Plivo-Signature-V3-Nonce headers sent by Plivo. Crucially, it constructs the requestUrl using the APP_BASE_URL environment variable and the route's path (/api/plivo/status). This exact URL is required for signature validation (e.g., if APP_BASE_URL is https://foo.ngrok.app, the URL used must be https://foo.ngrok.app/api/plivo/status).
    2. Input Validation: Checks if required headers and environment variables are present.
    3. Request Body: Plivo sends callback data as application/x-www-form-urlencoded. We use request.formData() to parse it into a usable JavaScript object (bodyParams).
    4. Signature Validation: This is the core security mechanism. plivo.validateV3Signature uses the full request URL, the nonce (a unique value per request), the signature provided by Plivo, and your Plivo Auth Token to verify the request's authenticity. If validation fails, a 403 Forbidden response is returned.
    5. Process Status: Extracts relevant fields like MessageUUID, Status, and ErrorCode from the validated data.
    6. (Optional) Store Status: If using Prisma, this section demonstrates an upsert operation. It tries to find a record by messageUuid and update its status, or creates a new record if one doesn't exist. Using upsert helps handle potential duplicate callbacks from Plivo gracefully (idempotency). Error handling for the database operation is included, with a note on deciding how to respond to Plivo upon database failure.
    7. Respond to Plivo: Returns a 200 OK JSON response. This is critical – Plivo needs this acknowledgment to know the callback was received successfully. If Plivo receives a non-2xx response or times out, it will retry sending the callback according to its retry policy.

3. (Optional) Creating a Database Schema

If you chose to store status updates, define the schema.

  1. Define Prisma Schema: Open prisma/schema.prisma and add the MessageStatus model:

    prisma
    // prisma/schema.prisma
    
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
    
    model MessageStatus {
      id          String   @id @default(cuid()) // Unique database ID
      messageUuid String   @unique // Plivo's unique message identifier
      status      String   // e.g., 'queued', 'sent', 'delivered', 'failed', 'undelivered'
      errorCode   String?  // Plivo error code if status is 'failed' or 'undelivered'
      fromNumber  String?  // Optional: store sender
      toNumber    String?  // Optional: store receiver
      receivedAt  DateTime @default(now()) // When the first callback was received
      updatedAt   DateTime @updatedAt // When the status was last updated
    
      @@index([status]) // Index status for faster querying
      @@index([updatedAt]) // Index update time
    }
    • messageUuid: Marked as @unique because it's the primary identifier from Plivo.
    • errorCode: Optional (?) as it's only present for certain statuses.
    • Indexes: Added for potentially common query patterns (e.g., finding all failed messages).
  2. Run Database Migration: Apply the schema changes to your database:

    bash
    npx prisma migrate dev --name add_message_status

    This command creates an SQL migration file and applies it to your database, creating the MessageStatus table. It will also generate/update the Prisma Client based on your schema.

  3. Uncomment Prisma Code: Go back to src/app/api/plivo/status/route.ts and uncomment the Prisma-related lines (import, client initialization, and the "Store Status in Database" section). Remember to handle Prisma client instantiation appropriately for your environment (e.g., using a cached helper in serverless).


4. Configuring Plivo

Now, tell Plivo where to send the status updates.

  1. Log in to Plivo Console: Go to https://console.plivo.com/.

  2. Navigate to Messaging -> Applications: https://console.plivo.com/messaging/application/

  3. Create or Edit an Application:

    • Click ""Add New Application"".
    • Give it a recognizable name (e.g., ""Next.js Callbacks App"").
    • Find the Message URL field under ""Messaging Settings"".
    • Crucially, set the ""Method"" dropdown next to Message URL to POST.
    • In the Message URL field, you need to enter the publicly accessible URL of your API route (/api/plivo/status).
      • For Local Development: You'll use ngrok. See Section 5 below.
      • For Production: You'll use your deployed application URL (e.g., https://your-app-name.vercel.app/api/plivo/status).
    • Leave other fields (like Answer URL, Hangup URL) blank unless you are also handling voice calls with this application.
    • Click ""Create Application"".
  4. (Optional) Assign a Plivo Number: If you want Plivo to automatically use this application when receiving messages on a specific Plivo number, go to Phone Numbers -> Your Numbers, select a number, and choose your newly created application from the ""Application"" dropdown. This is not strictly required for outbound message status callbacks but is good practice if you handle both inbound and outbound.

  5. Using the Application When Sending SMS: When sending an outbound SMS using the Plivo API, you need to specify the url parameter in your API request, pointing to your application's Message URL or directly to your webhook endpoint URL. Using an Application is generally cleaner. If you associated a number with the app, messages sent from that number might automatically use the app's settings (check Plivo defaults), but explicitly setting the url parameter when sending is the most reliable way to ensure status callbacks are sent to the correct endpoint.

    Example (Conceptual Node.js sending code):

    javascript
    // Example of sending SMS and specifying the callback URL
    const plivo = require('plivo-node');
    const client = new plivo.Client(process.env.PLIVO_AUTH_ID, process.env.PLIVO_AUTH_TOKEN);
    
    client.messages.create(
      '+14155551212', // Source number (Must be a Plivo number or Alphanumeric Sender ID)
      '+14155551213', // Destination number
      'Hello from Next.js with callbacks!', // Text
      {
        // THIS IS KEY: Point to your callback endpoint
        url: process.env.APP_BASE_URL + '/api/plivo/status',
        method: 'POST' // Ensure method matches your endpoint
      }
    ).then(function(message_created) {
      console.log(message_created);
    }).catch(function(err) {
      console.error(err);
    });

5. Local Development and Testing with ngrok

Plivo needs to reach your development machine to send callbacks. ngrok creates a secure tunnel.

  1. Start Your Next.js Dev Server:

    bash
    npm run dev

    This usually starts the server on http://localhost:3000.

  2. Start ngrok: Open a new terminal window and run:

    bash
    ngrok http 3000
    • Replace 3000 if your Next.js app runs on a different port.
  3. Get the ngrok URL: ngrok will display output similar to this:

    text
    Session Status                online
    Account                       Your Name (Plan: Free)
    Version                       x.x.x
    Region                        United States (us-cal-1)
    Forwarding                    https://<random-subdomain>.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:// URL (e.g., https://<random-subdomain>.ngrok-free.app). This is your temporary public URL.

  4. Update Environment Variable: Open your .env.local file and update APP_BASE_URL with this ngrok HTTPS URL:

    dotenv
    # .env.local
    # ... other vars
    APP_BASE_URL="https://<random-subdomain>.ngrok-free.app" # Use your actual ngrok URL

    Restart your Next.js development server (Ctrl+C and npm run dev) for the change to take effect.

  5. Update Plivo Application: Go back to your Plivo Application settings in the Plivo Console. Paste the full ngrok callback URL into the Message URL field: https://<random-subdomain>.ngrok-free.app/api/plivo/status Ensure the method is POST. Save the application settings.

  6. Test by Sending an SMS: Use the Plivo API (via code like the example in Section 4, the Plivo console, Postman, or curl) to send an SMS from a Plivo number associated with your application or by explicitly setting the url parameter to your ngrok callback URL.

  7. Observe Logs:

    • Watch the terminal running your Next.js app (npm run dev). You should see logs like "Received request...", "Received body params...", "Plivo signature validated...", and potentially "Successfully saved status..." if using the database.
    • Watch the terminal running ngrok. You should see POST /api/plivo/status requests listed with 200 OK responses.

6. Error Handling, Logging, and Retries

  • Error Handling: The provided API route includes basic try...catch blocks. For production, consider more specific error handling:
    • Catch database errors separately from validation errors.
    • Return appropriate HTTP status codes (400 for bad requests, 403 for auth failures, 500 for server errors).
  • Logging: The current console.log is suitable for development. For production:
    • Use a structured logging library (e.g., Pino, Winston) to output logs in JSON format.
    • Include request IDs for tracing.
    • Adjust log levels (e.g., log informational messages in dev, but only warnings/errors in prod).
    • Integrate with log management services (e.g., Datadog, Logtail, Axiom).
  • Plivo Retries: Plivo automatically retries sending callbacks if it doesn't receive a 2xx response within a timeout period (typically 5 seconds). Retries happen with an exponential backoff. This means your endpoint needs to be:
    • Fast: Process the request quickly. Offload heavy tasks (e.g., complex database updates, calling other APIs) to background jobs if necessary.
    • Idempotent: Design your logic (especially database writes) so that processing the same callback multiple times doesn't cause incorrect side effects. The upsert example helps achieve this.

7. Security Considerations

  • Signature Validation: This is the most critical security measure. Always validate the X-Plivo-Signature-V3 header using plivo.validateV3Signature and your Auth Token. Ensure you are using the exact full URL (including https:// and the path /api/plivo/status) that Plivo is configured to call. For example, if APP_BASE_URL is https://foo.ngrok.app, the URL used for validation must be https://foo.ngrok.app/api/plivo/status.
  • Environment Variables: Keep your Plivo Auth ID, Auth Token, and Database URL secure in environment variables. Do not commit them to your repository. Use .env.local for local development and your hosting provider's mechanism (e.g., Vercel Environment Variables) for production.
  • HTTPS: Always use HTTPS for your callback URL. ngrok provides this automatically for local testing, and platforms like Vercel enforce it for deployments.
  • Input Sanitization: While Plivo's data is generally structured, it's good practice to treat all incoming data as potentially untrusted. If you use the callback data in database queries or display it in a UI, ensure proper sanitization or escaping to prevent injection attacks (though Prisma helps with SQL injection).
  • Rate Limiting: While Plivo is unlikely to abuse your endpoint, consider implementing rate limiting on your API route if you expose it publicly for other reasons, or as a general security hardening measure.

8. Handling Different Statuses and Edge Cases

  • Status Types: Plivo sends various statuses: queued, sent, delivered, undelivered, failed. Your application logic might need to react differently based on the status (e.g., notify support for failed messages, retry sending undelivered messages later).
  • Error Codes: When the status is failed or undelivered, the ErrorCode field provides more detail. Consult the Plivo Error Codes documentation to understand the reasons for failure.
  • Duplicate Callbacks: Due to network issues or retry logic, Plivo might occasionally send the same status update more than once. Ensure your endpoint is idempotent (as discussed with upsert).
  • Callback Order: Callbacks might not always arrive in chronological order (e.g., a delivered status might arrive before a sent status in rare network conditions). Rely on the timestamp (updatedAt in the Prisma schema) if strict ordering is critical, or design your logic to handle out-of-order updates gracefully.

9. Performance Optimizations

  • Stateless API Route: Next.js API Routes deployed on platforms like Vercel are often serverless functions. They should be fast and stateless. Avoid heavy computations directly within the request handler.
  • Asynchronous Processing: If handling a callback requires significant work (e.g., calling multiple other services, complex data processing), acknowledge Plivo's request quickly (return 200 OK) and then trigger a background job (using services like Vercel Background Functions, BullMQ, or external queueing systems) to perform the heavy lifting.
  • Database Indexing: If storing statuses, ensure appropriate database indexes are created (like on messageUuid and status) to speed up queries. The Prisma schema example includes basic indexes.
  • Caching: Caching is generally not applicable for the receiving endpoint itself, but might be relevant if you build a separate API or UI to query the stored statuses.

10. Monitoring and Observability

  • Logging: As mentioned, structured logging is key. Monitor logs for errors (signature validation failures, database errors, unexpected statuses).
  • Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) to capture and alert on exceptions within your API route.
  • Platform Metrics: Use your hosting platform's monitoring tools (e.g., Vercel Analytics and Logs) to track invocation counts, execution duration, and error rates for your /api/plivo/status function.
  • Health Checks: The simple GET handler in the example can serve as a basic health check, although monitoring invocation logs and error rates is usually more informative for webhook endpoints.
  • Plivo Logs: Regularly check the Plivo Console Logs (Messaging -> Logs) to see the status of callbacks Plivo attempted to send and any errors reported by Plivo itself.

11. Troubleshooting and Caveats

  • Callbacks Not Received:
    • Check URL: Double-check the Message URL in your Plivo Application settings. Ensure it exactly matches your public endpoint URL (ngrok or production), includes /api/plivo/status, and uses https.
    • Check Method: Ensure the method in Plivo is set to POST.
    • Check ngrok: If testing locally, ensure ngrok is running and hasn't expired. Check the ngrok console for request logs.
    • Check Server Logs: Look for any errors in your Next.js application logs when a callback should have arrived.
    • Firewall: Ensure no firewall is blocking incoming requests to your endpoint (less common with ngrok or standard hosting platforms like Vercel, but possible in self-hosted scenarios).
    • Check Plivo Logs: Look in the Plivo Console for logs related to the message; it might show errors encountered when trying to send the callback.
  • 403 Forbidden / Invalid Signature Errors:
    • Verify APP_BASE_URL: Ensure the APP_BASE_URL environment variable in your Next.js app exactly matches the base URL part of the endpoint configured in Plivo (e.g., https://<random-subdomain>.ngrok-free.app or https://your-app.vercel.app). Remember to include https://.
    • Verify Auth Token: Ensure the PLIVO_AUTH_TOKEN environment variable is correct and matches the Auth Token used in your Plivo account. The validation function uses the Auth Token, not the Auth ID.
    • Restart Server: If you changed environment variables, ensure you restarted your Next.js server.
    • Full URL: Confirm the validation logic uses the full path (process.env.APP_BASE_URL + '/api/plivo/status').
  • 500 Internal Server Error:
    • Check your Next.js server logs for detailed error messages (e.g., database connection issues, coding errors, missing environment variables like DATABASE_URL).
  • 400 Bad Request / Missing Headers:
    • Indicates Plivo might not be sending the expected signature headers, or there's an issue with how your server/proxy handles headers. Check Plivo configuration and any intermediate proxy settings.
  • Data Not Saving to Database:
    • Check Next.js logs for database-specific errors.
    • Verify DATABASE_URL is correct.
    • Ensure migrations have been run (npx prisma migrate dev).
    • Check database permissions.
  • ngrok Free Tier Limitations: Free ngrok tunnels have rate limits and temporary URLs. For sustained testing or production, consider a paid ngrok plan or deploying to a staging environment.

12. Deployment and CI/CD

Deploying a Next.js app with API routes is straightforward on platforms like Vercel or Netlify.

Deploying to Vercel (Example):

  1. Push to Git: Ensure your project is pushed to a Git repository (GitHub, GitLab, Bitbucket). Do not commit .env.local. Use a .gitignore file (Next.js includes a default one).
  2. Import Project in Vercel: Log in to Vercel and import the Git repository. Vercel usually auto-detects Next.js projects.
  3. Configure Environment Variables: In the Vercel project settings (Settings -> Environment Variables), add:
    • PLIVO_AUTH_ID
    • PLIVO_AUTH_TOKEN
    • DATABASE_URL (Use your production database connection string)
    • APP_BASE_URL (Set this to your Vercel production URL, e.g., https://your-project-name.vercel.app) Ensure these are configured for the ""Production"" environment (and optionally ""Preview"" and ""Development"").
  4. Deploy: Trigger a deployment (usually happens automatically on push to the main branch).
  5. Update Plivo Application URL: Once deployed, Vercel provides a production URL. Update the Message URL in your Plivo Application settings to use this production URL (e.g., https://your-project-name.vercel.app/api/plivo/status). Ensure the method is POST.
  6. CI/CD: Vercel automatically sets up CI/CD. Pushing to your main branch will trigger a build and deployment. You can configure preview deployments for other branches.
    • (Optional) Run Migrations: For database changes, you might need to add a build step to your package.json or Vercel build settings to run migrations: ""build"": ""prisma generate && prisma migrate deploy && next build"". Note: Running migrations during the build process requires careful consideration, especially regarding database credentials and permissions. Sometimes manual migration execution or a separate migration service is preferred.

13. Verification and Testing

  1. Manual Verification:
    • Deploy your application (staging or production).
    • Configure the Plivo Application Message URL to point to your deployed endpoint.
    • Send an SMS using the Plivo API (like the example code, or via Plivo's API explorer) ensuring you specify the url parameter pointing to your deployed endpoint.
    • Check your application logs (Vercel Logs or your configured logging service) to confirm the callback was received, validated, and processed (including database saves if applicable).
    • Check the Plivo Console Logs to see the callback status from Plivo's perspective.
  2. Automated Testing:
    • Unit Tests: Write unit tests for the signature validation logic (you might need to mock Plivo's SDK functions or create test vectors). Test the data extraction and processing logic.
    • Integration Tests: Create tests that simulate a POST request to your /api/plivo/status endpoint with sample Plivo payloads (including valid and invalid signatures/data). Assert that your endpoint returns the correct HTTP status codes and responses. If using a database, verify data persistence.
    • End-to-End Tests (Optional/Complex): A full E2E test would involve sending a real SMS via Plivo and verifying the callback is received by a deployed test instance. This is more complex due to the external dependencies and potential costs.

Frequently Asked Questions

How to set up Plivo SMS status callbacks in Next.js?

Create a Next.js API route at `/api/plivo/status` to receive POST requests. Install the `plivo-node` SDK to validate signatures, ensuring requests originate from Plivo. Configure your Plivo application's Message URL to point to this endpoint, selecting POST as the method.

What is the purpose of Plivo's X-Plivo-Signature-V3 header?

The `X-Plivo-Signature-V3` header, along with the nonce, is crucial for verifying the authenticity of incoming webhook requests from Plivo. It ensures that the request genuinely came from Plivo and hasn't been tampered with, preventing security risks.

Why does Plivo send SMS status callbacks?

Plivo sends status callbacks to provide real-time updates on the delivery status of your SMS messages. The initial API response only confirms Plivo accepted the message, not final delivery. Callbacks provide updates such as 'delivered', 'failed', or 'undelivered'.

When should I use ngrok for Plivo callback testing?

Use `ngrok` during local development to create a secure tunnel, allowing Plivo to reach your local server. This lets you test your webhook endpoint before deploying, ensuring it handles callbacks correctly.

Can I store Plivo SMS statuses in a database?

Yes, the article recommends using Prisma, a database toolkit, to optionally store status updates in a database like PostgreSQL. This allows you to maintain a history of message delivery statuses for analysis or reporting.

How to validate Plivo webhook signatures in Next.js?

Use the `plivo.validateV3Signature` function from the `plivo-node` SDK. Provide the full request URL, nonce, signature, and your Plivo Auth Token. Ensure the URL used matches your Plivo application's Message URL exactly, including `https://` and the path.

What is the role of the APP_BASE_URL environment variable?

`APP_BASE_URL` stores the base URL of your application, crucial for constructing the full URL used in signature validation. This variable is essential for Plivo to correctly verify incoming webhook requests, and it changes depending on your environment (local vs. production).

Why does my Next.js Plivo callback API route return a 403 error?

A 403 Forbidden error usually indicates signature validation failure. Double-check your `APP_BASE_URL` and `PLIVO_AUTH_TOKEN` environment variables. Ensure they match your Plivo application settings and that the URL used for validation includes the full path (`/api/plivo/status`).

How to handle Plivo callback retries in Next.js?

Ensure your API route is idempotent, meaning processing the same callback multiple times doesn't have unintended side effects. Using Prisma's `upsert` for database updates can achieve this. Design your logic to handle potential duplicate callbacks gracefully.

What are the different Plivo SMS callback statuses?

Plivo callbacks include statuses like `queued`, `sent`, `delivered`, `undelivered`, and `failed`. Implement logic in your Next.js route to handle each status appropriately, such as notifying support for failed messages or retrying undelivered ones.

When setting up a Plivo application, what HTTP method should the Message URL use?

The Message URL in your Plivo application settings *must* use the `POST` method because Plivo sends message status callbacks as POST requests. Ensure the method is set to POST in the Plivo console to receive delivery updates.

How to troubleshoot Plivo callbacks not being received in Next.js?

Verify the Message URL in your Plivo application is correct and uses HTTPS. Check your Next.js server logs and the Plivo console logs for errors. Ensure `ngrok` is running if testing locally. Confirm no firewalls block incoming requests to your endpoint.

What should the status code of my Next.js API route response to Plivo callbacks be?

Your Next.js API route should return a 2xx status code (ideally 200 OK) to acknowledge successful receipt of the Plivo callback. Failing to return a 2xx response or timing out will cause Plivo to retry sending the callback.

What are Plivo message status error codes?

Error codes provide additional context when a message status is 'failed' or 'undelivered'. Refer to the Plivo documentation for the complete list of error codes. This helps diagnose specific delivery issues and implement targeted handling mechanisms in your Next.js application.