Skip to main content
Version: Latest

6.3 WhatsAppInboxController

Route: api/v{version:apiVersion}/whatsapp/inbox · Class attributes: [Authorize] · 28 endpoints — the largest controller in the WhatsApp surface, and the agent-facing core of the customer-service Inbox.

All permissions are PermissionConstants.WhatsAppInbox.*. Unless a user holds WhatsAppInbox.ViewAll, conversation lists are always scoped to conversations assigned to (or otherwise visible to) the current agent — filter.ShowAll is forced to false server-side regardless of what the client sends.


POST GetConversations

Purpose: paginated, filterable conversation list (DataTable-style grid) — the main Inbox screen. Permission: WhatsAppInbox.View

Request: DtParameters body + WhatsAppConversationFilterDto query string:

{ "draw": 1, "start": 0, "length": 25, "search": {"value": "", "regex": false},
"order": [{"column": 0, "dir": "desc"}], "columns": [{"data": "lastMessageAt", "name": "", "searchable": true, "orderable": true}] }

Query: ?status=Open&onlyUnread=false&showArchived=false

Response (DtResult<WhatsAppConversationDto>, unwrapped):

{ "draw": 1, "recordsTotal": 58, "recordsFiltered": 12, "data": [
{ "id": "c1a2b3c4-0000-0000-0000-000000000001", "customerWaId": "966501112222",
"customerPhone": "966501112222", "customerName": "Ahmed Al-Otaibi", "displayName": "Ahmed Al-Otaibi",
"lastMessagePreview": "Hello, I have a question about my order", "lastMessageDirection": 1,
"lastMessageType": 1, "lastMessageAt": "2026-07-05T10:12:00Z", "lastInboundMessageAt": "2026-07-05T10:12:00Z",
"conversationWindowExpiresAt": "2026-07-06T10:12:00Z", "canSendFreeText": true, "unreadCount": 2,
"assignedToUserId": null, "assignedToUserName": null, "status": 1, "priority": 2, "isPinned": false,
"isArchived": false, "createdOn": "2026-07-05T10:12:00Z", "isFavoriteForCurrentUser": false } ] }

Error cases: invalid DtParameters shape → 400. Backend: WhatsAppInboxService.GetConversationsAsync. Calls Meta API: No. Angular: the primary Inbox grid; refresh on SignalR UnreadCountChanged/ConversationAssigned etc. Flutter: same list, typically infinite-scroll rather than DataTable paging.


GET GetDetails/{conversationId}

Purpose: full conversation detail including SLA metrics, for the conversation header panel. Permission: WhatsAppInbox.View · Request: route conversationId:Guid Response (Result<WhatsAppConversationDetailsDto>):

{ "succeeded": true, "message": null, "data": {
"id": "c1a2b3c4-0000-0000-0000-000000000001", "customerWaId": "966501112222", "customerName": "Ahmed Al-Otaibi",
"phoneNumberId": "1088505757690326", "businessAccountId": "2091544401399393",
"assignedToUserName": null, "status": 1, "priority": 2,
"totalMessages": 14, "firstResponseSeconds": 95.0, "averageResponseSeconds": 142.5,
"waitingForCustomer": false, "waitingForAgent": true, "createdOn": "2026-07-05T10:12:00Z" } }

Error cases: unknown conversationId404 via EntityNotFoundException. Backend: WhatsAppInboxService.GetConversationDetailsAsync. Calls Meta API: No. Angular/Flutter: loaded when a conversation is opened; drives the SLA/header panel.


POST GetMessages/{conversationId}

Purpose: paginated message list for one conversation (the chat thread). Permission: WhatsAppInbox.View · Request: route conversationId + DtParameters body Response (DtResult<WhatsAppMessageDto>, unwrapped):

{ "draw": 1, "recordsTotal": 14, "recordsFiltered": 14, "data": [
{ "id": "m1...", "conversationId": "c1a2b3c4-0000-0000-0000-000000000001", "direction": 1, "messageType": 1,
"messageStatus": 3, "fromWaId": "966501112222", "textBody": "Hello, I have a question about my order",
"sentOn": null, "deliveredOn": null, "readOn": null, "createdOn": "2026-07-05T10:12:00Z" } ] }

Error cases: unknown conversationId → empty result set (not a 404 — DataTable convention). Backend: WhatsAppInboxService.GetMessagesAsync. Calls Meta API: No. Angular/Flutter: the chat thread; typically appended-to in real time as new messages arrive rather than re-fetched wholesale.


GET UnreadCount

Purpose: global unread badge counters for the current agent. Permission: WhatsAppInbox.View · Request: none Response: { "succeeded": true, "message": null, "data": { "unreadConversations": 3, "totalUnreadMessages": 7 } } Backend: WhatsAppInboxService.GetUnreadCountAsync. Calls Meta API: No. Angular: poll or refresh on SignalR UnreadCountChanged. Flutter: bottom-nav/tab badge.


GET Summary

Purpose: open/pending/closed conversation totals for the Inbox sidebar. Permission: WhatsAppInbox.View · Response data: { "totalOpen": 12, "totalPending": 3, "totalClosed": 43, "totalUnread": 7, "totalAssignedToMe": 5, "totalUnassigned": 2 } Backend: WhatsAppInboxService.GetInboxSummaryAsync. Calls Meta API: No.


GET Dashboard

Purpose: agent dashboard including online-agent count and average response time. Permission: WhatsAppInbox.View · Response data: { "openConversations": 12, "pendingConversations": 3, "closedToday": 8, "assignedToMe": 5, "unassigned": 2, "unreadCount": 7, "onlineAgents": 4, "averageResponseTimeSeconds": 132.4 } Backend: WhatsAppInboxService.GetDashboardSummaryAsync. Calls Meta API: No.


POST SendReply/{conversationId}

Purpose: send a free-text reply — only permitted inside Meta's 24-hour customer-service window. Permission: WhatsAppInbox.SendReply · Request (WhatsAppSendReplyDto):

{ "body": "Thank you for reaching out, checking your order now.", "previewUrl": false }

Response: Result<Guid> — the new WhatsAppMessage.Id. Error cases: outside the 24h window → 400 ValidationException ("conversation window expired, use a template reply"); Meta rejection → error surfaced in the persisted message's ErrorMessage. Backend: WhatsAppInboxService.SendReplyAsyncIWhatsAppService.SendTextAsync. Calls Meta API: Yes. Angular/Flutter: the chat composer's default send action; disable free-text input and force template picker when CanSendFreeText is false on the conversation DTO.


POST SendTemplateReply/{conversationId}

Purpose: send an approved template reply — usable any time, including outside the 24h window. Permission: WhatsAppInbox.SendTemplate · Request (WhatsAppSendTemplateReplyDto):

{ "templateName": "order_status_update", "languageCode": "ar",
"components": [{ "type": "body", "parameters": [{ "type": "text", "text": "12500 SAR" }] }] }

Response: Result<Guid>. Backend: WhatsAppInboxService.SendTemplateReplyAsyncIWhatsAppService.SendTemplateAsync. Calls Meta API: Yes. Angular/Flutter: the composer's "Send Template" action, always available regardless of window state.


POST Assign/{conversationId}

Purpose: assign a conversation to an agent. Permission: WhatsAppInbox.Assign Request (WhatsAppAssignConversationDto): { "userId": "5f1a3b2c-0000-0000-0000-000000000001" } Response: Result<Guid>. Side effects: broadcasts SignalR ConversationAssigned, logs a TagAdded-style timeline event (ConversationAssigned event type). Backend: WhatsAppInboxService.AssignConversationAsync. Calls Meta API: No. Angular/Flutter: assignment dropdown on the conversation header.


POST Unassign/{conversationId}

Purpose: remove the current assignment. Permission: WhatsAppInbox.Assign · Request: route only. Response: Result<Guid>. Backend: WhatsAppInboxService.UnassignConversationAsync. Calls Meta API: No.


POST Transfer/{conversationId}

Purpose: transfer a conversation to another agent with an optional handoff comment. Permission: WhatsAppInbox.Transfer · Request (WhatsAppTransferConversationDto): { "targetUserId": "...", "comment": "Escalating — customer requesting refund" } Response: Result<Guid>. Side effects: broadcasts ConversationTransferred. Backend: WhatsAppInboxService.TransferConversationAsync. Calls Meta API: No.


POST MarkAsRead/{conversationId} · POST MarkAsUnread/{conversationId}

Purpose: update the current user's read state for a conversation. Permission: WhatsAppInbox.View Request: route only. Response: Result<Guid>. Side effects: broadcasts UnreadCountChanged. Backend: WhatsAppInboxService.MarkAsReadAsync / MarkAsUnreadAsync. Calls Meta API: No. Angular/Flutter: called when a conversation is opened (MarkAsRead) or explicitly flagged for follow-up (MarkAsUnread).


POST Close/{conversationId}

Purpose: close a conversation, optionally with a reason. Permission: WhatsAppInbox.Close Request (WhatsAppCloseConversationDto): { "reason": "Resolved — refund processed" } Response: Result<Guid>. Side effects: broadcasts ConversationClosed, logs timeline event. Backend: WhatsAppInboxService.CloseConversationAsync. Calls Meta API: No.


POST Reopen/{conversationId}

Purpose: reopen a closed or archived conversation. Permission: WhatsAppInbox.Close Response: Result<Guid>. Side effects: broadcasts ConversationReopened. Backend: WhatsAppInboxService.ReopenConversationAsync. Calls Meta API: No.


POST Archive/{conversationId} · POST Unarchive/{conversationId}

Purpose: archive/unarchive a conversation (removes/restores it from the default Inbox view). Permission: WhatsAppInbox.Archive · Response: Result<Guid>. Side effects: broadcasts ConversationArchived / ConversationUnarchived. Backend: WhatsAppInboxService.ArchiveConversationAsync / UnarchiveConversationAsync. Calls Meta API: No.


POST Pin/{conversationId} · POST Unpin/{conversationId}

Purpose: pin/unpin a conversation to the top of the agent's list. Permission: WhatsAppInbox.Pin Response: Result<Guid>. Side effects: broadcasts ConversationPinned / ConversationUnpinned. Backend: WhatsAppInboxService.PinConversationAsync / UnpinConversationAsync. Calls Meta API: No.


GET MediaInfo/{messageId}

Purpose: media metadata (type, filename, size, caption) without downloading the binary. Permission: WhatsAppInbox.MediaView · Request: route messageId:Guid Response data: { "messageId": "m1...", "mediaId": "1234567890", "mimeType": "image/jpeg", "fileName": "receipt.jpg", "caption": "Payment receipt", "sizeBytes": 84213, "sha256": "a1b2..." } Backend: WhatsAppMediaService.GetMediaInfoAsync. Calls Meta API: No (reads persisted metadata, not Meta).


GET Media/{messageId}

Purpose: stream the actual media bytes through the backend — the Meta media URL is never exposed to clients. Permission: WhatsAppInbox.MediaView · Request: route messageId Response: binary file stream with the correct Content-Type, or 400 if the message has no media or the download fails. Backend: WhatsAppMediaService.DownloadMediaAsyncIWhatsAppService.DownloadMediaBytesAsync. Calls Meta API: Yes. Angular/Flutter: bind directly to this URL as an <img src>/download link with the auth header attached (e.g. via an interceptor or blob fetch) — never link to a raw Meta CDN URL.


POST Favorite/{conversationId} · POST Unfavorite/{conversationId}

Purpose: star/unstar a conversation for the current user only. Permission: WhatsAppInbox.Favorite Response: Result<Guid>. Backend: WhatsAppFavoriteService.FavoriteAsync / UnfavoriteAsync. Calls Meta API: No.


GET Favorites

Purpose: list the current user's favorited conversations. Permission: WhatsAppInbox.Favorite Response data: [{ "id": "f1...", "conversationId": "c1...", "userId": "...", "createdOn": "2026-07-05T09:00:00Z" }] Backend: WhatsAppFavoriteService.GetFavoritesAsync. Calls Meta API: No.


GET ReadState/{conversationId}

Purpose: per-user read-state detail (last read message, unread count) for a conversation. Permission: WhatsAppInbox.View · Response data: { "id": "r1...", "conversationId": "c1...", "userId": "...", "lastReadMessageId": "m9...", "lastReadOn": "2026-07-05T10:00:00Z", "unreadCount": 0 } Backend: WhatsAppReadStateService.GetReadStateAsync. Calls Meta API: No.


GET CustomerProfile/{customerWaId}

Purpose: aggregate customer profile across every conversation ever had with a WhatsApp ID. Permission: WhatsAppInbox.CustomerProfile · Request: route customerWaId:string Response data (abbreviated):

{ "customerWaId": "966501112222", "name": "Ahmed Al-Otaibi", "totalConversations": 4,
"openConversations": 1, "closedConversations": 3, "totalMessages": 62, "inboundMessages": 31,
"outboundMessages": 31, "tags": ["VIP", "Wholesale"], "averageFirstResponseSeconds": 88.2 }

Backend: WhatsAppInboxService.GetCustomerProfileAsync. Calls Meta API: No. Angular/Flutter: the "Customer" side panel next to an open conversation.


GET Export/{conversationId}/json

Purpose: structured JSON export of a full conversation (messages + notes) for archiving/compliance. Permission: WhatsAppInbox.Export · Response: Result<WhatsAppConversationExportDto> — full conversation metadata plus messages[] and notes[] arrays. Backend: WhatsAppInboxService.GetConversationExportJsonAsync. Calls Meta API: No.

GET Export/{conversationId}/text

Purpose: plain-text export of the same conversation, for sharing/printing. Permission: WhatsAppInbox.Export · Response: text/plain file download, or 400 if the conversation has no messages. Backend: WhatsAppInboxService.GetConversationExportTextAsync. Calls Meta API: No.