Integrate Infobip WhatsApp Business API with RedwoodJS
Complete guide to integrating Infobip WhatsApp Business API into RedwoodJS. Learn to send messages, handle webhooks, and build production-ready WhatsApp features.
This guide provides a step-by-step walkthrough for integrating the Infobip WhatsApp Business API into your RedwoodJS application. You'll learn how to send messages, handle incoming messages via webhooks, manage templates, and set up a robust foundation for production use.
By the end of this tutorial, you will have a RedwoodJS application capable of:
Sending WhatsApp messages (template and free-form) via the Infobip API
Receiving incoming WhatsApp messages and delivery reports through RedwoodJS functions acting as webhooks
Basic logging of message interactions
This guide solves the common challenge of connecting a modern full-stack framework like RedwoodJS with a powerful communication channel like WhatsApp, leveraging Infobip as an official WhatsApp Business Solution Provider (BSP).
Technologies Used:
RedwoodJS: A full-stack, serverless web application framework built on React, GraphQL, Prisma, and TypeScript. Chosen for its integrated frontend/backend structure, ease of deployment, and developer experience
Infobip: A global cloud communications platform providing APIs for various channels, including WhatsApp. Chosen for its official WhatsApp Business API access, reliability, and feature set
Prisma: Next-generation ORM for Node.js and TypeScript. Used within RedwoodJS for database interaction
GraphQL: Query language for APIs. Used by RedwoodJS for frontend-backend communication
TypeScript: Typed superset of JavaScript. Used throughout RedwoodJS for enhanced code quality and maintainability
Node.js & Yarn: Runtime environment and package manager
System Architecture Overview:
The system involves the user's WhatsApp application interacting with the WhatsApp network. Messages sent by the user are forwarded by WhatsApp through the Infobip platform to a RedwoodJS Function (webhook). This function processes the message, potentially interacts with a service layer and database (Prisma), and acknowledges receipt. To send messages, the RedwoodJS frontend triggers a GraphQL mutation in the RedwoodJS API. This API calls a service layer function, which interacts with the database and then calls the Infobip WhatsApp API. Infobip delivers the message via the WhatsApp network to the user's device. Delivery reports follow a similar path back from Infobip to the RedwoodJS webhook.
Prerequisites:
Node.js (v18 or later recommended; LTS support until April 2025)
A registered WhatsApp Sender number via Infobip (Test senders are available for development)
Basic understanding of RedwoodJS, GraphQL, and TypeScript
ngrok or a similar tunneling service for local webhook development
1. RedwoodJS Project Setup for WhatsApp Integration
Initialize a new RedwoodJS project and configure the necessary environment variables for Infobip.
Create RedwoodJS App:
Open your terminal and run:
Bash
yarn create redwood-app ./redwood-infobip-whatsapp
cd redwood-infobip-whatsapp
Follow the prompts (choose TypeScript).
Infobip Account Setup & Credentials:
Log in to your Infobip account
API Key: Navigate to the API Keys section (often under your account settings or developer tools). Create a new API key if needed. Copy this key securely.
Base URL: Infobip assigns a unique base URL to your account for API requests. Find this URL in the developer documentation or API overview section of your portal. It typically looks like https://<your-unique-id>.api.infobip.com.
WhatsApp Sender: You need a WhatsApp number registered through Infobip. For development, Infobip usually provides a shared test sender number you can activate (often involves sending a specific keyword from your personal WhatsApp to the test number). For production, register your own business number through the Infobip onboarding process, which involves Meta's approval. Note your activated sender number.
Configure Environment Variables:
RedwoodJS uses a .env file for environment variables. Create this file in the project root:
Replace the placeholder values with your actual credentials
The INFOBIP_WEBHOOK_SECRET is a simple shared secret for basic webhook validation. For production, implement HMAC signature validation as described in Infobip's webhook security documentation
Important: Add .env to your .gitignore file to prevent committing secrets. RedwoodJS does this by default, but always verify
Install Dependencies (Optional – HTTP Client):
Node's native fetch is sufficient for this guide. If you prefer axios for complex scenarios:
Create a RedwoodJS service on the API side to encapsulate the logic for interacting with the Infobip API.
Generate Infobip Service:
Bash
yarn rw g service infobip --typescript
This creates api/src/services/infobip/infobip.ts and related test files.
Implement sendTemplateMessage Function:
Sending pre-approved template messages is often the starting point, especially for initiating conversations outside the 24-hour customer service window.
Open api/src/services/infobip/infobip.ts and add the following code:
TypeScript
// api/src/services/infobip/infobip.tsimporttype { Fetch } from'@whatwg-node/fetch'// Import Fetch type if needed, often globally availableimport { logger } from'src/lib/logger'// Redwood's loggerinterfaceInfobipTemplatePlaceholder {
// Define structure based on template needs, e.g., text, location, media, etc.// Example for simple text placeholders in the body:body?: { placeholders: string[] }
// Example for URL button parameter:buttons?: { type: 'URL'; parameter: string }[]
// Add header, etc., as needed by your template
}
interfaceInfobipSendMessagePayload {
messages: {
from: stringto: stringmessageId?: string// Optional: client-generated message IDcontent: {
templateName: stringtemplateData: InfobipTemplatePlaceholderlanguage: string
}
// Optional: Add smsFailover, notifyUrl, etc. here if needed// urlOptions?: { shortenUrl?: boolean; trackClicks?: boolean; trackingUrl?: string; customDomain?: string }
}[]
}
interfaceInfobipSendResponse {
// Define based on expected Infobip response structure// Example:messages?: {
to: stringstatus?: {
groupId?: numbergroupName?: stringid?: numbername?: stringdescription?: string
}
messageId?: string
}[]
bulkId?: string
}
constINFOBIP_API_KEY = process.env.INFOBIP_API_KEYconstINFOBIP_BASE_URL = process.env.INFOBIP_BASE_URLconstINFOBIP_WHATSAPP_SENDER = process.env.INFOBIP_WHATSAPP_SENDER// Helper function for API callsconst callInfobipApi = async <T = any>(
endpoint: string,
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'POST',
body?: Record<string, any>
): Promise<T> => {
if (!INFOBIP_API_KEY || !INFOBIP_BASE_URL) {
thrownewError(
'Infobip API Key or Base URL not configured in environment variables.'
)
}
const url = `${INFOBIP_BASE_URL}${endpoint}`const headers = {
Authorization: `App ${INFOBIP_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
}
logger.debug({ custom: { url, method } }, 'Calling Infobip API')
try {
const response = awaitfetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
logger.debug(
{ custom: { status: response.status } },
'Infobip API Response Status'
)
if (!response.ok) {
const errorBody = await response.text()
logger.error(
{ custom: { status: response.status, errorBody } },
'Infobip API request failed'
)
thrownewError(
`Infobip API Error (${response.status}): ${errorBody}`
)
}
// Handle cases where response might be empty (e.g., 204 No Content)if (response.status === 204) {
return {} as T
}
const responseData = (await response.json()) as T
logger.debug(
{ custom: responseData },
'Infobip API Response Success Data'
)
return responseData
} catch (error) {
logger.error({ error }, 'Error calling Infobip API')
throw error
}
}
/**
* Sends a pre-approved WhatsApp template message via Infobip.
*
* @paramto - Recipient's phone number in E.164 format (e.g., 441134960001)
* @paramtemplateName - Exact name of the approved template
* @paramtemplateData - Object containing placeholders required by the template
* @paramlanguage - Language code of the template (e.g., "en", "en_US")
* @returns Response from the Infobip API
*/exportconst sendTemplateMessage = async ({
to,
templateName,
templateData,
language,
}: {
to: stringtemplateName: stringtemplateData: InfobipTemplatePlaceholderlanguage: string
}): Promise<InfobipSendResponse> => {
if (!INFOBIP_WHATSAPP_SENDER) {
thrownewError('Infobip WhatsApp Sender not configured.')
}
constpayload: InfobipSendMessagePayload = {
messages: [
{
from: INFOBIP_WHATSAPP_SENDER,
to,
content: {
templateName,
templateData,
language,
},
},
],
}
logger.info(
{ custom: { to, templateName } },
'Attempting to send template message'
)
// Infobip WhatsApp API endpoint for template messages// Reference: https://www.infobip.com/docs/api/channels/whatsapp/send-whatsapp-template-messageconst endpoint = '/whatsapp/1/message/template'return callInfobipApi<InfobipSendResponse>(endpoint, 'POST', payload)
}
// Add functions for sending other message types (text, media) later if needed// Remember: free-form messages require an active 24-hour window initiated by the user
Explanation:
Type-safe interfaces (InfobipTemplatePlaceholder, InfobipSendMessagePayload, InfobipSendResponse) are defined based on Infobip's API documentation. Adjust InfobipTemplatePlaceholder based on the actual placeholders your specific templates require (body text, button parameters, header variables, etc.).
Environment variables are read securely using process.env
A reusable callInfobipApi helper function handles constructing the request (URL, headers, authorization, body) and basic error checking/logging. Crucially, it uses the Authorization: App YOUR_API_KEY header format specified by Infobip
sendTemplateMessage constructs the specific payload for the /whatsapp/1/message/template endpoint and calls the helper function
Logging is added using Redwood's built-in logger (src/lib/logger) for debugging
3. Building the API Layer (GraphQL Mutation for WhatsApp)
Expose the sendTemplateMessage functionality through a GraphQL mutation so your frontend (or other services) can trigger it.
Define GraphQL Schema:
Open api/src/graphql/infobip.sdl.ts (create it if it doesn't exist) and define the mutation:
GraphQL
# api/src/graphql/infobip.sdl.ts
export const schema= gql`
# Define types based on your template data structure# This is a flexible example – make it specific!input InfobipTemplateBodyInput {placeholders:[String!]!}input InfobipTemplateButtonInput {type: String!# e.g., "URL"parameter: String!}input InfobipTemplateDataInput {body: InfobipTemplateBodyInput
buttons:[InfobipTemplateButtonInput!]# Add header, etc., if needed}type InfobipSendStatus {groupId: Int
groupName: String
id: Int
name: String
description: String
}type InfobipSentMessageInfo {to: String
status: InfobipSendStatus
messageId: String
}type InfobipSendResponse {messages:[InfobipSentMessageInfo!]bulkId: String
}typeMutation{"""
Sends a WhatsApp template message via Infobip.
Requires appropriate permissions.
"""
sendWhatsAppTemplate(to: String!templateName: String!templateData: InfobipTemplateDataInput!language: String!): InfobipSendResponse @requireAuth# Add appropriate auth directive}
`
Explanation:
Define input types (InfobipTemplateDataInput, etc.) to structure the data passed into the mutation. Make these specific to match the InfobipTemplatePlaceholder interface in your service and the requirements of your actual templates
Define output types (InfobipSendResponse, etc.) to match the expected structure returned by the sendTemplateMessage service function
The sendWhatsAppTemplate mutation takes the necessary arguments
@requireAuth (or a more specific roles-based directive) should be added to protect this mutation. This guide doesn't implement the auth logic itself, but it's crucial for production
Implement Mutation Resolver:
RedwoodJS automatically maps the sendWhatsAppTemplate mutation to the sendTemplateMessage function in the api/src/services/infobip/infobip.ts service because the names match. No extra resolver code is needed for this mapping.
Testing the Mutation:
Start your Redwood development server: yarn rw dev
Open the GraphQL Playground (usually http://localhost:8911/graphql)
Important: The templateName used below ("welcome_multiple_languages") is just an example. Replace this with the exact name of an active, approved template associated with your Infobip sender number. Ensure the templateData matches the placeholders defined in your specific template.
Run a mutation like this (replace placeholders with your actual test number, template name, and data):
GraphQL
mutation SendTestTemplate {
sendWhatsAppTemplate(to:"YOUR_PERSONAL_WHATSAPP_NUMBER_E164"# e.g., 15551234567# !! Replace with YOUR approved template name !!templateName:"YOUR_APPROVED_TEMPLATE_NAME"language:"en"templateData:{# !! Adjust placeholders to match YOUR template !!body:{placeholders:["Developer Guide User"]}# Add button data if your template has URL buttons, e.g.:# buttons: [# { type: "URL", parameter: "some-dynamic-path" }# ]}){
bulkId
messages {
to
messageId
status {
id
name
description
}}}}
Check your personal WhatsApp – you should receive the message shortly if your setup, sender activation, and template details are correct. Check the API console logs for success or error messages.
4. Webhook Integration for Inbound WhatsApp Messages
To receive messages sent to your WhatsApp number or get delivery status updates, Infobip needs to send HTTP requests (webhooks) to your application. Use a RedwoodJS Function for this.
Generate Webhook Function:
Bash
yarn rw g function inboundWebhook --typescript
This creates api/src/functions/inboundWebhook.ts.
Implement Webhook Handler:
Open api/src/functions/inboundWebhook.ts and add the handler logic:
TypeScript
// api/src/functions/inboundWebhook.tsimporttype { APIGatewayEvent, Context } from'aws-lambda'import { logger } from'src/lib/logger'// Optional: Import DB functions if you want to store messages/reports// import { db } from 'src/lib/db'constINFOBIP_WEBHOOK_SECRET = process.env.INFOBIP_WEBHOOK_SECRET/**
* Handler function receives HTTP requests from Infobip for incoming messages
* and delivery reports.
*
* @paramevent - Lambda event object containing request details (headers, body)
* @param_context - Lambda context object (unused here)
*/exportconsthandler = async (event: APIGatewayEvent, _context: Context) => {
logger.info('Inbound webhook received')
logger.debug({ custom: { headers: event.headers, body: event.body } }, 'Webhook Raw Data')
// --- Basic Security Check (Shared Secret) ---// IMPORTANT: This is basic. For production, implement HMAC signature validation.// Infobip provides X-Hub-Signature header for HMAC-SHA256 validation.// Reference: https://www.infobip.com/docs/essentials/webhooks#securing-your-webhooksconst receivedSecret = event.headers['x-webhook-secret'] // Use a custom headerif (!INFOBIP_WEBHOOK_SECRET || receivedSecret !== INFOBIP_WEBHOOK_SECRET) {
logger.warn('Unauthorized webhook access attempt or missing/invalid secret.')
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' }),
headers: { 'Content-Type': 'application/json' },
}
}
// --- End Security Check ---try {
if (!event.body) {
logger.warn('Webhook received with empty body.')
return {
statusCode: 400,
body: JSON.stringify({ error: 'Bad Request: Empty body' }),
headers: { 'Content-Type': 'application/json' },
}
}
const payload = JSON.parse(event.body)
// Infobip sends results in an array, usually containing one item per webhook callconst results = payload.resultsif (!results || !Array.isArray(results) || results.length === 0) {
logger.warn({ custom: payload }, 'Webhook payload missing "results" array.')
return {
statusCode: 400,
body: JSON.stringify({ error: 'Bad Request: Invalid payload structure' }),
headers: { 'Content-Type': 'application/json' },
}
}
// Process each result (usually just one)for (const result of results) {
if (result.error) {
logger.error({ custom: result }, 'Infobip reported an error in webhook payload')
// Handle specific errors if necessarycontinue// Process next result if any
}
// --- Distinguish between Inbound Message and Delivery Report ---// The structure differs significantly. Consult Infobip's documentation// for the exact payload structures for different event types (message received,// delivery reports like SENT, DELIVERED, READ, FAILED, etc.) and adapt this logic.// Example differentiation logic (adapt based on actual payloads you receive):if (result.messageId && result.from && result.to && result.receivedAt && result.message) {
// Likely an INBOUND MESSAGE
logger.info({ custom: { from: result.from, messageId: result.messageId, type: result.message.type } }, 'Processing Inbound Message')
// TODO: Add your logic here// - Parse result.message (text, image, document, location, interactive reply, button, etc.)// - Store in database (e.g., using Prisma: await db.messageLog.create(…))// - Trigger downstream actions (e.g., auto-reply, notify support)
} elseif (result.messageId && result.sentAt && result.status && result.doneAt) {
// Likely a DELIVERY REPORT
logger.info({ custom: { messageId: result.messageId, status: result.status.name } }, 'Processing Delivery Report')
// TODO: Add your logic here// - Update message status in your database// (e.g., await db.messageLog.update({ where: { infobipMessageId: result.messageId }, data: { status: result.status.name } }))
} else {
logger.warn({ custom: result }, 'Unrecognized webhook event type')
}
} // End loop processing results// Acknowledge receipt to Infobipreturn {
statusCode: 200,
body: JSON.stringify({ message: 'Webhook received successfully' }),
headers: { 'Content-Type': 'application/json' },
}
} catch (error) {
logger.error({ error, custom: { body: event.body } }, 'Error processing webhook')
// Return 500 for server errors; Infobip may retryreturn {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
headers: { 'Content-Type': 'application/json' },
}
}
} // End handler
Explanation:
The function receives the event object containing the HTTP request details
Security: It performs a basic check using a shared secret passed in a custom header (x-webhook-secret). This is minimal security. For production, implement HMAC signature validation using Infobip's X-Hub-Signature header. Reference: Infobip Webhook Security
It parses the JSON body, expecting Infobip's standard results array structure
It iterates through the results (usually just one)
It attempts to differentiate between inbound messages and delivery reports based on common fields (from, receivedAt vs. sentAt, status). Consult actual payloads from Infobip and their documentation to adjust this logic accurately
Placeholder comments (// TODO:) indicate where you'd add your application-specific logic (database interaction, notifications, etc.)
It returns a 200 OK status code to Infobip upon successful processing, or appropriate error codes (401, 400, 500). Infobip generally expects a 2xx response to consider the webhook delivered
Expose Webhook Locally with ngrok:
Infobip needs a publicly accessible URL to send webhooks to. During development, ngrok creates a secure tunnel to your local machine.
Start your Redwood app: yarn rw dev (Note the API server port, usually 8911)
In a separate terminal window, run ngrok:
Bash
ngrok http 8911
ngrok will display forwarding URLs (e.g., https://<random-string>.ngrok.io). Copy the https URL.
Configure Webhook in Infobip:
Log in to your Infobip portal
Navigate to the WhatsApp configuration section for your sender number or general webhook settings (the exact location varies – look for "Apps," "WhatsApp," "API Integrations," "Webhooks," "Event Notifications," or similar under the channel settings)
Find the settings for Inbound Messages and Delivery Reports (or a unified webhook setting)
Paste your ngrok HTTPS URL, appending the path to your Redwood function: /inboundWebhook. The full URL will be: https://<random-string>.ngrok.io/api/functions/inboundWebhook
Webhook Security: Look for an option to add custom HTTP headers to the webhook request. Add a header named x-webhook-secret (or your chosen name) with the value set to your INFOBIP_WEBHOOK_SECRET from the .env file. For production, configure HMAC signature validation following Infobip's documentation
Enable the webhook for the event types you need (e.g., "Message Received," "Message Sent," "Message Delivered," "Message Read," "Message Failed")
Save the configuration
Testing the Webhook:
Inbound Message: Send a WhatsApp message from your personal phone to your Infobip Sender number. Observe the terminal running yarn rw dev and the terminal running ngrok. You should see log output from your inboundWebhook function indicating it received and processed the message.
Delivery Report: Use the GraphQL Playground (from Section 3) to send another template message to your personal phone. After the message is delivered (or fails), Infobip should send a delivery report webhook. Check the logs again.
5. Error Handling, Logging, and Retry Mechanisms
Error Handling:
The callInfobipApi helper includes basic try…catch blocks and checks the HTTP response status. Expand this to handle specific Infobip error codes or messages if needed (refer to Infobip API documentation for error details)
The webhook handler also uses try…catch and returns appropriate HTTP status codes (4xx for client errors/bad data, 500 for server errors). Infobip may retry webhooks on receiving 5xx errors or timeouts
Logging:
RedwoodJS's built-in Pino logger (src/lib/logger) is used extensively. Use different log levels (debug, info, warn, error) appropriately
Log critical information like request parameters, API responses (especially errors), and webhook payloads for easier debugging. Avoid logging sensitive data like full API keys in production logs. Configure log levels and destinations (e.g., file, external service) for production environments
Retry Mechanisms (API Calls):
For transient network issues when calling the Infobip API, implement a simple retry strategy within the callInfobipApi function using libraries like async-retry or a manual loop with exponential backoff. Be cautious not to retry excessively for errors that are unlikely to resolve (e.g., invalid API key, malformed request)
Bash
# Example dependency if using async-retry# yarn workspace api add async-retry @types/async-retry
6. Database Schema and Data Layer (Optional but Recommended)
Storing message logs helps with tracking, debugging, and analytics.
Define Prisma Schema:
Open api/db/schema.prisma and add a model:
Prisma
// api/db/schema.prisma
model MessageLog {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
direction String // "INBOUND" or "OUTBOUND"
infobipMessageId String? @unique // The ID returned by Infobip when sending or present in webhooks
bulkId String? // The bulk ID returned by Infobip for batch sends
sender String // E.164 format
recipient String // E.164 format
status String? // e.g., PENDING, SENT, DELIVERED, READ, FAILED, RECEIVED
statusDescription String? // Detailed status text from Infobip
messageType String? // TEXT, TEMPLATE, IMAGE, etc.
templateName String? // If it was a template message
contentPreview String? // A snippet of the message content (be careful with PII)
errorCode Int? // Error code from Infobip if failed
errorMessage String? // Error message from Infobip if failed
webhookPayload Json? // Store the raw webhook payload for debugging (optional)
}
Run Migration:
Bash
yarn rw prisma migrate dev --name add_message_log
Update Services/Functions to Use DB:
Sending: In api/src/services/infobip/infobip.ts (sendTemplateMessage), after a successful API call, create a MessageLog entry
Receiving (Webhook): In api/src/functions/inboundWebhook.ts, inside the loop processing results, create or update MessageLog entries for inbound messages and delivery reports
Frequently Asked Questions (FAQ)
How do I get a WhatsApp Business number for Infobip?
Log in to your Infobip account and navigate to the WhatsApp section. For development, Infobip provides shared test sender numbers that you can activate by sending a specific keyword from your personal WhatsApp. For production, you'll need to register your own business number through the Infobip onboarding process, which requires Meta (Facebook) approval and typically takes 1-2 weeks.
What is the 24-hour customer service window for WhatsApp?
WhatsApp Business API enforces a 24-hour customer service window. You can only send free-form messages to users who have messaged you within the last 24 hours. Outside this window, you must use pre-approved message templates. This window resets each time the user sends you a message.
How do I create and approve WhatsApp message templates?
Create templates in your Infobip portal under the WhatsApp templates section. Templates must follow WhatsApp's guidelines and include placeholders for dynamic content. Submit templates for Meta's approval – this process typically takes 1-2 business days. Only approved templates can be used to initiate conversations outside the 24-hour window.
Why is my webhook not receiving messages?
Common issues include: (1) Your webhook URL is not publicly accessible – ensure ngrok is running during development, (2) The webhook is not configured correctly in Infobip portal – verify the URL includes the correct path /api/functions/inboundWebhook, (3) Security headers don't match – ensure your x-webhook-secret matches between .env and Infobip configuration, or (4) Your webhook is returning an error status code – check your application logs.
Can I send media files (images, documents, videos) via WhatsApp?
Yes, Infobip supports sending media through the WhatsApp API. You'll need to implement additional service functions for media messages using the /whatsapp/1/message/image, /whatsapp/1/message/document, and /whatsapp/1/message/video endpoints. Media files can be sent via URL or uploaded directly to Infobip's servers.
How do I implement HMAC signature validation for webhooks?
For production, replace the basic shared secret validation with HMAC-SHA256 signature validation. Infobip sends a X-Hub-Signature header with each webhook request. Compute the HMAC-SHA256 hash of the request body using your webhook secret as the key, and compare it to the signature in the header. Reference: Infobip Webhook Security Documentation
What are the rate limits for Infobip WhatsApp API?
Rate limits vary based on your Infobip account tier and the Meta-approved messaging throughput for your WhatsApp Business number. New numbers typically start with lower limits (e.g., 1,000 messages per day) that increase based on message quality and user engagement. Check your Infobip account dashboard for your specific limits and contact support to request increases.
Next Steps and Additional Resources
After implementing your RedwoodJS + Infobip WhatsApp integration, consider these enhancements:
Implement interactive messages with buttons and quick replies for better user engagement
Add conversation management to track message threads and user context
Set up automated responses using RedwoodJS background jobs for common queries
Monitor message delivery rates and optimize template approval ratings
Explore WhatsApp Commerce features like product catalogs and payment integrations
Integrate Infobip WhatsApp with RedwoodJS by setting up a new RedwoodJS project, configuring Infobip credentials as environment variables, implementing a service to handle sending messages, creating a GraphQL mutation to expose the sending functionality, setting up a webhook function to receive incoming messages and delivery reports, and optionally setting up a database schema to log message interactions. This guide uses TypeScript, GraphQL, Prisma, and Node.js to achieve the integration.
ngrok creates a secure tunnel to your local RedwoodJS development server, making it publicly accessible for receiving webhooks from Infobip. This is necessary because Infobip needs a public URL to send real-time message and delivery report notifications to during development.
Create a RedwoodJS service function that constructs the message payload, including recipient number, template name, placeholders, and language. This function then calls the Infobip API using an HTTP client like fetch or axios. You'll need your Infobip API key, base URL, and WhatsApp sender number, all configured as environment variables.
Infobip is an official Business Solution Provider (BSP) for the WhatsApp Business API. They provide the necessary infrastructure and API access to connect your application with the WhatsApp network, enabling you to send and receive messages and manage templates.
RedwoodJS offers a robust, full-stack, serverless framework with built-in features like GraphQL and Prisma, simplifying development and deployment. Its integrated frontend/backend structure streamlines the process of connecting user interfaces with WhatsApp communication flows.
Incoming messages are forwarded from WhatsApp through the Infobip platform to a RedwoodJS function acting as a webhook. This function processes the message, potentially interacts with a database to log the message, and acknowledges receipt to Infobip. Security measures like shared secrets or signature validation protect the webhook.
The guide provides a basic security example using a shared secret passed in an HTTP header. For production, you must implement robust signature validation (e.g., HMAC) according to Infobip's documentation to prevent unauthorized access to your webhook.
You will need Node.js, Yarn, the RedwoodJS CLI, an Infobip account, a registered WhatsApp Sender number, and basic understanding of RedwoodJS, GraphQL, and TypeScript. ngrok is necessary for local webhook development. An active and approved template on Infobip is required to send templated messages.
InfobipTemplatePlaceholder is a TypeScript interface defined to enforce the structure of data required by WhatsApp message templates. It ensures that the message sent to the Infobip API contains the correct placeholders for variables like body text, buttons, and headers, which get populated with dynamic values when sending the message.
Yes, the provided code can be extended to send free-form messages. Ensure the free-form messages comply with WhatsApp Business API guidelines and consider that initiating a conversation usually requires a pre-approved message template first, followed by a 24-hour window.
Infobip sends delivery reports (sent, delivered, read, failed) to the same webhook as inbound messages. The webhook handler must distinguish between message types by analyzing the payload structure. You can then update message status in your database based on the delivery reports received.
The guide recommends defining a Prisma schema with a MessageLog model to store message details (sender, recipient, status, content preview). Update your service and webhook functions to create and update MessageLog entries when sending and receiving messages and delivery reports. Be mindful of PII when logging content.
While the example uses Node's native fetch, a dedicated HTTP client like axios might be beneficial for more complex API interactions, features like interceptors for managing auth tokens or retries, or if you're more familiar with its API.
Use the language code that aligns with your approved WhatsApp message template and your target audience's language. The guide uses 'en' for English, but change this to match your template's language, e.g., 'es' for Spanish, 'fr' for French.
This guide provides a step-by-step walkthrough for integrating the Infobip WhatsApp Business API into your RedwoodJS application. You'll learn how to send messages, handle incoming messages via webhooks, manage templates, and set up a robust foundation for production use.
By the end of this tutorial, you will have a RedwoodJS application capable of:
This guide solves the common challenge of connecting a modern full-stack framework like RedwoodJS with a powerful communication channel like WhatsApp, leveraging Infobip as an official WhatsApp Business Solution Provider (BSP).
Technologies Used:
System Architecture Overview:
The system involves the user's WhatsApp application interacting with the WhatsApp network. Messages sent by the user are forwarded by WhatsApp through the Infobip platform to a RedwoodJS Function (webhook). This function processes the message, potentially interacts with a service layer and database (Prisma), and acknowledges receipt. To send messages, the RedwoodJS frontend triggers a GraphQL mutation in the RedwoodJS API. This API calls a service layer function, which interacts with the database and then calls the Infobip WhatsApp API. Infobip delivers the message via the WhatsApp network to the user's device. Delivery reports follow a similar path back from Infobip to the RedwoodJS webhook.
Prerequisites:
yarn global add @redwoodjs/cli)ngrokor a similar tunneling service for local webhook development1. RedwoodJS Project Setup for WhatsApp Integration
Initialize a new RedwoodJS project and configure the necessary environment variables for Infobip.
Create RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-infobip-whatsapp cd redwood-infobip-whatsappFollow the prompts (choose TypeScript).
Infobip Account Setup & Credentials:
https://<your-unique-id>.api.infobip.com.Configure Environment Variables: RedwoodJS uses a
.envfile for environment variables. Create this file in the project root:touch .envAdd your Infobip credentials and sender number:
INFOBIP_WEBHOOK_SECRETis a simple shared secret for basic webhook validation. For production, implement HMAC signature validation as described in Infobip's webhook security documentation.envto your.gitignorefile to prevent committing secrets. RedwoodJS does this by default, but always verifyInstall Dependencies (Optional – HTTP Client): Node's native
fetchis sufficient for this guide. If you preferaxiosfor complex scenarios:2. Implementing Core Functionality: Sending WhatsApp Messages
Create a RedwoodJS service on the API side to encapsulate the logic for interacting with the Infobip API.
Generate Infobip Service:
This creates
api/src/services/infobip/infobip.tsand related test files.Implement
sendTemplateMessageFunction: Sending pre-approved template messages is often the starting point, especially for initiating conversations outside the 24-hour customer service window.Open
api/src/services/infobip/infobip.tsand add the following code:// api/src/services/infobip/infobip.ts import type { Fetch } from '@whatwg-node/fetch' // Import Fetch type if needed, often globally available import { logger } from 'src/lib/logger' // Redwood's logger interface InfobipTemplatePlaceholder { // Define structure based on template needs, e.g., text, location, media, etc. // Example for simple text placeholders in the body: body?: { placeholders: string[] } // Example for URL button parameter: buttons?: { type: 'URL'; parameter: string }[] // Add header, etc., as needed by your template } interface InfobipSendMessagePayload { messages: { from: string to: string messageId?: string // Optional: client-generated message ID content: { templateName: string templateData: InfobipTemplatePlaceholder language: string } // Optional: Add smsFailover, notifyUrl, etc. here if needed // urlOptions?: { shortenUrl?: boolean; trackClicks?: boolean; trackingUrl?: string; customDomain?: string } }[] } interface InfobipSendResponse { // Define based on expected Infobip response structure // Example: messages?: { to: string status?: { groupId?: number groupName?: string id?: number name?: string description?: string } messageId?: string }[] bulkId?: string } const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL const INFOBIP_WHATSAPP_SENDER = process.env.INFOBIP_WHATSAPP_SENDER // Helper function for API calls const callInfobipApi = async <T = any>( endpoint: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'POST', body?: Record<string, any> ): Promise<T> => { if (!INFOBIP_API_KEY || !INFOBIP_BASE_URL) { throw new Error( 'Infobip API Key or Base URL not configured in environment variables.' ) } const url = `${INFOBIP_BASE_URL}${endpoint}` const headers = { Authorization: `App ${INFOBIP_API_KEY}`, 'Content-Type': 'application/json', Accept: 'application/json', } logger.debug({ custom: { url, method } }, 'Calling Infobip API') try { const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, }) logger.debug( { custom: { status: response.status } }, 'Infobip API Response Status' ) if (!response.ok) { const errorBody = await response.text() logger.error( { custom: { status: response.status, errorBody } }, 'Infobip API request failed' ) throw new Error( `Infobip API Error (${response.status}): ${errorBody}` ) } // Handle cases where response might be empty (e.g., 204 No Content) if (response.status === 204) { return {} as T } const responseData = (await response.json()) as T logger.debug( { custom: responseData }, 'Infobip API Response Success Data' ) return responseData } catch (error) { logger.error({ error }, 'Error calling Infobip API') throw error } } /** * Sends a pre-approved WhatsApp template message via Infobip. * * @param to - Recipient's phone number in E.164 format (e.g., 441134960001) * @param templateName - Exact name of the approved template * @param templateData - Object containing placeholders required by the template * @param language - Language code of the template (e.g., "en", "en_US") * @returns Response from the Infobip API */ export const sendTemplateMessage = async ({ to, templateName, templateData, language, }: { to: string templateName: string templateData: InfobipTemplatePlaceholder language: string }): Promise<InfobipSendResponse> => { if (!INFOBIP_WHATSAPP_SENDER) { throw new Error('Infobip WhatsApp Sender not configured.') } const payload: InfobipSendMessagePayload = { messages: [ { from: INFOBIP_WHATSAPP_SENDER, to, content: { templateName, templateData, language, }, }, ], } logger.info( { custom: { to, templateName } }, 'Attempting to send template message' ) // Infobip WhatsApp API endpoint for template messages // Reference: https://www.infobip.com/docs/api/channels/whatsapp/send-whatsapp-template-message const endpoint = '/whatsapp/1/message/template' return callInfobipApi<InfobipSendResponse>(endpoint, 'POST', payload) } // Add functions for sending other message types (text, media) later if needed // Remember: free-form messages require an active 24-hour window initiated by the userExplanation:
InfobipTemplatePlaceholder,InfobipSendMessagePayload,InfobipSendResponse) are defined based on Infobip's API documentation. AdjustInfobipTemplatePlaceholderbased on the actual placeholders your specific templates require (body text, button parameters, header variables, etc.).process.envcallInfobipApihelper function handles constructing the request (URL, headers, authorization, body) and basic error checking/logging. Crucially, it uses theAuthorization: App YOUR_API_KEYheader format specified by InfobipsendTemplateMessageconstructs the specific payload for the/whatsapp/1/message/templateendpoint and calls the helper functionsrc/lib/logger) for debugging3. Building the API Layer (GraphQL Mutation for WhatsApp)
Expose the
sendTemplateMessagefunctionality through a GraphQL mutation so your frontend (or other services) can trigger it.Define GraphQL Schema: Open
api/src/graphql/infobip.sdl.ts(create it if it doesn't exist) and define the mutation:# api/src/graphql/infobip.sdl.ts export const schema = gql` # Define types based on your template data structure # This is a flexible example – make it specific! input InfobipTemplateBodyInput { placeholders: [String!]! } input InfobipTemplateButtonInput { type: String! # e.g., "URL" parameter: String! } input InfobipTemplateDataInput { body: InfobipTemplateBodyInput buttons: [InfobipTemplateButtonInput!] # Add header, etc., if needed } type InfobipSendStatus { groupId: Int groupName: String id: Int name: String description: String } type InfobipSentMessageInfo { to: String status: InfobipSendStatus messageId: String } type InfobipSendResponse { messages: [InfobipSentMessageInfo!] bulkId: String } type Mutation { """ Sends a WhatsApp template message via Infobip. Requires appropriate permissions. """ sendWhatsAppTemplate( to: String! templateName: String! templateData: InfobipTemplateDataInput! language: String! ): InfobipSendResponse @requireAuth # Add appropriate auth directive } `Explanation:
InfobipTemplateDataInput, etc.) to structure the data passed into the mutation. Make these specific to match theInfobipTemplatePlaceholderinterface in your service and the requirements of your actual templatesInfobipSendResponse, etc.) to match the expected structure returned by thesendTemplateMessageservice functionsendWhatsAppTemplatemutation takes the necessary arguments@requireAuth(or a more specific roles-based directive) should be added to protect this mutation. This guide doesn't implement the auth logic itself, but it's crucial for productionImplement Mutation Resolver: RedwoodJS automatically maps the
sendWhatsAppTemplatemutation to thesendTemplateMessagefunction in theapi/src/services/infobip/infobip.tsservice because the names match. No extra resolver code is needed for this mapping.Testing the Mutation:
yarn rw devhttp://localhost:8911/graphql)templateNameused below ("welcome_multiple_languages") is just an example. Replace this with the exact name of an active, approved template associated with your Infobip sender number. Ensure thetemplateDatamatches the placeholders defined in your specific template.mutation SendTestTemplate { sendWhatsAppTemplate( to: "YOUR_PERSONAL_WHATSAPP_NUMBER_E164" # e.g., 15551234567 # !! Replace with YOUR approved template name !! templateName: "YOUR_APPROVED_TEMPLATE_NAME" language: "en" templateData: { # !! Adjust placeholders to match YOUR template !! body: { placeholders: ["Developer Guide User"] } # Add button data if your template has URL buttons, e.g.: # buttons: [ # { type: "URL", parameter: "some-dynamic-path" } # ] } ) { bulkId messages { to messageId status { id name description } } } }4. Webhook Integration for Inbound WhatsApp Messages
To receive messages sent to your WhatsApp number or get delivery status updates, Infobip needs to send HTTP requests (webhooks) to your application. Use a RedwoodJS Function for this.
Generate Webhook Function:
yarn rw g function inboundWebhook --typescriptThis creates
api/src/functions/inboundWebhook.ts.Implement Webhook Handler: Open
api/src/functions/inboundWebhook.tsand add the handler logic:// api/src/functions/inboundWebhook.ts import type { APIGatewayEvent, Context } from 'aws-lambda' import { logger } from 'src/lib/logger' // Optional: Import DB functions if you want to store messages/reports // import { db } from 'src/lib/db' const INFOBIP_WEBHOOK_SECRET = process.env.INFOBIP_WEBHOOK_SECRET /** * Handler function receives HTTP requests from Infobip for incoming messages * and delivery reports. * * @param event - Lambda event object containing request details (headers, body) * @param _context - Lambda context object (unused here) */ export const handler = async (event: APIGatewayEvent, _context: Context) => { logger.info('Inbound webhook received') logger.debug({ custom: { headers: event.headers, body: event.body } }, 'Webhook Raw Data') // --- Basic Security Check (Shared Secret) --- // IMPORTANT: This is basic. For production, implement HMAC signature validation. // Infobip provides X-Hub-Signature header for HMAC-SHA256 validation. // Reference: https://www.infobip.com/docs/essentials/webhooks#securing-your-webhooks const receivedSecret = event.headers['x-webhook-secret'] // Use a custom header if (!INFOBIP_WEBHOOK_SECRET || receivedSecret !== INFOBIP_WEBHOOK_SECRET) { logger.warn('Unauthorized webhook access attempt or missing/invalid secret.') return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }), headers: { 'Content-Type': 'application/json' }, } } // --- End Security Check --- try { if (!event.body) { logger.warn('Webhook received with empty body.') return { statusCode: 400, body: JSON.stringify({ error: 'Bad Request: Empty body' }), headers: { 'Content-Type': 'application/json' }, } } const payload = JSON.parse(event.body) // Infobip sends results in an array, usually containing one item per webhook call const results = payload.results if (!results || !Array.isArray(results) || results.length === 0) { logger.warn({ custom: payload }, 'Webhook payload missing "results" array.') return { statusCode: 400, body: JSON.stringify({ error: 'Bad Request: Invalid payload structure' }), headers: { 'Content-Type': 'application/json' }, } } // Process each result (usually just one) for (const result of results) { if (result.error) { logger.error({ custom: result }, 'Infobip reported an error in webhook payload') // Handle specific errors if necessary continue // Process next result if any } // --- Distinguish between Inbound Message and Delivery Report --- // The structure differs significantly. Consult Infobip's documentation // for the exact payload structures for different event types (message received, // delivery reports like SENT, DELIVERED, READ, FAILED, etc.) and adapt this logic. // Example differentiation logic (adapt based on actual payloads you receive): if (result.messageId && result.from && result.to && result.receivedAt && result.message) { // Likely an INBOUND MESSAGE logger.info({ custom: { from: result.from, messageId: result.messageId, type: result.message.type } }, 'Processing Inbound Message') // TODO: Add your logic here // - Parse result.message (text, image, document, location, interactive reply, button, etc.) // - Store in database (e.g., using Prisma: await db.messageLog.create(…)) // - Trigger downstream actions (e.g., auto-reply, notify support) } else if (result.messageId && result.sentAt && result.status && result.doneAt) { // Likely a DELIVERY REPORT logger.info({ custom: { messageId: result.messageId, status: result.status.name } }, 'Processing Delivery Report') // TODO: Add your logic here // - Update message status in your database // (e.g., await db.messageLog.update({ where: { infobipMessageId: result.messageId }, data: { status: result.status.name } })) } else { logger.warn({ custom: result }, 'Unrecognized webhook event type') } } // End loop processing results // Acknowledge receipt to Infobip return { statusCode: 200, body: JSON.stringify({ message: 'Webhook received successfully' }), headers: { 'Content-Type': 'application/json' }, } } catch (error) { logger.error({ error, custom: { body: event.body } }, 'Error processing webhook') // Return 500 for server errors; Infobip may retry return { statusCode: 500, body: JSON.stringify({ error: 'Internal Server Error' }), headers: { 'Content-Type': 'application/json' }, } } } // End handlerExplanation:
x-webhook-secret). This is minimal security. For production, implement HMAC signature validation using Infobip'sX-Hub-Signatureheader. Reference: Infobip Webhook Securityresultsarray structurefrom,receivedAtvs.sentAt,status). Consult actual payloads from Infobip and their documentation to adjust this logic accurately// TODO:) indicate where you'd add your application-specific logic (database interaction, notifications, etc.)200 OKstatus code to Infobip upon successful processing, or appropriate error codes (401, 400, 500). Infobip generally expects a 2xx response to consider the webhook deliveredExpose Webhook Locally with ngrok: Infobip needs a publicly accessible URL to send webhooks to. During development,
ngrokcreates a secure tunnel to your local machine.yarn rw dev(Note the API server port, usually 8911)ngrokwill display forwarding URLs (e.g.,https://<random-string>.ngrok.io). Copy thehttpsURL.Configure Webhook in Infobip:
ngrokHTTPS URL, appending the path to your Redwood function:/inboundWebhook. The full URL will be:https://<random-string>.ngrok.io/api/functions/inboundWebhookx-webhook-secret(or your chosen name) with the value set to yourINFOBIP_WEBHOOK_SECRETfrom the.envfile. For production, configure HMAC signature validation following Infobip's documentationTesting the Webhook:
yarn rw devand the terminal runningngrok. You should see log output from yourinboundWebhookfunction indicating it received and processed the message.5. Error Handling, Logging, and Retry Mechanisms
Error Handling:
callInfobipApihelper includes basictry…catchblocks and checks the HTTP response status. Expand this to handle specific Infobip error codes or messages if needed (refer to Infobip API documentation for error details)try…catchand returns appropriate HTTP status codes (4xx for client errors/bad data, 500 for server errors). Infobip may retry webhooks on receiving 5xx errors or timeoutsLogging:
src/lib/logger) is used extensively. Use different log levels (debug,info,warn,error) appropriatelyRetry Mechanisms (API Calls):
callInfobipApifunction using libraries likeasync-retryor a manual loop with exponential backoff. Be cautious not to retry excessively for errors that are unlikely to resolve (e.g., invalid API key, malformed request)# Example dependency if using async-retry # yarn workspace api add async-retry @types/async-retry6. Database Schema and Data Layer (Optional but Recommended)
Storing message logs helps with tracking, debugging, and analytics.
Define Prisma Schema: Open
api/db/schema.prismaand add a model:Run Migration:
Update Services/Functions to Use DB:
api/src/services/infobip/infobip.ts(sendTemplateMessage), after a successful API call, create aMessageLogentryapi/src/functions/inboundWebhook.ts, inside the loop processing results, create or updateMessageLogentries for inbound messages and delivery reportsFrequently Asked Questions (FAQ)
How do I get a WhatsApp Business number for Infobip?
Log in to your Infobip account and navigate to the WhatsApp section. For development, Infobip provides shared test sender numbers that you can activate by sending a specific keyword from your personal WhatsApp. For production, you'll need to register your own business number through the Infobip onboarding process, which requires Meta (Facebook) approval and typically takes 1-2 weeks.
What is the 24-hour customer service window for WhatsApp?
WhatsApp Business API enforces a 24-hour customer service window. You can only send free-form messages to users who have messaged you within the last 24 hours. Outside this window, you must use pre-approved message templates. This window resets each time the user sends you a message.
How do I create and approve WhatsApp message templates?
Create templates in your Infobip portal under the WhatsApp templates section. Templates must follow WhatsApp's guidelines and include placeholders for dynamic content. Submit templates for Meta's approval – this process typically takes 1-2 business days. Only approved templates can be used to initiate conversations outside the 24-hour window.
Why is my webhook not receiving messages?
Common issues include: (1) Your webhook URL is not publicly accessible – ensure ngrok is running during development, (2) The webhook is not configured correctly in Infobip portal – verify the URL includes the correct path
/api/functions/inboundWebhook, (3) Security headers don't match – ensure yourx-webhook-secretmatches between.envand Infobip configuration, or (4) Your webhook is returning an error status code – check your application logs.Can I send media files (images, documents, videos) via WhatsApp?
Yes, Infobip supports sending media through the WhatsApp API. You'll need to implement additional service functions for media messages using the
/whatsapp/1/message/image,/whatsapp/1/message/document, and/whatsapp/1/message/videoendpoints. Media files can be sent via URL or uploaded directly to Infobip's servers.How do I implement HMAC signature validation for webhooks?
For production, replace the basic shared secret validation with HMAC-SHA256 signature validation. Infobip sends a
X-Hub-Signatureheader with each webhook request. Compute the HMAC-SHA256 hash of the request body using your webhook secret as the key, and compare it to the signature in the header. Reference: Infobip Webhook Security DocumentationWhat are the rate limits for Infobip WhatsApp API?
Rate limits vary based on your Infobip account tier and the Meta-approved messaging throughput for your WhatsApp Business number. New numbers typically start with lower limits (e.g., 1,000 messages per day) that increase based on message quality and user engagement. Check your Infobip account dashboard for your specific limits and contact support to request increases.
Next Steps and Additional Resources
After implementing your RedwoodJS + Infobip WhatsApp integration, consider these enhancements:
For more information, consult: