code examples

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

Developer Guide: Implementing Infobip Delivery Status Callbacks in RedwoodJS

A step-by-step guide on creating a secure RedwoodJS API endpoint to receive, verify, and process Infobip SMS delivery status webhooks.

Track the real-time status of your SMS messages sent via Infobip by implementing webhook callbacks directly within your RedwoodJS application. This guide provides a step-by-step process for creating a secure API endpoint (serverless function) in RedwoodJS to receive, verify, and process delivery status updates pushed from Infobip.

By implementing this, you gain crucial visibility into message delivery, enabling better error handling, analytics, and user experience. You'll know if a message was delivered, failed, or is still pending, allowing your application to react accordingly.

Project Overview and Goals

What We'll Build:

  • A RedwoodJS API function (serverless function) that acts as a webhook endpoint.
  • Logic within the function to securely verify incoming webhook requests from Infobip.
  • A database schema (MessageLog model) using Prisma to store SMS message details and their delivery status.
  • A RedwoodJS service to update the message status in the database based on the received callback data.

Problem Solved:

This implementation solves the problem of ""fire and forget"" SMS sending. Instead of just sending a message and hoping it arrives, this provides a mechanism to track the delivery lifecycle of each message, enabling:

  • Confirmation of successful deliveries.
  • Identification of failed messages and reasons for failure.
  • Real-time updates for dashboards or user interfaces.
  • Automated retries or alternative actions for failed messages.

Technologies Involved:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. We'll use its API side for serverless functions, Prisma integration for the database, and built-in webhook verification tools.
  • Infobip: The SMS communications platform. We assume you're already using Infobip to send SMS and will now configure it to push delivery reports back to our application.
  • Node.js: The runtime environment for RedwoodJS API functions.
  • Prisma: The ORM used by RedwoodJS for database interactions.
  • Webhook Verification: Using RedwoodJS's @redwoodjs/api/webhooks package for security.

System Architecture:

+-------------+ +-----------------+ +-------------------+ +-----------------+ | User Action | ----> | RedwoodJS App | ----> | Infobip API | ----> | End User | | (Sends SMS) | | (Calls Send API)| | (Sends SMS) | | (Receives SMS) | +-------------+ +-----------------+ +-------------------+ +-----------------+ ^ | | (Stores Initial Status) | (Pushes Delivery Report) | v +-----------------+ +-----------------+ +-------------------+ | Database | <---- | RedwoodJS App | <---- | Infobip Webhook | | (MessageLog) | | (Webhook Func) | | | +-----------------+ +-----------------+ +-------------------+ (Verifies & Updates DB)

Prerequisites:

  • Node.js and Yarn (or npm) installed.
  • An existing RedwoodJS project (or willingness to create one).
  • Basic understanding of RedwoodJS concepts (API functions, services, Prisma).
  • An active Infobip account with API access.
  • An SMS message already sent via Infobip for which you want to track the status (you'll need its messageId, which is typically obtained from the response when you initially send the SMS via the Infobip API).
  • A way to expose your local development environment to the internet (e.g., using ngrok - to create a public URL for your local machine, as webhooks require a publicly accessible endpoint) for testing webhooks, or deployment to a hosting provider (like Vercel or Netlify) for production.

Final Outcome:

A RedwoodJS application with a secure endpoint /api/infobipWebhook that listens for POST requests from Infobip, verifies their authenticity using a shared secret, parses the delivery status, and updates a corresponding record in the application's database.


1. Setting up the Project Environment

We assume you have an existing RedwoodJS project. If not, create one:

bash
yarn create redwood-app ./my-infobip-app
cd my-infobip-app

Environment Variables:

Webhooks rely on a shared secret for verification. We'll store this securely in environment variables.

  1. Define the Secret: Create a strong, unique secret string. You can use a password generator.

  2. Add to .env: Add the secret to your project's .env file (create it if it doesn't exist). Never commit .env to version control.

    bash
    # .env
    # Add other variables as needed
    
    # Secret used to verify incoming webhooks from Infobip
    # Ensure this EXACT secret is configured in your Infobip portal
    INFOBIP_WEBHOOK_SECRET="your_super_secret_string_here_replace_me"
  3. Accessing Variables: RedwoodJS automatically loads variables from .env into process.env.

Project Structure:

RedwoodJS provides a clear structure. We'll be working primarily in:

  • api/src/functions/: Where our webhook handler function will live.
  • api/src/services/: Where our database interaction logic will reside.
  • api/db/schema.prisma: Where we define our database model.
  • .env: For environment variables.

No special tooling beyond standard RedwoodJS is needed for this part.


2. Implementing Core Functionality: The Webhook Handler

This function will receive, verify, and process the incoming delivery reports from Infobip.

  1. Generate the Function: Use the RedwoodJS CLI to create the serverless function boilerplate.

    bash
    yarn rw g function infobipWebhook

    This creates api/src/functions/infobipWebhook.ts (or .js).

  2. Implement the Handler: Replace the contents of api/src/functions/infobipWebhook.ts with the following code:

    typescript
    // api/src/functions/infobipWebhook.ts
    import type { APIGatewayEvent, Context } from 'aws-lambda'
    import {
      verifyEvent,
      VerifyOptions,
      WebhookVerificationError,
    } from '@redwoodjs/api/webhooks'
    import { logger } from 'src/lib/logger'
    import { db } from 'src/lib/db' // Import Prisma client
    import { updateMessageStatus } from 'src/services/messageLogs/messageLogs' // Service we'll create later
    
    /**
     * Handles incoming Delivery Report webhooks from Infobip.
     * Verifies the request signature and updates the message status in the database.
     */
    export const handler = async (event: APIGatewayEvent, _context: Context) => {
      // Add context for logging, like a correlation ID if available
      const webhookInfo = { webhook: 'infobipDeliveryReport' }
      const webhookLogger = logger.child({ webhookInfo })
    
      webhookLogger.info('>> Infobip Webhook Received')
      webhookLogger.debug({ headers: event.headers }, 'Received headers')
      // Log raw body *very carefully* only in dev/trace if needed for debugging.
      // Avoid in production due to potential sensitive data/PII exposure.
      // webhookLogger.trace({ body: event.body }, 'Received raw body')
    
      try {
        // --- Verification ---
        // IMPORTANT: Confirm the *exact* header Infobip uses for its signature/secret.
        // Common patterns include 'Authorization', 'X-Infobip-Signature', etc.
        // ** CONSULT INFOBIP DOCUMENTATION FOR DELIVERY REPORTS **
        const signatureHeader = 'Authorization' // Example: ** MUST BE VERIFIED **
    
        // ** CRITICAL SECURITY POINT **
        // Determine the verification method Infobip uses. This example uses
        // 'secretKeyVerifier' which expects the raw secret directly in the header.
        // Infobip might use HMAC (e.g., SHA256), which is generally more secure.
        // If using HMAC, use 'sha256Verifier' or similar and ensure the 'secret'
        // provided to verifyEvent is the key used for hashing, not the header value itself.
        // Using the wrong verifier type will lead to failed verification (401).
        const verifierType = 'secretKeyVerifier' // Example: ** MUST BE VERIFIED **
    
        const options: VerifyOptions = {
          signatureHeader: signatureHeader,
          // Additional options might be needed depending on the verifier type
          // (e.g., 'issuer', 'audience' for JWT, encoding for certain HMACs)
        }
    
        webhookLogger.debug(
          `Attempting verification using header: ${signatureHeader} and verifier: ${verifierType}`
        )
    
        // The 'verifyEvent' function checks the signature/secret.
        // It throws WebhookVerificationError if verification fails.
        await verifyEvent(verifierType, {
          event,
          secret: process.env.INFOBIP_WEBHOOK_SECRET, // Your shared secret
          options,
        })
    
        webhookLogger.info('Webhook verified successfully.')
    
        // --- Processing ---
        // Infobip delivery reports usually come as JSON in the body.
        // ** CONFIRM THE PAYLOAD STRUCTURE FROM INFOBIP DOCUMENTATION **
        let payload
        try {
          payload = JSON.parse(event.body)
          webhookLogger.debug({ payload }, 'Parsed webhook payload')
        } catch (e) {
          webhookLogger.error({ error: e }, 'Failed to parse request body as JSON.')
          return { statusCode: 400, body: 'Bad Request: Invalid JSON payload.' }
        }
    
        // Extract necessary data - ** ADJUST PROPERTY NAMES BASED ON ACTUAL INFOBIP PAYLOAD **
        // This structure is an EXAMPLE and likely needs adjustment.
        const results = payload?.results?.[0] // Infobip often nests results in an array
        if (!results) {
          webhookLogger.warn('Payload structure unexpected. Missing ""results"" array or object.')
          return { statusCode: 400, body: 'Bad Request: Unexpected payload structure.' }
        }
    
        // Extract core fields - ** VERIFY THESE FIELD NAMES WITH INFOBIP DOCS **
        const messageId = results.messageId
        const status = results.status?.groupName // e.g., DELIVERED, FAILED, PENDING, REJECTED
        const statusDescription = results.status?.description
        const errorCode = results.error?.id
        const errorDescription = results.error?.description
    
        if (!messageId || !status) {
          webhookLogger.warn('Missing required fields (messageId, status groupName) in payload results.')
          return { statusCode: 400, body: 'Bad Request: Missing required fields.' }
        }
    
        // --- Update Database ---
        await updateMessageStatus({
          messageId,
          status,
          statusDescription,
          errorCode,
          errorDescription,
        })
    
        webhookLogger.info(`Successfully processed status update for messageId: ${messageId}`)
    
        // --- Respond to Infobip ---
        // Acknowledge receipt successfully. Infobip typically expects a 2xx status.
        return {
          statusCode: 200,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ message: 'Webhook processed successfully.' }),
        }
    
      } catch (error) {
        if (error instanceof WebhookVerificationError) {
          webhookLogger.warn({ error }, 'Webhook verification failed.')
          return { statusCode: 401, body: 'Unauthorized' } // Verification failure
        } else {
          webhookLogger.error({ error }, 'Error processing Infobip webhook.')
          // Avoid exposing internal error details to the caller
          return {
            statusCode: 500,
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ error: 'Internal Server Error' }),
          }
        }
      }
    }

Explanation:

  1. Imports: Standard imports for AWS Lambda types, RedwoodJS webhook utils, logger, DB client, and the service function.
  2. Logging: Contextual logger for traceability. Added stronger warning about logging raw body in production.
  3. Verification:
    • signatureHeader: Crucially important: You must confirm the exact HTTP header Infobip uses (e.g., Authorization, X-Infobip-Signature).
    • verifierType: Equally critical: Determine if Infobip uses a simple secret ('secretKeyVerifier') or a more secure HMAC method (like 'sha256Verifier'). Using HMAC is generally recommended if available.
    • VerifyOptions: Configures the verification based on the header.
    • verifyEvent: Executes the verification using the chosen type, event data, your secret, and options. Throws WebhookVerificationError on failure.
  4. Processing:
    • Parses the event.body as JSON. Confirm the expected Content-Type and structure.
    • Extracts fields (messageId, status, etc.). The structure (payload.results[0]...) is an illustrative example. You must inspect actual Infobip payloads or documentation. The messageId needed for database lookup is extracted here from the body.
  5. Database Update: Calls the updateMessageStatus service.
  6. Response: Returns 200 OK on success.
  7. Error Handling: Catches WebhookVerificationError (returns 401) and other errors (returns 500, logs details internally).

3. Building the API Layer

The RedwoodJS function (/api/infobipWebhook) is our API endpoint.

  • Endpoint: POST /api/infobipWebhook (Path might vary slightly based on deployment, e.g., Netlify uses /.netlify/functions/infobipWebhook).
  • Method: POST
  • Authentication: Handled by webhook signature verification (verifyEvent).
  • Request Validation: Basic payload parsing and field presence checks.
  • Request Body: Expected to be JSON (confirm with Infobip). The structure must be verified.
  • Response Body:
    • 200 OK: { "message": "Webhook processed successfully." }
    • 400 Bad Request: Error message for invalid payload/missing fields.
    • 401 Unauthorized: Signature verification failed.
    • 500 Internal Server Error: { "error": "Internal Server Error" }

Testing with curl (requires ngrok or deployment):

This example assumes your webhook is at https://your-ngrok-id.ngrok.io/api/infobipWebhook, uses secretKeyVerifier, expects the secret your_super_secret_string_here_replace_me in the Authorization header, and receives the example JSON payload structure below. You MUST adjust the URL, header, secret, and payload structure based on your actual setup and Infobip's specifications.

bash
# Example Payload - ILLUSTRATIVE ONLY - ADJUST TO MATCH ACTUAL INFOBIP FORMAT
JSON_PAYLOAD='{
  "results": [
    {
      "messageId": "some-infobip-message-id-123",
      "to": "15551234567",
      "status": {
        "groupId": 1,
        "groupName": "DELIVERED",
        "id": 5,
        "name": "DELIVERED_TO_HANDSET",
        "description": "Message delivered to handset"
      },
      "error": {
        "groupId": 0,
        "groupName": "OK",
        "id": 0,
        "name": "NO_ERROR",
        "description": "No Error",
        "permanent": false
      },
      "sentAt": "2024-01-10T10:00:00.000+0000",
      "doneAt": "2024-01-10T10:00:05.000+0000"
    }
  ]
}'

# Test Success
curl -X POST \
  https://your-ngrok-id.ngrok.io/api/infobipWebhook \
  -H "Content-Type: application/json" \
  -H "Authorization: your_super_secret_string_here_replace_me" \
  -d "$JSON_PAYLOAD"

# Expected Success Response (200 OK):
# {"message":"Webhook processed successfully."}

# Test Failure (Incorrect Secret)
curl -X POST \
  https://your-ngrok-id.ngrok.io/api/infobipWebhook \
  -H "Content-Type: application/json" \
  -H "Authorization: wrong_secret" \
  -d "$JSON_PAYLOAD"

# Expected Failure Response (401 Unauthorized):
# Unauthorized

4. Integrating with Infobip

Configure Infobip to send delivery reports to your RedwoodJS webhook endpoint.

  1. Find Webhook Configuration in Infobip: Log in to your Infobip account. Look for settings related to:

    • API Settings
    • Webhook Configuration
    • Delivery Reports (DLR)
    • SMS Product Settings -> Callbacks or Webhooks (The exact location may vary, consult Infobip support or documentation if needed.)
  2. Configure the Endpoint URL:

    • Enter the full, publicly accessible URL of your deployed RedwoodJS function (e.g., https://<your-app-domain>.com/.netlify/functions/infobipWebhook or https://<your-app-domain>.com/api/infobipWebhook).
    • For local testing, use the https:// URL provided by ngrok.
  3. Configure Authentication/Security:

    • Find the security settings for the webhook.
    • Method: Select the method matching your implementation (e.g., ""HTTP Header Authentication"" if using secretKeyVerifier with Authorization, or ""HMAC"" if using an HMAC verifier). This must match your code's verifierType and signatureHeader.
    • Secret/Token/Key: Enter the exact same secret string from your .env file (INFOBIP_WEBHOOK_SECRET).
    • Header Name: If applicable (e.g., for simple header auth), specify the header name used in VerifyOptions (e.g., Authorization). If using HMAC, Infobip often dictates the header (e.g., X-Infobip-Signature) - ensure this matches your signatureHeader setting.
  4. Enable Delivery Reports: Ensure Delivery Reports are enabled for the SMS sending method you are using (e.g., via API).

Environment Variables Recap:

  • INFOBIP_WEBHOOK_SECRET:
    • Purpose: Shared secret for authenticating/verifying webhook requests.
    • Format: Strong, unique string.
    • How to Obtain: Generate it yourself. Ensure the same value is in your application's environment and the Infobip portal configuration.

5. Error Handling, Logging, and Retry Mechanisms

  • Error Handling Strategy:
    • WebhookVerificationError -> 401 Unauthorized.
    • Payload parsing errors / missing required fields -> 400 Bad Request.
    • Other internal errors (database, etc.) -> 500 Internal Server Error.
  • Logging: Uses RedwoodJS logger. Configure LOG_LEVEL appropriately for dev vs. prod. Logs go to console/hosting provider logs.
  • Retry Mechanisms (Infobip Side): Infobip typically retries if your endpoint fails to return a 2xx status quickly.
    • Responses like 401 or 400 usually signal a permanent issue with the request itself, often preventing retries. (This is typical webhook behavior, but confirm Infobip's specific retry logic in their documentation.)
    • 500 errors or timeouts likely trigger retries. Design your updateMessageStatus logic to be idempotent (safe to run multiple times with the same input) using upsert or checks.

6. Creating Database Schema and Data Layer

Store message status information.

  1. Define Prisma Schema: Add/update a model in api/db/schema.prisma.

    prisma
    // api/db/schema.prisma
    
    datasource db {
      provider = ""postgresql"" // Or your chosen database
      url      = env(""DATABASE_URL"")
    }
    
    generator client {
      provider      = ""prisma-client-js""
    }
    
    // Stores logs for messages sent via Infobip
    model MessageLog {
      id        String   @id @default(cuid())
      messageId String   @unique // The ID received from Infobip when sending the SMS
      status    String?  // Status from webhook (e.g., PENDING, DELIVERED, FAILED)
      statusDescription String? // Detailed description from Infobip
      errorCode Int?     // Infobip error code if applicable
      errorDescription String? // Infobip error description
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt // Automatically updated by Prisma
    
      // Consider adding fields populated when the message is initially sent:
      // to        String?
      // from      String?
      // body      String? // Be mindful of PII
      // initialSentAt DateTime?
    }

    Recommendation: Your application logic that sends the SMS should ideally create an initial MessageLog record with the messageId and a status like PENDING or SENT.

  2. Create Migration:

    bash
    yarn rw prisma migrate dev --name add_message_log
  3. Create Service:

    bash
    yarn rw g service messageLogs
  4. Implement Update Logic: Add updateMessageStatus to api/src/services/messageLogs/messageLogs.ts.

    typescript
    // api/src/services/messageLogs/messageLogs.ts
    import type { Prisma } from '@prisma/client'
    import { db } from 'src/lib/db'
    import { logger } from 'src/lib/logger'
    
    interface UpdateMessageStatusArgs {
      messageId: string
      status: string
      statusDescription?: string | null // Allow null from payload
      errorCode?: number | null
      errorDescription?: string | null
    }
    
    export const updateMessageStatus = async ({
      messageId,
      status,
      statusDescription,
      errorCode,
      errorDescription,
    }: UpdateMessageStatusArgs) => {
      logger.debug(
        `Updating status for messageId ${messageId} to ${status}`
      )
    
      try {
        // Use upsert for robustness: Update if exists, create if not.
        // This handles cases where the initial send might not have created a log,
        // although ideally, it should. If creation is guaranteed elsewhere,
        // `update` can be used instead.
        const messageLog = await db.messageLog.upsert({
          where: { messageId: messageId },
          update: {
            status: status,
            statusDescription: statusDescription,
            errorCode: errorCode,
            errorDescription: errorDescription,
            // updatedAt is handled automatically
          },
          create: {
            messageId: messageId,
            status: status,
            statusDescription: statusDescription,
            errorCode: errorCode,
            errorDescription: errorDescription,
            // Add initial values for other fields if creating and applicable
          },
        })
        logger.info(`Database updated for messageId: ${messageId}`)
        return messageLog
      } catch (error) {
        logger.error(
          { error, messageId },
          `Failed to update message status in DB for messageId: ${messageId}`
        )
        // Re-throw to signal failure to the webhook handler
        throw error
      }
    }
    
    // Optional: Add standard service functions if needed for other operations
    // export const messageLogs = () => {
    //   return db.messageLog.findMany()
    // }
    //
    // export const messageLog = ({ id }: Prisma.MessageLogWhereUniqueInput) => {
    //   return db.messageLog.findUnique({
    //     where: { id },
    //   })
    // }

    Explanation:

    • Uses db.messageLog.upsert based on the unique messageId.
    • Updates status fields if the record exists.
    • Creates a new record if it doesn't exist (though creating during the initial send is preferred).
    • Logs errors and re-throws them.

7. Adding Security Features

  • Webhook Signature Verification: Mandatory. Implemented via @redwoodjs/api/webhooks. Ensures authenticity and integrity.
  • HTTPS: Mandatory. Encrypts data in transit. Use https:// URLs. Standard on deployment platforms and ngrok.
  • Input Validation: Basic checks in the handler prevent processing malformed data.
  • Secret Management: Use environment variables (.env, platform settings) for INFOBIP_WEBHOOK_SECRET. Never hardcode secrets. Consider secret rotation policies.
  • Rate Limiting: (Optional) Implement at the infrastructure level (API Gateway, platform features) if expecting very high webhook volume.

8. Handling Special Cases

  • Infobip Status Codes/Groups: Understand Infobip's specific status values (DELIVERED, UNDELIVERABLE, FAILED, etc.) and error codes to build appropriate application logic.
  • Payload Structure Variations: Be prepared for potential differences in payload structure. Test thoroughly and consult Infobip docs.
  • Idempotency: Ensure updateMessageStatus is safe to re-run with the same data (achieved via upsert or status checks) due to potential Infobip retries.
  • Time Zones: Be mindful of timezone information in timestamps (sentAt, doneAt) provided by Infobip.

9. Implementing Performance Optimizations

Generally not needed unless facing very high volume:

  • Database Indexing: Prisma automatically creates a unique index on messageId (@unique), which is essential for upsert/update performance.
  • Asynchronous Processing: For slow downstream tasks triggered by the webhook, offload work to a background job queue (e.g., RedwoodJS Background Jobs) after verification. The handler returns 200 OK quickly.
  • Connection Pooling: Handled by Prisma. Ensure database resources are adequate.

10. Adding Monitoring, Observability, and Analytics

  • Logging: Use platform logging tools (Netlify/Vercel logs, etc.) to monitor function execution and errors.
  • Health Checks: Endpoint responding 200 OK is a basic health indicator.
  • Error Tracking: Integrate services like Sentry or Bugsnag (see RedwoodJS docs/recipes).
  • Metrics & Dashboards: Track webhook counts (total, 2xx, 4xx, 5xx), function latency, and message status distribution (e.g., DELIVERED vs. FAILED) using platform metrics or database queries.
  • Alerting: Set up alerts for high error rates (401, 500), increased FAILED statuses, or function timeouts.

11. Troubleshooting and Caveats

  • Verification Failed (401 Unauthorized):
    • Check INFOBIP_WEBHOOK_SECRET (exact match in env vs. Infobip portal).
    • Check signatureHeader in code vs. what Infobip sends.
    • Check verifierType in code vs. Infobip's configured method (e.g., secretKeyVerifier vs. HMAC/sha256Verifier).
    • For HMAC, ensure request body isn't modified before verification.
  • Infobip Not Sending Webhooks:
    • Check Endpoint URL in Infobip (correct, public, HTTPS).
    • Check Delivery Reports are enabled in Infobip.
    • Check Infobip's webhook delivery logs for errors.
  • Payload Parsing Error (400 Bad Request / Log Error):
    • Verify expected Content-Type (application/json?).
    • Inspect raw event.body (in dev) to confirm structure; adjust parsing logic (payload.results[0]...) accordingly.
  • Database Errors (500 Internal Server Error / Log Error):
    • Check DATABASE_URL and connectivity.
    • Verify Prisma schema matches data.
    • Check database constraints (e.g., messageId uniqueness).
  • Platform Limitations: Be aware of serverless function execution time limits. Use background jobs for long tasks.
  • Infobip Documentation is Key: Details like header names, payload structure, and authentication methods must be confirmed with the official Infobip developer documentation for Delivery Reports. Do not rely solely on the examples here.

12. Deployment and CI/CD

  1. Choose Deployment Provider: Vercel, Netlify, Render, etc.
  2. Configure Environment Variables: Set INFOBIP_WEBHOOK_SECRET, DATABASE_URL in the provider's UI for production.
  3. Deploy: Use yarn rw deploy <provider>.
  4. CI/CD: Usually set up automatically by the provider based on Git pushes.
  5. Database Migrations: Run production migrations (yarn rw prisma migrate deploy) as part of your deployment process.
  6. Rollback: Use provider features to revert to previous deployments if needed.

13. Verification and Testing

  1. Manual Verification:

    • Deploy or use ngrok.
    • Configure Infobip webhook URL and secret.
    • Send an SMS via Infobip, note messageId.
    • (Recommended) Ensure an initial MessageLog record exists (status PENDING).
    • Wait for the webhook.
    • Check application logs for successful receipt, verification, and processing messages. No errors.
    • Check the database: MessageLog record for the messageId should be updated correctly.
  2. Automated Testing (Service Layer):

    • Focus testing on the messageLogs service (updateMessageStatus) using RedwoodJS scenarios to ensure database logic is correct. Mocking signed webhooks directly is complex.
    • Example Service Test (api/src/services/messageLogs/messageLogs.test.ts):
    typescript
    // api/src/services/messageLogs/messageLogs.test.ts
    import { updateMessageStatus } from './messageLogs'
    import { db } from 'src/lib/db' // Uses test database context provided by scenarios
    
    scenario('MessageLogs service', 'updates an existing message log status', async (scenario) => {
      // Assumes a scenario named 'messageLog' defines a basic log record
      const existingLog = scenario.messageLog.one
      const messageId = existingLog.messageId
    
      const updatedLog = await updateMessageStatus({
        messageId: messageId,
        status: 'DELIVERED',
        statusDescription: 'Delivered to handset',
      })
    
      expect(updatedLog.messageId).toEqual(messageId)
      expect(updatedLog.status).toEqual('DELIVERED')
      expect(updatedLog.statusDescription).toEqual('Delivered to handset')
    
      const dbRecord = await db.messageLog.findUnique({ where: { messageId }})
      expect(dbRecord.status).toEqual('DELIVERED')
    })
    
    scenario('MessageLogs service', 'creates a message log if it does not exist (upsert)', async () => {
      const messageId = 'new-message-id-upsert-test'
      // No initial record for this messageId
    
      const newLog = await updateMessageStatus({
         messageId: messageId,
         status: 'FAILED',
         errorCode: 2,
         errorDescription: 'Absent Subscriber'
      })
    
      expect(newLog.messageId).toEqual(messageId)
      expect(newLog.status).toEqual('FAILED')
    
      const dbRecord = await db.messageLog.findUnique({ where: { messageId }})
      expect(dbRecord).not.toBeNull()
      expect(dbRecord.status).toEqual('FAILED')
    })
  3. Verification Checklist:

    • INFOBIP_WEBHOOK_SECRET matches in env/platform and Infobip portal.
    • Correct Endpoint URL configured in Infobip.
    • Correct verification method (verifierType) and signatureHeader used in code match Infobip config.
    • Webhook function deployed and publicly accessible (HTTPS).
    • Test SMS sent.
    • Infobip logs show successful delivery attempt (2xx response).
    • Application logs show successful verification & processing.
    • MessageLog table updated correctly.
    • Tested failure case (e.g., wrong secret -> 401 logged).

14. GitHub Repository

A complete working example including the setup described in this guide can be found here:

(Link to GitHub Repository to be added)


This guide provides a robust foundation for handling Infobip delivery reports in your RedwoodJS application. Remember to always consult the official Infobip documentation for the definitive details regarding their payload structure, signature methods, header names, and status codes, as these are critical for a secure and functional implementation.

Frequently Asked Questions

How to track Infobip SMS delivery status in RedwoodJS?

Implement a webhook callback function in your RedwoodJS API. This function will receive real-time delivery updates from Infobip, allowing you to monitor message status, handle errors, and improve user experience by providing feedback or taking alternative actions.

What is the purpose of Infobip delivery report webhooks?

Infobip webhooks provide real-time updates on the status of your sent SMS messages. This moves beyond "fire and forget" messaging, giving you insights into delivery success, failures, and pending statuses, so your application can react accordingly.

Why does RedwoodJS need webhook verification for Infobip?

Webhook verification is crucial for security. It confirms that incoming requests genuinely originate from Infobip, preventing unauthorized access or malicious data manipulation by verifying signatures using a shared secret.

When should I create the MessageLog record in Prisma?

Ideally, create the `MessageLog` record with a status like `PENDING` when the SMS is initially sent via the Infobip API. The webhook handler then updates this record based on the delivery report. The handler uses `upsert` to create a record if one doesn't already exist, but it's best practice to create initially.

What is the RedwoodJS API endpoint for Infobip webhooks?

The endpoint is typically `/api/infobipWebhook` for a RedwoodJS function named `infobipWebhook`. This path might vary slightly depending on the deployment platform (e.g., Netlify uses `/.netlify/functions/infobipWebhook`), but it's always a POST request.

How to verify Infobip webhook requests in RedwoodJS?

Use the `verifyEvent` function from `@redwoodjs/api/webhooks`. Provide the correct `verifierType` ('secretKeyVerifier' or an HMAC verifier), the shared secret from your `.env` file, and specify the `signatureHeader` used by Infobip.

How to set up Infobip to send delivery reports to my app?

Log into your Infobip account and navigate to the Webhook or Callback settings (often under API or DLR settings). Configure the URL of your RedwoodJS endpoint (public and HTTPS), and set the authentication method (e.g., HTTP Header Authentication or HMAC) and secret, ensuring these match your application's setup.

How to handle webhook verification failures with Infobip?

Verification failures (401 Unauthorized errors) usually indicate an issue with the shared secret or the HTTP headers used for verification. Double-check that the `INFOBIP_WEBHOOK_SECRET` in your app's environment precisely matches the one configured in your Infobip account, and that the header names (`signatureHeader`) align.

What are common troubleshooting steps for Infobip webhooks?

If Infobip webhooks are not working, verify the following: correct endpoint URL, delivery reports enabled in Infobip, proper setup of webhook security (secret, signature method), and log files from both Infobip and your application for clues about the issue. Check Infobip's documentation to confirm their webhook configuration options and payload format.

How to store Infobip delivery status in a database?

Define a `MessageLog` model in your Prisma schema with fields for `messageId`, `status`, error codes, descriptions, timestamps, etc. Create a RedwoodJS service with an `updateMessageStatus` function that uses Prisma to upsert records based on the `messageId` received in the webhook payload.

How to test my Infobip webhook integration?

Deploy your application or use ngrok to expose it publicly. Send test SMS messages through Infobip and observe the logs to verify the webhook function is correctly receiving, verifying, and processing data. Confirm that the database is updating correctly and that your error handling is working.

Can I use ngrok for testing Infobip webhooks locally?

Yes, ngrok creates a public HTTPS URL for your local development environment, enabling you to test webhooks from Infobip during development before deploying your application.

Why does my Infobip webhook handler return a 400 Bad Request?

A 400 Bad Request error usually indicates a problem parsing the incoming JSON payload from Infobip. Double-check Infobip's documentation for the expected structure and ensure your parsing logic extracts data correctly. Log the raw body carefully (in development) to understand the payload format.

How to secure Infobip webhooks in RedwoodJS?

Use webhook signature verification (`@redwoodjs/api/webhooks`), always use HTTPS, validate incoming payload structure, and securely store the shared secret (`INFOBIP_WEBHOOK_SECRET`) in environment variables, never hardcoding it. Rate limiting can be an additional layer of security, preventing abuse.