code examples

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

Build Two-Way SMS Messaging with Infobip and RedwoodJS

Complete guide to integrating Infobip SMS into RedwoodJS applications with webhook support for inbound messages, database storage, and secure two-way communication.

Build Two-Way SMS Messaging with Infobip and RedwoodJS

Integrate Infobip's SMS capabilities into your RedwoodJS application to enable sending outbound messages and receiving inbound messages via webhooks for complete two-way communication.

You'll build a RedwoodJS application that can:

  1. Send SMS messages programmatically using the Infobip Node.js SDK.
  2. Receive incoming SMS messages sent to your Infobip number via a RedwoodJS API function configured as a webhook endpoint.
  3. Store message history (both inbound and outbound) in a database using Prisma.

This setup enables applications to interact with users via SMS for notifications, alerts, verification, or conversational features, while leveraging the robust structure and developer experience of RedwoodJS.

Technologies used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the Jamstack. Provides structure for API (GraphQL/REST), services, and database interaction (Prisma).
  • Node.js: The runtime environment for RedwoodJS's backend API.
  • Infobip: A cloud communications platform providing SMS API services with global carrier connectivity.
  • Prisma: A next-generation ORM used by RedwoodJS for database modeling and access.
  • PostgreSQL (or other Prisma-compatible DB): For storing message logs.

Prerequisites:

  • Node.js (LTS version recommended) and Yarn installed.
  • An active Infobip account (a free trial account works, but has limitations).
  • Your Infobip API Key and Base URL.
  • A provisioned phone number within your Infobip account capable of sending and receiving SMS.
  • Basic understanding of RedwoodJS concepts (API functions, services, Prisma). Review the official RedwoodJS documentation if you're less familiar.
  • A way to expose your local development server to the internet for webhook testing (e.g., ngrok, cloudflared tunnel).

System architecture:

mermaid
graph LR
    A[User/Client] --> B(RedwoodJS Frontend);
    B --> C{RedwoodJS API};
    C -- Send SMS Request --> D[Infobip Service];
    D -- Use SDK --> E[Infobip API];
    E -- Sends SMS --> F[End User's Phone];
    F -- Sends Reply --> E;
    E -- Inbound Webhook POST --> C;
    C -- Store/Process Message --> G[Database (Prisma)];
    D -- Store Outbound Status --> G;

    style B fill:#f9f,stroke:#333,stroke-width:2px
    style C fill:#ccf,stroke:#333,stroke-width:2px
    style D fill:#ccf,stroke:#333,stroke-width:2px
    style G fill:#9cf,stroke:#333,stroke-width:2px
    style E fill:#f96,stroke:#333,stroke-width:2px

Ensure your target platform supports Mermaid rendering for this diagram.

Final outcome:

You'll have a functional RedwoodJS application capable of sending SMS messages via an API call and automatically receiving and processing inbound SMS replies sent to your Infobip number, storing relevant details in your database.


Set up your RedwoodJS project

Create a new RedwoodJS project and install the necessary dependencies.

  1. Create a new RedwoodJS app: Open your terminal and run:

    bash
    yarn create redwood-app ./redwood-infobip-sms --typescript
    cd redwood-infobip-sms

    This scaffolds a new RedwoodJS project with TypeScript enabled in the redwood-infobip-sms directory.

  2. Install the Infobip Node.js SDK: Navigate to the api directory and install the SDK:

    bash
    cd api
    yarn add @infobip-api/sdk
    cd ..
  3. Configure environment variables: RedwoodJS uses .env files for environment variables. Create a .env file in the project's root directory:

    bash
    touch .env

    Add your Infobip credentials and a secret for webhook validation:

    plaintext
    # .env
    INFOBIP_BASE_URL=<YOUR_INFOBIP_BASE_URL> # e.g., yggdd4.api.infobip.com
    INFOBIP_API_KEY=<YOUR_INFOBIP_API_KEY>
    INFOBIP_WEBHOOK_SECRET=<generate_a_strong_random_secret_string> # Used to verify incoming webhooks
    # Optional: Specify the Infobip number you are sending from if needed globally
    # INFOBIP_SENDER_ID=<YOUR_INFOBIP_PHONE_NUMBER_OR_SENDER_NAME>
    • INFOBIP_BASE_URL / INFOBIP_API_KEY: Find these in your Infobip account dashboard (usually under API Keys or similar).
    • INFOBIP_WEBHOOK_SECRET: Generate a strong, unique random string using a password manager or openssl rand -hex 32. Share this secret with Infobip to verify webhook authenticity.
  4. Initialize your database with Prisma: RedwoodJS uses Prisma. By default, it's configured for PostgreSQL. Change the provider in api/db/schema.prisma if needed (e.g., SQLite for simple testing). Ensure your database connection string is correctly set in the .env file (Redwood creates a DATABASE_URL variable).

    plaintext
    # .env (add this if not present or using a different DB)
    DATABASE_URL="postgresql://user:password@host:port/database"

    This guide proceeds with the default PostgreSQL setup assumption.


Implement outbound SMS functionality

Create a RedwoodJS service to handle Infobip SDK interactions and an API function to expose this functionality.

  1. Define your database schema for messages: Update api/db/schema.prisma to include a model for storing SMS messages:

    prisma
    // api/db/schema.prisma
    
    datasource db {
      provider = "postgresql" // Or your chosen provider
      url      = env("DATABASE_URL")
    }
    
    generator client {
      provider      = "prisma-client-js"
      binaryTargets = "native"
    }
    
    model SmsMessage {
      id                String   @id @default(cuid())
      createdAt         DateTime @default(now())
      updatedAt         DateTime @updatedAt
      direction         String   // "INBOUND" or "OUTBOUND"
      sender            String   // Phone number (E.164 format)
      recipient         String   // Phone number (E.164 format)
      body              String?  // Message content
      status            String?  // Status from Infobip (e.g., PENDING_ACCEPTED, DELIVERED)
      infobipMessageId  String?  @unique // Infobip's ID for the message
      infobipBulkId     String?  // Infobip's ID if part of a bulk send
      processed         Boolean  @default(false) // Flag for inbound processing
      errorMessage      String?  // Store errors if sending/processing failed
    }
  2. Apply database migrations: Run the migration command to apply the schema changes to your database:

    bash
    yarn rw prisma migrate dev --name add-sms-message-model

    This creates the SmsMessage table.

  3. Generate the Infobip service: Use Redwood's generator to create a service file:

    bash
    yarn rw g service infobip --no-crud

    This creates api/src/services/infobip/infobip.ts.

  4. Implement sendSms in the service: Open api/src/services/infobip/infobip.ts and add the logic to send SMS messages using the SDK and store the record.

    typescript
    // api/src/services/infobip/infobip.ts
    import { Infobip, AuthType } from '@infobip-api/sdk'
    import { db } from 'src/lib/db'
    import { logger } from 'src/lib/logger'
    
    // Initialize Infobip client instance
    let infobipClient: Infobip | null = null
    
    const getInfobipClient = () => {
      if (!infobipClient) {
        if (!process.env.INFOBIP_BASE_URL || !process.env.INFOBIP_API_KEY) {
          logger.error('Infobip Base URL or API Key not configured in .env')
          throw new Error('Infobip credentials not configured.')
        }
        // Verify AuthType.ApiKey is the recommended method in current Infobip SDK docs
        infobipClient = new Infobip({
          baseUrl: process.env.INFOBIP_BASE_URL,
          apiKey: process.env.INFOBIP_API_KEY,
          authType: AuthType.ApiKey,
        })
        logger.info('Infobip client initialized.')
      }
      return infobipClient
    }
    
    interface SendSmsArgs {
      to: string // Recipient phone number (E.164 format)
      text: string
      from?: string // Optional: Sender ID or number (defaults to INFOBIP_SENDER_ID or Infobip default)
    }
    
    // Define the structure expected for the GraphQL response
    interface SendSmsResponse {
      success: boolean
      infobipMessageId?: string
      status?: string
      errorMessage?: string
      dbId: string // ID of the created database record
    }
    
    export const sendSms = async ({
      to,
      text,
      from,
    }: SendSmsArgs): Promise<SendSmsResponse> => {
      const client = getInfobipClient()
      const senderId = from || process.env.INFOBIP_SENDER_ID // Use provided 'from', fallback to env var
    
      logger.info({ recipient: to, sender: senderId }, `Attempting to send SMS`)
    
      let dbRecord // To store the DB record ID even on failure
    
      try {
        // --- IMPORTANT: Verify SDK Method and Payload ---
        // Ensure `client.channels.sms.send` and its payload structure ({ messages: [...] })
        // match the current official Infobip Node.js SDK documentation.
        // API details can change between SDK versions.
        // ---
        const infobipResponse = await client.channels.sms.send({
          messages: [
            {
              destinations: [{ to }],
              from: senderId, // Optional: Can be configured in Infobip portal too
              text,
            },
          ],
        })
    
        logger.info(
          { response: infobipResponse.data },
          'Infobip SMS send response received'
        )
    
        // --- IMPORTANT: Verify Response Structure ---
        // The parsing logic below (`infobipResponse.data.messages?.[0]`) assumes a specific
        // structure for the success response. Verify this against the actual API response
        // documented by Infobip for the SMS send endpoint.
        // ---
        const messageResult = infobipResponse.data.messages?.[0]
    
        // Store successful send attempt in DB
        dbRecord = await db.smsMessage.create({
          data: {
            direction: 'OUTBOUND',
            sender: senderId || 'UNKNOWN', // Best effort sender ID
            recipient: to,
            body: text,
            status: messageResult?.status?.name || 'UNKNOWN_STATUS',
            infobipMessageId: messageResult?.messageId,
            infobipBulkId: infobipResponse.data.bulkId,
            processed: true, // Mark as processed since it's outbound
          },
        })
    
        logger.info({ dbId: dbRecord.id }, 'Outbound SMS record created in DB')
    
        return {
          success: true,
          infobipMessageId: messageResult?.messageId,
          status: messageResult?.status?.name,
          dbId: dbRecord.id,
        }
      } catch (error) {
        logger.error({ error }, 'Error sending SMS via Infobip')
    
        // Log failed attempt in DB
        dbRecord = await db.smsMessage.create({
          data: {
            direction: 'OUTBOUND',
            sender: senderId || 'UNKNOWN',
            recipient: to,
            body: text,
            status: 'SEND_FAILED',
            errorMessage: error.message || JSON.stringify(error),
            processed: true,
          },
        })
        logger.error({ dbId: dbRecord.id }, 'Failed outbound SMS record created')
    
        // Return a structured error object for the API layer
        return {
          success: false,
          status: 'SEND_FAILED',
          errorMessage: error.message || JSON.stringify(error),
          dbId: dbRecord.id, // Return the DB record ID even on failure
        }
      }
    }
    
    // Add the webhook handler function here later
    • Explanation:
      • Initialize the Infobip client lazily using credentials from .env. Notes verify AuthType and the SDK method/payload/response structure against official documentation.
      • The sendSms function takes recipient (to), message text, and an optional from sender ID.
      • It calls the Infobip SDK's channels.sms.send method (verify this against current documentation).
      • It logs the result (success or failure) to the database (SmsMessage table) along with relevant IDs and status information returned by Infobip (verify response structure).
      • If an error occurs during the API call, it logs the failure to the DB and returns a structured error object ({ success: false, … }) instead of throwing, which is suitable for GraphQL resolvers.
      • Proper logging using Redwood's logger is included.
  5. Expose the service via GraphQL API: Create a GraphQL mutation to trigger the sendSms service.

    bash
    yarn rw g sdl sms --no-crud

    This generates api/src/graphql/sms.sdl.ts. Define the mutation:

    typescript
    // api/src/graphql/sms.sdl.ts
    export const schema = gql`
      type SmsSendResponse {
        success: Boolean!
        infobipMessageId: String
        status: String
        errorMessage: String # Included for clarity on failure
        dbId: String!
      }
    
      type Mutation {
        sendSms(to: String!, text: String!, from: String): SmsSendResponse! @requireAuth
        # Consider adding input validation directives if needed
      }
    `
    • Note: The sendSms service function implemented above matches this SDL, returning the SmsSendResponse type including the errorMessage on failure. The @requireAuth directive requires Redwood's authentication. Remove it for testing without auth, but secure it for production.

Build the inbound webhook API

This is the core of two-way messaging – receiving messages from Infobip. Create a standard Redwood API function (REST-like) to act as the webhook receiver.

  1. Generate Webhook API Function:

    bash
    yarn rw g function infobipWebhook --no-auth

    This creates api/src/functions/infobipWebhook.ts. The --no-auth flag makes it publicly accessible, which is necessary for Infobip to reach it. We will add security manually via signature verification.

  2. Implement Webhook Handler Logic: Open api/src/functions/infobipWebhook.ts and implement the handler. This function needs to:

    • Verify the incoming request originated from Infobip (using the shared secret).
    • Parse the incoming SMS data.
    • Store the message in the database.
    • Return an appropriate HTTP status code to Infobip.
    typescript
    // api/src/functions/infobipWebhook.ts
    import type { APIGatewayEvent, Context } from 'aws-lambda'
    import crypto from 'crypto'
    import { db } from 'src/lib/db'
    import { logger } from 'src/lib/logger'
    
    /**
     * Verifies the signature of the incoming webhook request from Infobip.
     * THIS IS CRUCIAL FOR SECURITY.
     *
     * --- VERY IMPORTANT ---
     * The header name ('x-infobip-signature') and hashing algorithm ('sha256') used below
     * are COMMON DEFAULTS but **MUST BE VERIFIED** against the official Infobip documentation
     * for *SMS Inbound Webhooks*. Infobip might use a different header or algorithm (e.g., SHA1).
     * Update this function accordingly based on their specific requirements.
     * Failure to implement this correctly exposes your endpoint to fake requests.
     * --- /VERY IMPORTANT ---
     */
    const verifyInfobipSignature = (event: APIGatewayEvent): boolean => {
      // >>> VERIFY THIS HEADER NAME WITH INFOBIP DOCS <<<
      const signatureHeader = event.headers['x-infobip-signature']
      const secret = process.env.INFOBIP_WEBHOOK_SECRET
    
      if (!signatureHeader || !secret) {
        logger.warn('Missing signature header or webhook secret. Denying request.')
        return false
      }
    
      if (!event.body) {
        logger.warn('Missing request body for signature verification.')
        return false
      }
    
      try {
        // >>> VERIFY THIS HASHING ALGORITHM WITH INFOBIP DOCS <<<
        const hmac = crypto.createHmac('sha256', secret)
        const digest = Buffer.from(
          // The prefix 'sha256=' might also vary based on Infobip's format. Check docs.
          'sha256=' + hmac.update(event.body).digest('hex'),
          'utf8'
        )
        const checksum = Buffer.from(signatureHeader, 'utf8')
    
        if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
          logger.warn('Invalid webhook signature.')
          return false;
        }
        logger.info('Webhook signature verified successfully.')
        return true
      } catch (error) {
        logger.error({ error }, 'Error during webhook signature verification.')
        return false
      }
    }
    
    
    /**
     * Processes inbound SMS messages received via Infobip webhook.
     */
    export const handler = async (event: APIGatewayEvent, _context: Context) => {
      logger.info('Infobip webhook received')
      logger.debug({ headers: event.headers, body: event.body }, 'Webhook details')
    
      // 1. Verify Signature (SECURITY CRITICAL)
      // This check MUST be active in production. Only comment out for specific, isolated debugging.
      const isVerified = verifyInfobipSignature(event)
      if (!isVerified) {
        logger.error('Webhook signature verification failed. Unauthorized.')
        return {
          statusCode: 401, // Unauthorized
          body: JSON.stringify({ error: 'Unauthorized. Invalid signature.' }),
        }
      }
      // If you absolutely MUST bypass temporarily for initial local testing *only*:
      // logger.warn('!!! Webhook signature verification is currently BYPASSED for testing !!!');
    
      // 2. Check HTTP Method
      if (event.httpMethod !== 'POST') {
        logger.warn(`Invalid HTTP method: ${event.httpMethod}`)
        return {
          statusCode: 405, // Method Not Allowed
          headers: { Allow: 'POST' },
          body: JSON.stringify({ error: 'Method Not Allowed' }),
        }
      }
    
      // 3. Parse Request Body
      let payload
      try {
        payload = JSON.parse(event.body || '{}')
        logger.info({ payload }, 'Parsed webhook payload')
    
        // --- VERY IMPORTANT: Verify Payload Structure ---
        // The exact structure of the 'payload' object depends entirely on Infobip's format
        // for inbound SMS webhooks. **Consult the official Infobip documentation.**
        // The structure assumed below (with a 'results' array) is an *example* and may be incorrect.
        // Example *assumed* structure (VERIFY THIS):
        // {
        //   "results": [
        //     {
        //       "messageId": "abc...",
        //       "from": "15551234567",
        //       "to": "15559876543",
        //       "text": "Hello from user!",
        //       "receivedAt": "2025-04-20T10:30:00Z",
        //       "keyword": "...", // etc.
        //       "status": { "groupName": "DELIVERED", "name": "DELIVERED_TO_HANDSET" } // Example status
        //       // ... other fields provided by Infobip
        //     }
        //     // Potentially multiple messages in one webhook call
        //   ],
        //   "messageCount": 1,
        //   "pendingMessageCount": 0
        // }
        // --- /VERY IMPORTANT ---
    
        // Adjust this validation based on the *actual* payload structure from Infobip docs
        if (!payload.results || !Array.isArray(payload.results)) {
          throw new Error('Invalid payload structure: Missing or invalid "results" array.')
        }
    
      } catch (error) {
        logger.error({ error, body: event.body }, 'Failed to parse webhook body')
        return {
          statusCode: 400, // Bad Request
          body: JSON.stringify({ error: 'Invalid JSON payload' }),
        }
      }
    
      // 4. Process Each Message in the Payload
      try {
        // Adjust loop based on the *actual* payload structure (e.g., maybe it's not `payload.results`)
        for (const message of payload.results) {
          // Validate essential fields based on *actual* payload structure from Infobip docs
          if (!message.messageId || !message.from || !message.to || !message.text) {
            logger.warn({ message }, 'Skipping message due to missing essential fields (messageId, from, to, text)')
            continue // Skip this message, process others
          }
    
          // Check if message already processed (using Infobip's ID) - Idempotency check
          const existing = await db.smsMessage.findUnique({
            where: { infobipMessageId: message.messageId },
          })
    
          if (existing) {
            logger.warn({ infobipMessageId: message.messageId }, 'Duplicate message received (based on messageId), skipping.')
            continue
          }
    
          // Store the inbound message
          await db.smsMessage.create({
            data: {
              direction: 'INBOUND',
              sender: message.from, // Ensure format matches (E.164 expected)
              recipient: message.to, // Ensure format matches (E.164 expected)
              body: message.text,
              status: message.status?.name || 'RECEIVED', // Use status if available, verify field name
              infobipMessageId: message.messageId,
              processed: false, // Mark as unprocessed initially for potential async work
              // Add other relevant fields from the *actual* payload if needed (e.g., receivedAt, keyword)
            },
          })
          logger.info({ infobipMessageId: message.messageId }, 'Inbound message stored.')
    
          // --- Application Specific Logic ---
          // This is where you add your own business logic based on the incoming message.
          // Examples:
          // - Auto-reply based on keywords.
          // - Handle STOP/HELP commands for compliance.
          // - Update user state in your application.
          // - Trigger notifications to administrators.
          // - Route message to a support system.
          // Consider moving complex logic to an async job queue if processing takes time.
          // TODO: Add business logic here based on `message.text`, `message.from`, etc.
          // Example: if (message.text.toUpperCase() === 'STOP') { /* handle opt-out logic */ }
          // --- /Application Specific Logic ---
        }
    
        // 5. Respond to Infobip
        // A 2xx status tells Infobip you received the webhook successfully.
        // Failure to respond with 2xx may cause Infobip to retry the webhook.
        return {
          statusCode: 200,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ message: 'Webhook received successfully' }),
        }
    
      } catch (dbError) {
        logger.error({ error: dbError }, 'Database error processing webhook message(s)')
        // Respond with 500 if there's an internal server error during processing
        // Infobip might retry in this case.
        return {
          statusCode: 500,
          body: JSON.stringify({ error: 'Internal Server Error processing message' }),
        }
      }
    }
    • Key Points:
      • Webhook Security (verifyInfobipSignature): This is paramount. The function is now uncommented. Extremely strong warnings were added emphasizing the need to verify the header name, algorithm, and signature format against official Infobip documentation for SMS webhooks.
      • Payload Parsing & Structure: Similar strong warnings were added about verifying the incoming JSON payload structure against official Infobip documentation. The code's reliance on a results array is explicitly called out as an assumption that needs verification.
      • Idempotency: The check for existing messages using infobipMessageId remains crucial for handling potential webhook retries.
      • Database Storage: Creates a record in SmsMessage with direction: 'INBOUND'.
      • Business Logic Placeholder: The // TODO: comment remains, but the surrounding text now better explains its purpose as the integration point for application-specific actions.
      • Response Code: Returning statusCode: 200 acknowledges successful receipt to Infobip. Non-2xx codes signal issues.

Configure Infobip webhook settings

Tell Infobip where to send incoming messages.

  1. Expose your local endpoint: For development, expose your RedwoodJS API endpoint to the public internet. Tools like ngrok or cloudflared can do this.

    • Example using ngrok:
      • Install ngrok.
      • Start your RedwoodJS dev server: yarn rw dev (runs on port 8911 for the API by default).
      • In a new terminal, run: ngrok http 8911
      • Ngrok provides a public HTTPS URL (e.g., https://abcdef123456.ngrok.io). Your webhook endpoint URL will be this base URL plus the Redwood function path: https://abcdef123456.ngrok.io/infobipWebhook
    • Production alternatives: When deploying, platforms like Vercel or Netlify automatically provide stable public HTTPS URLs for your functions. Other managed tunnel services can also be used if required.
  2. Configure the webhook in Infobip:

    • Log in to your Infobip account portal.
    • The Infobip dashboard layout can change, so precise navigation steps might vary. Look for sections related to your SMS Numbers, Apps, or API Settings. Common locations include "Numbers" → select your number → "Forwarding" or "Messaging Settings", or under an "SMS" application configuration → "Inbound Rules" or "Webhook Settings".
    • Find the setting to specify a URL for receiving incoming SMS messages. This might be labeled "Incoming Messages URL", "MO Forwarding URL", or similar.
    • Enter the public URL of your webhook function (e.g., your ngrok URL for testing, or your production URL like https://your-app.com/api/infobipWebhook). Ensure it uses HTTPS.
    • Crucially: Look for a field to enter your Webhook Secret (the value of INFOBIP_WEBHOOK_SECRET from your .env). This allows Infobip to calculate the signature it sends in the header, enabling your verifyInfobipSignature function to work. If Infobip does not provide a specific field for a shared secret for signature generation for SMS webhooks, consult their documentation on how they recommend securing inbound SMS webhooks (e.g., Basic Auth, IP allow-listing, or another mechanism). Do not proceed without confirming Infobip's documented security method.
    • Save the configuration in the Infobip portal.
  3. Test your webhook:

    • With your RedwoodJS server running and ngrok active, send a test SMS from your phone to your Infobip number.
    • Check your application logs for webhook receipt confirmation.
    • Query your database to verify the inbound message was stored:
      bash
      yarn rw prisma studio
    • Navigate to the SmsMessage table and verify the inbound message appears with direction: "INBOUND".
    • If messages aren't arriving, check:
      • Ngrok/tunnel is running and URL is correct in Infobip
      • Webhook signature verification is working (check logs for signature errors)
      • Infobip webhook configuration is saved and active

Implement error handling and logging

  • Error handling:
    • The service and API functions include try…catch blocks. The sendSms service returns structured errors.
    • Errors during SDK calls or database operations are logged using logger.error.
    • The webhook handler returns specific HTTP status codes (400, 401, 405, 500) to indicate different error types to Infobip.
    • Store meaningful error messages in the errorMessage field of the SmsMessage model.
  • Logging:
    • RedwoodJS's built-in Pino logger (logger) is used. Configure log levels in api/src/lib/logger.ts as needed (e.g., level: 'debug' for development).
    • Log key events: initialization, sending attempts, webhook reception, payload parsing, database operations, errors. Include relevant context (IDs, payload snippets if safe).
  • Retry mechanisms:
    • Infobip webhook retries: Infobip typically retries sending webhooks if your endpoint doesn't respond with a 2xx status within a certain timeout. Ensure your webhook handler is reasonably fast and handles errors gracefully (returning correct status codes) to avoid unnecessary retries. The idempotency check (findUnique by infobipMessageId) handles duplicates caused by retries.
    • Outbound send retries: For critical outbound messages, implement retries within your sendSms service (e.g., using libraries like async-retry with exponential backoff) if the initial Infobip API call fails due to transient network issues or temporary Infobip problems (e.g., 5xx errors from their API).

Design your database schema

  • Schema: The SmsMessage model in schema.prisma (defined earlier) provides the structure.
  • Data layer: Prisma Client (db imported from src/lib/db) is used in the service and webhook function for database operations (create, findUnique).
  • Migrations: yarn rw prisma migrate dev handles schema changes. Commit migration files (api/db/migrations/*) to version control.
  • Performance: For high volume, ensure indexes exist on commonly queried fields like infobipMessageId, sender, recipient, and createdAt. Prisma adds indexes on @id and @unique fields automatically. Add @index([sender, createdAt]) if you query by sender frequently.

Secure your SMS integration

  • API key security: Store INFOBIP_API_KEY and INFOBIP_BASE_URL securely in .env and never commit them to version control. Use environment variable management in your deployment environment.
  • Webhook security:
    • Signature verification: This is the most critical security measure (covered earlier). Implement and test it thoroughly based on Infobip's official documentation. Verify the header name, algorithm, and secret configuration.
    • HTTPS: Always use HTTPS for your webhook endpoint URL. Ngrok and production hosting providers typically handle this.
    • IP filtering (allow-listing): If Infobip publishes a list of IPs they send webhooks from, configure your firewall or infrastructure (e.g., AWS WAF, Cloudflare firewall rules) to only allow requests from those IPs as an additional layer of security. This is generally less flexible and robust than signature verification.
  • Input validation:
    • The webhook handler performs basic checks on the payload structure (verify expected structure against documentation).
    • For the GraphQL sendSms mutation, Redwood automatically provides some input type validation. Add more specific validation (e.g., phone number format using a library like libphonenumber-js) in your service if needed.
    • Sanitize any user input used in responses to prevent injection attacks if you build conversational logic.
  • Rate limiting:
    • Protect both your sendSms endpoint and potentially the webhook endpoint from abuse or accidental loops. RedwoodJS doesn't have built-in rate limiting for API functions/GraphQL. Implement it using:
      • Infrastructure-level services (Cloudflare Rate Limiting, API Gateway usage plans).
      • Third-party Node.js libraries implementing algorithms like token bucket or fixed window counter (e.g., rate-limiter-flexible, express-rate-limit adapted for serverless contexts). Search for libraries compatible with your deployment target (e.g., "rate limiting middleware for AWS Lambda" or specific solutions for Vercel/Netlify Edge Functions).
  • Authentication: The example sendSms GraphQL mutation uses @requireAuth. Ensure proper authentication and authorization are enforced on endpoints that trigger actions or expose sensitive data.

Handle special cases and compliance

  • Phone number formatting: Always store and handle phone numbers in E.164 format (e.g., +15551234567). Use libraries like libphonenumber-js for parsing and validation, especially for user-provided input.
  • Message encoding & length: Standard SMS messages have length limits (160 characters for GSM-7, fewer for UCS-2/Unicode). Infobip typically handles concatenation for longer messages, but be aware of billing implications (multiple message segments). Ensure text content doesn't contain unsupported characters or handle encoding explicitly via Infobip API options if needed (SDK defaults are often sufficient).
  • STOP/HELP keywords: Implement handling for standard keywords like STOP (opt-out) and HELP as required by regulations (e.g., TCPA in the US) and carrier guidelines. Your webhook logic should parse inbound messages for these keywords and update user preferences or trigger appropriate responses.
  • Opt-out management: Store opt-out status in your database and check before sending any marketing messages. Respect unsubscribe requests immediately to maintain compliance with telecommunications regulations.
  • Message delivery status tracking: Use Infobip delivery receipt webhooks to track message delivery status and update your database accordingly. This requires configuring an additional webhook endpoint for delivery reports.

Testing your integration

Before deploying to production, thoroughly test both outbound and inbound messaging:

Test outbound SMS

  1. Using GraphQL Playground:

    • Start your RedwoodJS server: yarn rw dev
    • Open GraphQL Playground: http://localhost:8911/graphql
    • Execute the mutation:
      graphql
      mutation {
        sendSms(
          to: "+15551234567"
          text: "Test message from RedwoodJS"
          from: "YourBrand"
        ) {
          success
          infobipMessageId
          status
          errorMessage
          dbId
        }
      }
    • Verify the response includes success: true and check your database for the outbound record.
  2. Check message delivery:

    • Verify the SMS arrives at the recipient's phone.
    • Log into Infobip portal and check SMS logs for delivery status.
    • Query your database to see the stored message record.

Test inbound SMS

  1. Send test message:

    • Send an SMS from your phone to your Infobip number.
    • Monitor your application logs for webhook activity.
  2. Verify database storage:

    • Open Prisma Studio: yarn rw prisma studio
    • Check the SmsMessage table for the inbound message.
    • Verify fields like sender, recipient, body, and infobipMessageId are populated correctly.
  3. Test error scenarios:

    • Invalid webhook signature (temporarily modify your secret to test rejection)
    • Malformed payload (test with invalid JSON if possible)
    • Duplicate messages (send the same webhook payload twice to verify idempotency)

Load testing considerations

For production deployments handling high volume:

  • Test webhook endpoint performance under load using tools like Apache Bench or k6.
  • Verify database can handle concurrent inserts during message spikes.
  • Consider implementing message queue (e.g., Redis, AWS SQS) for async processing of inbound messages if business logic is complex.
  • Monitor memory usage and cold start times in serverless deployments.

Deploying to production

Deploy your RedwoodJS application with SMS capabilities:

Environment configuration

  • Set all environment variables in your hosting platform:
    • INFOBIP_BASE_URL
    • INFOBIP_API_KEY
    • INFOBIP_WEBHOOK_SECRET
    • DATABASE_URL
  • Never commit .env files to version control.
  • Use platform-specific secret management (Vercel Environment Variables, Netlify Environment Variables, AWS Secrets Manager).

Deployment platforms

  • Vercel: Automatic deployment from Git with serverless functions for API.
  • Netlify: Similar to Vercel with built-in function support.
  • AWS: Deploy using AWS Amplify, Elastic Beanstalk, or container services.
  • Render: Full-stack hosting with automatic database provisioning.

Post-deployment checklist

  • ✅ Update Infobip webhook URL to production endpoint (remove ngrok URL).
  • ✅ Verify webhook signature validation is active and working.
  • ✅ Test both outbound and inbound messaging in production.
  • ✅ Set up monitoring and alerting for webhook failures.
  • ✅ Configure database backups and retention policies.
  • ✅ Implement rate limiting on the sendSms endpoint.
  • ✅ Review and comply with SMS regulations for your target regions.
  • ✅ Set up error tracking (Sentry, Rollbar, or similar).

Frequently asked questions

How do I handle multiple Infobip numbers? Add a phoneNumber field to your database schema and pass it as the from parameter when sending. Configure separate webhooks in Infobip for each number, or use a query parameter to differentiate them.

Can I send MMS or multimedia messages? Infobip supports MMS through their API. Update the SDK call to use the MMS endpoint and include media URLs in the payload. Refer to Infobip's MMS documentation for specific requirements.

How do I implement conversation threading? Store a conversationId or threadId in your database to group related messages. Link inbound and outbound messages by phone number and timestamp to create conversation views.

What happens if my webhook endpoint is down? Infobip will retry webhook delivery according to their retry policy (typically exponential backoff). Check Infobip documentation for specific retry behavior and implement the idempotency check to handle duplicates.

How do I handle international phone numbers? Always use E.164 format (+ followed by country code and number). Use the libphonenumber-js library to parse and validate international numbers before storing or sending.

Can I schedule SMS messages for later delivery? Yes, use Infobip's scheduling feature by adding a sendAt parameter to your SMS payload with an ISO 8601 timestamp. Store the scheduled time in your database for reference.

How do I track message costs? Enable cost tracking in your Infobip account and use their reporting API to fetch cost data. Store the bulkId from Infobip responses to correlate messages with billing reports.

Next steps and enhancements

Extend your two-way SMS integration:

  • Conversation UI: Build a React-based interface to display message threads and send replies.
  • Auto-responders: Implement keyword-based automatic responses for common inquiries.
  • Broadcast messaging: Create bulk send functionality for marketing campaigns with opt-out handling.
  • Analytics dashboard: Track message volumes, delivery rates, response times, and conversation metrics.
  • Multi-channel support: Extend to WhatsApp, Viber, or RCS using additional Infobip channels.
  • AI integration: Add sentiment analysis or chatbot capabilities using OpenAI or similar services.
  • Scheduled messages: Build a job queue system for sending time-based notifications.
  • Message templates: Create reusable message templates with variable substitution for personalization.

Frequently Asked Questions

Where can I find my Infobip API key and base URL?

Your Infobip API Key and Base URL can be found in your Infobip account dashboard. Look for a section labeled 'API Keys,' 'API Credentials,' or a similar name. The specific location might vary based on the Infobip portal's layout.

How to send SMS messages with RedwoodJS?

You can send SMS messages within a RedwoodJS application by creating a service that utilizes the Infobip Node.js SDK. This service interacts with Infobip's API to send messages programmatically. You then expose this service functionality via a GraphQL mutation or a custom API function within your RedwoodJS application. Be sure to configure the necessary environment variables with your Infobip account credentials first.

What is Infobip's role in RedwoodJS SMS integration?

Infobip is a cloud communications platform that provides the SMS API used by the RedwoodJS application. The integration uses Infobip's Node.js SDK to interact with their services, allowing you to send and receive SMS messages. You'll need an Infobip account and API key to use this integration.

Why use Prisma for SMS message history in RedwoodJS?

Prisma, a next-generation ORM, is used to store the history of both inbound and outbound SMS messages. Prisma simplifies database interactions, making it easy to create, read, update, and delete records in the database. The schema in `api/db/schema.prisma` needs to include a `SmsMessage` model.

How to set up inbound SMS webhooks in RedwoodJS?

Create a RedwoodJS API function (using the `rw g function` command) to act as your webhook endpoint. This function will receive incoming messages from Infobip. Configure your Infobip account to send inbound SMS webhooks to the public URL of this function. Implement security measures within your RedwoodJS function to verify the origin and integrity of the incoming webhook data.

What are the prerequisites for RedwoodJS Infobip SMS integration?

You need Node.js (LTS recommended), Yarn, an active Infobip account, your Infobip API Key and Base URL, and a provisioned Infobip phone number. Basic RedwoodJS knowledge is also helpful, along with a way to expose your local development server for webhook testing (like ngrok).

How to expose RedwoodJS locally for webhook testing?

Use a tool like ngrok or cloudflared to create a tunnel from your local development environment to a public URL. This makes your RedwoodJS API function accessible to Infobip for webhook delivery during development. Run `ngrok http 8911` if your RedwoodJS API is running locally on port 8911.

How to secure Infobip SMS webhooks in RedwoodJS?

Implement robust signature verification using a strong, randomly generated secret shared with Infobip. Use this secret to verify that incoming webhook requests genuinely originate from Infobip, preventing unauthorized access or data manipulation. Always use HTTPS for webhook URLs.

What is the system architecture for this two-way SMS project?

The client interacts with the RedwoodJS frontend, which communicates with the RedwoodJS API. The API interacts with the Infobip service using the Node.js SDK to send SMS messages to the end user's phone. Inbound messages are sent to the RedwoodJS API via webhooks, then stored in a database using Prisma.

How to handle STOP and HELP keywords in Infobip SMS integration?

Within your inbound webhook handler, parse the incoming message body for keywords like 'STOP' and 'HELP'. Implement the appropriate logic, such as updating user opt-out status or providing help information. This is essential for compliance with regulations like TCPA.

What database model should be used for SMS messages?

The `SmsMessage` model is used to store details like message direction, sender and recipient numbers, message content, status, Infobip message and bulk IDs, and a processing flag. It is recommended to index the `infobipMessageId`, `sender`, `recipient`, and `createdAt` fields for efficient querying.

When should I configure the webhook URL in Infobip?

Configure the webhook URL in your Infobip account after you have a publicly accessible URL for your RedwoodJS API function. This URL is necessary for Infobip to send inbound SMS messages to your application. This is typically done during the integration phase after the local API endpoint is ready.

Can I use a different database provider with this RedwoodJS project?

Yes, RedwoodJS allows you to use other Prisma-compatible databases like SQLite, MySQL, or MongoDB by modifying the `provider` setting in the `api/db/schema.prisma` file and setting the correct connection string in the `.env` file. However, this guide assumes the default PostgreSQL settings.

How to handle errors with the Infobip SMS integration in RedwoodJS?

Implement `try...catch` blocks in both service and API functions to handle potential errors. Log errors using Redwood's logger and store error details in the database. Return appropriate HTTP status codes from the webhook handler and structured error responses from the sendSms service to manage issues effectively.