Developer Guide: Integrating Infobip WhatsApp with RedwoodJS
This guide provides a step-by-step walkthrough for integrating the Infobip WhatsApp Business API into your RedwoodJS application. We'll cover sending messages, handling incoming messages via webhooks, managing templates, and setting 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 the official 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)
- Yarn Classic (v1) recommended (RedwoodJS primarily targets v1; v2+ may require configuration)
- RedwoodJS CLI (
yarn global add @redwoodjs/cli
) - An Infobip Account (Sign up for free)
- 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. Setting up the RedwoodJS Project
Let's 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-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 you don't have one. 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, you'll need to 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:touch .env
Add your Infobip credentials and sender number:
# .env # Infobip API Credentials INFOBIP_API_KEY='YOUR_INFOBIP_API_KEY' INFOBIP_BASE_URL='YOUR_INFOBIP_BASE_URL' # e.g., https://xyz123.api.infobip.com # Infobip WhatsApp Sender INFOBIP_WHATSAPP_SENDER='YOUR_INFOBIP_WHATSAPP_SENDER_NUMBER' # e.g., 447860099299 # Webhook Security (Simple Token - Improve for Production) INFOBIP_WEBHOOK_SECRET='YOUR_STRONG_RANDOM_SECRET_STRING'
- Replace the placeholder values with your actual credentials.
- The
INFOBIP_WEBHOOK_SECRET
is a simple shared secret we'll use later to add a basic layer of security to our incoming webhook. Generate a strong random string for this. - Important: Add
.env
to your.gitignore
file to prevent committing secrets. RedwoodJS does this by default, but always double-check.
-
Install Dependencies (Optional - HTTP Client): While Node's native
fetch
is often sufficient, you might prefer a dedicated HTTP client likeaxios
for more complex scenarios or features like interceptors. For this guide, we'll stick withfetch
. If you wantedaxios
:yarn workspace api add axios
2. Implementing Core Functionality: Sending Messages
We'll create a RedwoodJS service on the API side to encapsulate the logic for interacting with the Infobip API.
-
Generate Infobip Service:
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:// 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') // Re-throw or handle specific errors as needed throw error } } /** * Sends a pre-approved WhatsApp template message via Infobip. * * @param to - The recipient's phone number in E.164 format (e.g., 441134960001). * @param templateName - The exact name of the approved template. * @param templateData - An object containing placeholders required by the template. * @param language - The language code of the template (e.g., 'en'). * @returns The 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' ) // Refer to Infobip docs for the exact endpoint: // 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 user.
- Explanation:
- We define interfaces (
InfobipTemplatePlaceholder
,InfobipSendMessagePayload
,InfobipSendResponse
) for type safety, based on Infobip's API documentation. AdjustInfobipTemplatePlaceholder
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 theAuthorization: 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.
- We define interfaces (
- Explanation:
3. Building the API Layer (GraphQL Mutation)
Now, let's expose the sendTemplateMessage
functionality through a GraphQL mutation so our 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:
- We define input types (
InfobipTemplateDataInput
, etc.) to structure the data passed into the mutation. Make these specific to match theInfobipTemplatePlaceholder
interface in your service and the requirements of your actual templates. - We define output types (
InfobipSendResponse
, etc.) to match the expected structure returned by thesendTemplateMessage
service function. - The
sendWhatsAppTemplate
mutation takes the necessary arguments. @requireAuth
(or a more specific roles-based directive) should be added to protect this mutation. We won't implement the auth logic itself in this guide, but it's crucial for production.
- We define input types (
- Explanation:
-
Implement Mutation Resolver: RedwoodJS automatically maps the
sendWhatsAppTemplate
mutation to thesendTemplateMessage
function in theapi/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. You must replace this with the exact name of an active, approved template associated with your Infobip sender number. Also, ensure thetemplateData
matches the placeholders defined in your specific template. - Run a mutation like this ( replace placeholders with your actual test number, template name, and data):
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.
- Start your Redwood development server:
4. Integrating with Infobip (Webhook for Inbound Messages/Reports)
To receive messages sent to your WhatsApp number or get delivery status updates, Infobip needs to send HTTP requests (webhooks) to your application. We'll use a RedwoodJS Function for this.
-
Generate Webhook Function:
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:// 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 /** * The handler function receives HTTP requests from Infobip for incoming messages * and delivery reports. * * @param event - The Lambda event object containing request details (headers, body). * @param _context - The 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. Infobip might offer more secure signature validation (e.g., HMAC). // Consult Infobip's documentation and implement signature validation for production. 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. You MUST check 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, location, interactive reply, 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 might retry return { 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, you must investigate Infobip's documentation for signature validation (e.g., HMAC) and implement that verification instead. - 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
). You MUST inspect actual payloads from Infobip and consult 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.
- Explanation:
-
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:
ngrok http 8911
ngrok
will display forwarding URLs (e.g.,https://<random-string>.ngrok.io
). Copy thehttps
URL.
- Start your Redwood app:
-
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
. So the full URL will be something like: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 yourINFOBIP_WEBHOOK_SECRET
from the.env
file. If Infobip supports signature validation (HMAC), configure that instead/additionally following their documentation. - Ensure the webhook is enabled 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 runningngrok
. You should see log output from yourinboundWebhook
function indicating it received and processed the message. - Delivery Report: Use the GraphQL Playground (from Step 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.
- Inbound Message: Send a WhatsApp message from your personal phone to your Infobip Sender number. Observe the terminal running
5. Error Handling, Logging, and Retry Mechanisms
- Error Handling:
- The
callInfobipApi
helper includes basictry...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 might retry webhooks on receiving 5xx errors or timeouts.
- The
- 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.
- RedwoodJS's built-in Pino logger (
- Retry Mechanisms (API Calls):
- For transient network issues when calling the Infobip API, you could implement a simple retry strategy within the
callInfobipApi
function using libraries likeasync-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).
# Example dependency if using async-retry # yarn workspace api add async-retry @types/async-retry
- For transient network issues when calling the Infobip API, you could implement a simple retry strategy within the
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:// 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:
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 aMessageLog
entry:// Add this import at the top import { db } from 'src/lib/db' // Inside sendTemplateMessage, after successful callInfobipApi: const apiResponse = await callInfobipApi<InfobipSendResponse>(endpoint, 'POST', payload) // Existing call // Add this block after the call const sentInfo = apiResponse.messages?.[0] // Assuming single message send try { await db.messageLog.create({ data: { direction: 'OUTBOUND', infobipMessageId: sentInfo?.messageId, bulkId: apiResponse.bulkId, sender: INFOBIP_WHATSAPP_SENDER, recipient: to, status: sentInfo?.status?.name ?? 'PENDING', // Initial status might be 'PENDING' or similar statusDescription: sentInfo?.status?.description, templateName: templateName, messageType: 'TEMPLATE', // contentPreview: JSON.stringify(templateData), // Be cautious with PII }, }) } catch (dbError) { logger.error({ error: dbError }, 'Failed to create outbound message log entry') // Decide if this failure should affect the overall function result } // End added block return apiResponse // Existing return
- Receiving (Webhook): In
api/src/functions/inboundWebhook.ts
, inside the loop processing results:// Add this import at the top import { db } from 'src/lib/db' // Inside the 'if (result.messageId && result.from ...)' block (INBOUND MESSAGE): logger.info({ custom: { from: result.from, messageId: result.messageId, type: result.message.type } }, 'Processing Inbound Message') try { await db.messageLog.create({ data: { direction: 'INBOUND', infobipMessageId: result.messageId, sender: result.from, recipient: result.to, status: 'RECEIVED', // Custom status for inbound messageType: result.message?.type.toUpperCase(), contentPreview: typeof result.message?.text === 'string' ? result.message.text.substring(0, 100) : null, // Example for text, adapt for others webhookPayload: result as any, // Store raw payload if needed (cast to 'any' or Prisma.JsonValue) }, }) } catch (dbError) { logger.error({ error: dbError, custom: { messageId: result.messageId } }, 'Failed to create inbound message log entry') } // End added DB logic for INBOUND // Inside the 'else if (result.messageId && result.sentAt ...)' block (DELIVERY REPORT): logger.info({ custom: { messageId: result.messageId, status: result.status.name } }, 'Processing Delivery Report') try { await db.messageLog.updateMany({ // Use updateMany as messageId might not be unique initially if PENDING where: { infobipMessageId: result.messageId }, data: { status: result.status?.name, statusDescription: result.status?.description, errorCode: result.error?.id, errorMessage: result.error?.name, updatedAt: new Date(), // Manually update timestamp }, }) } catch (dbError) { logger.error({ error: dbError, custom: { messageId: result.messageId } }, 'Failed to update message log for delivery report') } // End added DB logic for DELIVERY REPORT
- Sending: In