Compare commits

..
Author SHA1 Message Date
Muhsin e59df461f8 feat: Change API 2026-01-30 15:18:10 +04:00
Muhsin 077dcdd659 Revert "feat: Add APIs"
This reverts commit 49acab85f5.
2026-01-30 12:13:48 +04:00
Muhsin 49acab85f5 feat: Add APIs 2026-01-29 19:04:17 +04:00
60 changed files with 248 additions and 3452 deletions
+1 -1
View File
@@ -144,7 +144,7 @@ jobs:
# Backend tests with parallelization
backend-tests:
<<: *defaults
parallelism: 20
parallelism: 16
steps:
- checkout
- node/install:
-1
View File
@@ -276,4 +276,3 @@ AZURE_APP_SECRET=
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
# REDIS_ALFRED_SIZE=10
# REDIS_VELMA_SIZE=10
@@ -158,7 +158,6 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: message_type,
status: @outgoing_echo ? :delivered : :sent,
source_id: message_identifier,
content: message_content,
sender: @outgoing_echo ? nil : contact,
@@ -167,7 +166,6 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
}
}
params[:content_attributes][:external_echo] = true if @outgoing_echo
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
params
end
@@ -1,68 +0,0 @@
class V2::Reports::FirstResponseTimeDistributionBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account:, params:)
@account = account
@params = params
end
def build
build_distribution
end
private
def build_distribution
results = fetch_aggregated_counts
map_to_channel_types(results)
end
def fetch_aggregated_counts
ReportingEvent
.where(account_id: account.id, name: 'first_response')
.where(range_condition)
.group(:inbox_id)
.select(
:inbox_id,
bucket_case_statements
)
end
def bucket_case_statements
<<~SQL.squish
COUNT(CASE WHEN value < 3600 THEN 1 END) AS bucket_0_1h,
COUNT(CASE WHEN value >= 3600 AND value < 14400 THEN 1 END) AS bucket_1_4h,
COUNT(CASE WHEN value >= 14400 AND value < 28800 THEN 1 END) AS bucket_4_8h,
COUNT(CASE WHEN value >= 28800 AND value < 86400 THEN 1 END) AS bucket_8_24h,
COUNT(CASE WHEN value >= 86400 THEN 1 END) AS bucket_24h_plus
SQL
end
def range_condition
range.present? ? { created_at: range } : {}
end
def inbox_channel_types
@inbox_channel_types ||= account.inboxes.pluck(:id, :channel_type).to_h
end
def map_to_channel_types(results)
results.each_with_object({}) do |row, hash|
channel_type = inbox_channel_types[row.inbox_id]
next unless channel_type
hash[channel_type] ||= empty_buckets
hash[channel_type]['0-1h'] += row.bucket_0_1h
hash[channel_type]['1-4h'] += row.bucket_1_4h
hash[channel_type]['4-8h'] += row.bucket_4_8h
hash[channel_type]['8-24h'] += row.bucket_8_24h
hash[channel_type]['24h+'] += row.bucket_24h_plus
end
end
def empty_buckets
{ '0-1h' => 0, '1-4h' => 0, '4-8h' => 0, '8-24h' => 0, '24h+' => 0 }
end
end
@@ -1,65 +0,0 @@
class V2::Reports::InboxLabelMatrixBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account:, params:)
@account = account
@params = params
end
def build
{
inboxes: filtered_inboxes.map { |inbox| { id: inbox.id, name: inbox.name } },
labels: filtered_labels.map { |label| { id: label.id, title: label.title } },
matrix: build_matrix
}
end
private
def filtered_inboxes
@filtered_inboxes ||= begin
inboxes = account.inboxes
inboxes = inboxes.where(id: params[:inbox_ids]) if params[:inbox_ids].present?
inboxes.order(:name).to_a
end
end
def filtered_labels
@filtered_labels ||= begin
labels = account.labels
labels = labels.where(id: params[:label_ids]) if params[:label_ids].present?
labels.order(:title).to_a
end
end
def conversation_filter
filter = { account_id: account.id }
filter[:created_at] = range if range.present?
filter[:inbox_id] = params[:inbox_ids] if params[:inbox_ids].present?
filter
end
def fetch_grouped_counts
label_names = filtered_labels.map(&:title)
return {} if label_names.empty?
ActsAsTaggableOn::Tagging
.joins('INNER JOIN conversations ON taggings.taggable_id = conversations.id')
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
.where(taggable_type: 'Conversation', context: 'labels', conversations: conversation_filter)
.where(tags: { name: label_names })
.group('conversations.inbox_id', 'tags.name')
.count
end
def build_matrix
counts = fetch_grouped_counts
filtered_inboxes.map do |inbox|
filtered_labels.map do |label|
counts[[inbox.id, label.title]] || 0
end
end
end
end
@@ -0,0 +1,81 @@
class V2::Reports::OutgoingMessagesCountBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def build
send("build_by_#{params[:group_by]}")
end
private
def base_messages
scope = account.messages.outgoing.unscope(:order).where(created_at: range)
scope = scope.where.not(sender_type: ['AgentBot', 'Captain::Assistant']) if params[:group_by].to_s == 'agent'
scope
end
def build_by_agent
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.where.not(conversations: { assignee_id: nil })
.group('conversations.assignee_id')
.count
user_names = account.users.where(id: counts.keys).index_by(&:id)
counts.map do |user_id, count|
user = user_names[user_id]
{ id: user_id, name: user&.name, outgoing_messages_count: count }
end
end
def build_by_team
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.where.not(conversations: { team_id: nil })
.group('conversations.team_id')
.count
team_names = account.teams.where(id: counts.keys).index_by(&:id)
counts.map do |team_id, count|
team = team_names[team_id]
{ id: team_id, name: team&.name, outgoing_messages_count: count }
end
end
def build_by_inbox
counts = base_messages
.group(:inbox_id)
.count
inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id)
counts.map do |inbox_id, count|
inbox = inbox_names[inbox_id]
{ id: inbox_id, name: inbox&.name, outgoing_messages_count: count }
end
end
def build_by_label
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id
AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'")
.joins('INNER JOIN tags ON tags.id = taggings.tag_id')
.group('tags.name')
.count
label_ids = account.labels.where(title: counts.keys).index_by(&:title)
counts.map do |label_name, count|
label = label_ids[label_name]
{ id: label&.id, name: label_name, outgoing_messages_count: count }
end
end
end
@@ -62,19 +62,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: bot_metrics
end
def inbox_label_matrix
builder = V2::Reports::InboxLabelMatrixBuilder.new(
account: Current.account,
params: inbox_label_matrix_params
)
render json: builder.build
end
def first_response_time_distribution
builder = V2::Reports::FirstResponseTimeDistributionBuilder.new(
account: Current.account,
params: first_response_time_distribution_params
)
def outgoing_messages_count
builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params)
render json: builder.build
end
@@ -156,17 +145,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
V2::ReportBuilder.new(Current.account, conversation_params).conversation_metrics
end
def inbox_label_matrix_params
{
since: params[:since],
until: params[:until],
inbox_ids: params[:inbox_ids],
label_ids: params[:label_ids]
}
end
def first_response_time_distribution_params
def outgoing_messages_count_params
{
group_by: params[:group_by],
since: params[:since],
until: params[:until]
}
@@ -3,14 +3,12 @@ import { onMounted, computed, ref, toRefs } from 'vue';
import { useTimeoutFn } from '@vueuse/core';
import { provideMessageContext } from './provider.js';
import { useTrack } from 'dashboard/composables';
import { useMapGetter } from 'dashboard/composables/store';
import { emitter } from 'shared/helpers/mitt';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { LocalStorage } from 'shared/helpers/localStorage';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { getInboxIconByType } from 'dashboard/helper/inbox';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
MESSAGE_TYPES,
@@ -141,8 +139,6 @@ const showBackgroundHighlight = ref(false);
const showContextMenu = ref(false);
const { t } = useI18n();
const route = useRoute();
const inboxGetter = useMapGetter('inboxes/getInbox');
const inbox = computed(() => inboxGetter.value(props.inboxId) || {});
/**
* Computes the message variant based on props
@@ -166,10 +162,6 @@ const variant = computed(() => {
if (props.contentAttributes?.isUnsupported)
return MESSAGE_VARIANTS.UNSUPPORTED;
if (props.contentAttributes?.externalEcho) {
return MESSAGE_VARIANTS.AGENT;
}
const isBot = !props.sender || props.sender.type === SENDER_TYPES.AGENT_BOT;
if (isBot && props.messageType === MESSAGE_TYPES.OUTGOING) {
return MESSAGE_VARIANTS.BOT;
@@ -432,18 +424,6 @@ function handleReplyTo() {
}
const avatarInfo = computed(() => {
if (props.contentAttributes?.externalEcho) {
const { name, avatar_url, channel_type, medium } = inbox.value;
const iconName = avatar_url
? null
: getInboxIconByType(channel_type, medium);
return {
name: iconName ? '' : name || t('CONVERSATION.NATIVE_APP'),
src: avatar_url || '',
iconName,
};
}
// If no sender, return bot info
if (!props.sender) {
return {
@@ -471,9 +451,6 @@ const avatarInfo = computed(() => {
});
const avatarTooltip = computed(() => {
if (props.contentAttributes?.externalEcho) {
return t('CONVERSATION.NATIVE_APP_ADVISORY');
}
if (avatarInfo.value.name === '') return '';
return `${t('CONVERSATION.SENT_BY')} ${avatarInfo.value.name}`;
});
@@ -507,7 +484,7 @@ provideMessageContext({
<div
v-if="shouldRenderMessage"
:id="`message${props.id}`"
class="flex w-full mb-2 message-bubble-container"
class="flex mb-2 w-full message-bubble-container"
:data-message-id="props.id"
:class="[
flexOrientationClass,
@@ -7,7 +7,6 @@ import { emitter } from 'shared/helpers/mitt';
import { useMessageContext } from '../provider.js';
import { useI18n } from 'vue-i18n';
import MessageFormatter from 'shared/helpers/MessageFormatter.js';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { MESSAGE_VARIANTS, ORIENTATION } from '../constants';
@@ -81,7 +80,7 @@ const replyToPreview = computed(() => {
const { content, attachments } = inReplyTo.value;
if (content) return new MessageFormatter(content).formattedMessage;
if (content) return content;
if (attachments?.length) {
const firstAttachment = attachments[0];
const fileType = firstAttachment.fileType ?? firstAttachment.file_type;
@@ -108,10 +107,9 @@ const replyToPreview = computed(() => {
class="p-2 -mx-1 mb-2 rounded-lg cursor-pointer bg-n-alpha-black1"
@click="scrollToMessage"
>
<div
v-dompurify-html="replyToPreview"
class="prose prose-bubble line-clamp-2"
/>
<span class="break-all line-clamp-2">
{{ replyToPreview }}
</span>
</div>
<slot />
<MessageMeta
@@ -127,7 +127,6 @@ const validateSingleAction = action => {
'resolve_conversation',
'remove_assigned_team',
'open_conversation',
'pending_conversation',
];
if (
@@ -150,8 +150,7 @@
"ADD_PRIVATE_NOTE": "Add a Private Note",
"CHANGE_PRIORITY": "Change Priority",
"ADD_SLA": "Add SLA",
"OPEN_CONVERSATION": "Open conversation",
"PENDING_CONVERSATION": "Mark conversation as pending"
"OPEN_CONVERSATION": "Open conversation"
},
"MESSAGE_TYPES": {
"INCOMING": "Incoming Message",
@@ -253,8 +253,6 @@
"MESSAGE_ERROR": "Unable to send this message, please try again later",
"SENT_BY": "Sent by:",
"BOT": "Bot",
"NATIVE_APP": "Native app",
"NATIVE_APP_ADVISORY": "This message was sent from the native app. Reply from Chatwoot to maintain the message window.",
"SEND_FAILED": "Couldn't send message! Try again",
"TRY_AGAIN": "retry",
"ASSIGNMENT": {
@@ -116,10 +116,6 @@ export const AUTOMATIONS = {
key: 'open_conversation',
name: 'OPEN_CONVERSATION',
},
{
key: 'pending_conversation',
name: 'PENDING_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
@@ -236,10 +232,6 @@ export const AUTOMATIONS = {
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
},
{
key: 'pending_conversation',
name: 'PENDING_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
@@ -368,10 +360,6 @@ export const AUTOMATIONS = {
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
},
{
key: 'pending_conversation',
name: 'PENDING_CONVERSATION',
},
{
key: 'resolve_conversation',
name: 'RESOLVE_CONVERSATION',
@@ -494,10 +482,6 @@ export const AUTOMATIONS = {
key: 'snooze_conversation',
name: 'SNOOZE_CONVERSATION',
},
{
key: 'pending_conversation',
name: 'PENDING_CONVERSATION',
},
{
key: 'send_webhook_event',
name: 'SEND_WEBHOOK_EVENT',
@@ -684,11 +668,6 @@ export const AUTOMATION_ACTION_TYPES = [
label: 'OPEN_CONVERSATION',
inputType: null,
},
{
key: 'pending_conversation',
label: 'PENDING_CONVERSATION',
inputType: null,
},
{
key: 'send_webhook_event',
label: 'SEND_WEBHOOK_EVENT',
+1 -1
View File
@@ -54,7 +54,7 @@ class Webhooks::TiktokEventsJob < MutexApplicationJob
# Receive real-time notifications if you send a message to a user.
def im_send_msg
# This can be either an echo message or a message sent directly via tiktok application
::Tiktok::MessageService.new(channel: channel, content: content, outgoing_echo: true).perform
::Tiktok::MessageService.new(channel: channel, content: content).perform
end
# Receive real-time notifications if a user outside the European Economic Area (EEA), Switzerland, or the UK sends a message to you.
-50
View File
@@ -9,56 +9,6 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
return
end
if message_echo_event?(params)
handle_message_echo(channel, params)
else
handle_message_events(channel, params)
end
end
# Detects if the webhook is an SMB message echo event (message sent from WhatsApp Business app)
# This is part of WhatsApp coexistence feature where businesses can respond from both
# Chatwoot and the WhatsApp Business app, with messages synced to Chatwoot.
#
# Regular message payload (field: "messages"):
# {
# "entry": [{
# "changes": [{
# "field": "messages",
# "value": {
# "contacts": [{ "wa_id": "919745786257", "profile": { "name": "Customer" } }],
# "messages": [{ "from": "919745786257", "id": "wamid...", "text": { "body": "Hello" } }]
# }
# }]
# }]
# }
#
# Echo message payload (field: "smb_message_echoes"):
# {
# "entry": [{
# "changes": [{
# "field": "smb_message_echoes",
# "value": {
# "message_echoes": [{ "from": "971545296927", "to": "919745786257", "id": "wamid...", "text": { "body": "Hi" } }]
# }
# }]
# }]
# }
#
# Key differences:
# - field: "smb_message_echoes" instead of "messages"
# - message_echoes[] instead of messages[]
# - "from" is the business number, "to" is the contact (reversed from regular messages)
# - No "contacts" array in echo payload
def message_echo_event?(params)
params.dig(:entry, 0, :changes, 0, :field) == 'smb_message_echoes'
end
def handle_message_echo(channel, params)
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params, outgoing_echo: true).perform
end
def handle_message_events(channel, params)
case channel.provider
when 'whatsapp_cloud'
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
+2 -2
View File
@@ -41,8 +41,8 @@ class AutomationRule < ApplicationRecord
def actions_attributes
%w[send_message add_label remove_label send_email_to_team assign_team assign_agent send_webhook_event mute_conversation
send_attachment change_status resolve_conversation open_conversation pending_conversation snooze_conversation change_priority
send_email_transcript add_private_note].freeze
send_attachment change_status resolve_conversation open_conversation snooze_conversation change_priority send_email_transcript
add_private_note].freeze
end
def file_base_data
+1 -2
View File
@@ -344,11 +344,10 @@ class Message < ApplicationRecord
# if the sender is not a user, it's not a human response
# if automation rule id is present, it's not a human response
# if campaign id is present, it's not a human response
# external echo messages are responses sent from the native app (WhatsApp Business, Instagram)
outgoing? &&
content_attributes['automation_rule_id'].blank? &&
additional_attributes['campaign_id'].blank? &&
(sender.is_a?(User) || content_attributes['external_echo'].present?)
sender.is_a?(User)
end
def bot_response?
-4
View File
@@ -22,10 +22,6 @@ class ActionService
@conversation.open!
end
def pending_conversation(_params)
@conversation.pending!
end
def change_status(status)
@conversation.update!(status: status[0])
end
+3 -3
View File
@@ -1,10 +1,11 @@
class Tiktok::MessageService
include Tiktok::MessagingHelpers
pattr_initialize [:channel!, :content!, :outgoing_echo]
pattr_initialize [:channel!, :content!]
def perform
if outgoing_message?
# Skip processing echo messages
message = find_message(tt_conversation_id, tt_message_id)
return if message.present?
end
@@ -38,7 +39,7 @@ class Tiktok::MessageService
updated_at: tt_message_time
)
message.sender = contact_inbox.contact if incoming_message? && !outgoing_echo
message.sender = contact_inbox.contact if incoming_message?
message.status = :delivered if outgoing_message?
create_message_attachments(message)
@@ -90,7 +91,6 @@ class Tiktok::MessageService
attributes = {}
attributes[:in_reply_to_external_id] = tt_referenced_message_id if tt_referenced_message_id
attributes[:is_unsupported] = true unless supported_message?
attributes[:external_echo] = true if outgoing_echo
attributes
end
+1 -2
View File
@@ -66,8 +66,7 @@ class Whatsapp::FacebookApiClient
headers: request_headers,
body: {
override_callback_uri: callback_url,
verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes]
verify_token: verify_token
}.to_json
)
@@ -4,23 +4,18 @@
class Whatsapp::IncomingMessageBaseService
include ::Whatsapp::IncomingMessageServiceHelpers
pattr_initialize [:inbox!, :params!, :outgoing_echo]
pattr_initialize [:inbox!, :params!]
def perform
processed_params
if processed_params.try(:[], :statuses).present?
process_statuses
elsif messages_data.present?
elsif processed_params.try(:[], :messages).present?
process_messages
end
end
# Returns messages array for both regular messages and echo events
def messages_data
@processed_params&.dig(:messages) || @processed_params&.dig(:message_echoes)
end
private
def process_messages
@@ -31,7 +26,7 @@ class Whatsapp::IncomingMessageBaseService
# Multiple webhook event can be received against the same message due to misconfigurations in the Meta
# business manager account. While we have not found the core reason yet, the following line ensure that
# there are no duplicate messages created.
return if find_message_by_source_id(messages_data.first[:id]) || message_under_process?
return if find_message_by_source_id(@processed_params[:messages].first[:id]) || message_under_process?
cache_message_source_id_in_redis
set_contact
@@ -62,7 +57,7 @@ class Whatsapp::IncomingMessageBaseService
end
def create_messages
message = messages_data.first
message = @processed_params[:messages].first
log_error(message) && return if error_webhook_event?(message)
process_in_reply_to(message)
@@ -72,44 +67,20 @@ class Whatsapp::IncomingMessageBaseService
def create_contact_messages(message)
message['contacts'].each do |contact|
# Pass source_id from parent message since contact objects don't have :id
create_message(contact, source_id: message[:id])
create_message(contact)
attach_contact(contact)
@message.save!
end
end
def create_regular_message(message)
create_message(message, source_id: message[:id])
create_message(message)
attach_files
attach_location if message_type == 'location'
@message.save!
end
def set_contact
if outgoing_echo
set_contact_from_echo
else
set_contact_from_message
end
end
def set_contact_from_echo
# For echo messages, contact phone is in the 'to' field
phone_number = messages_data.first[:to]
waid = processed_waid(phone_number)
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: waid,
inbox: inbox,
contact_attributes: { name: "+#{phone_number}", phone_number: "+#{phone_number}" }
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
def set_contact_from_message
contact_params = @processed_params[:contacts]&.first
return if contact_params.blank?
@@ -118,7 +89,7 @@ class Whatsapp::IncomingMessageBaseService
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: waid,
inbox: inbox,
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{messages_data.first[:from]}" }
contact_attributes: { name: contact_params.dig(:profile, :name), phone_number: "+#{@processed_params[:messages].first[:from]}" }
).perform
@contact_inbox = contact_inbox
@@ -144,7 +115,7 @@ class Whatsapp::IncomingMessageBaseService
def attach_files
return if %w[text button interactive location contacts].include?(message_type)
attachment_payload = messages_data.first[message_type.to_sym]
attachment_payload = @processed_params[:messages].first[message_type.to_sym]
@message.content ||= attachment_payload[:caption]
attachment_file = download_attachment_file(attachment_payload)
@@ -162,7 +133,7 @@ class Whatsapp::IncomingMessageBaseService
end
def attach_location
location = messages_data.first['location']
location = @processed_params[:messages].first['location']
location_name = location['name'] ? "#{location['name']}, #{location['address']}" : ''
@message.attachments.new(
account_id: @message.account_id,
@@ -174,17 +145,14 @@ class Whatsapp::IncomingMessageBaseService
)
end
def create_message(message, source_id: nil)
def create_message(message)
@message = @conversation.messages.build(
content: message_content(message),
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: outgoing_echo ? :outgoing : :incoming,
# Set status to :delivered for echo messages to prevent SendReplyJob from trying to send them
status: outgoing_echo ? :delivered : :sent,
sender: outgoing_echo ? nil : @contact,
source_id: (source_id || message[:id]).to_s,
content_attributes: outgoing_echo ? { external_echo: true } : {},
message_type: :incoming,
sender: @contact,
source_id: message[:id].to_s,
in_reply_to_external_id: @in_reply_to_external_id
)
end
@@ -221,7 +189,7 @@ class Whatsapp::IncomingMessageBaseService
end
def contact_name_matches_phone_number?
phone_number = "+#{messages_data.first[:from]}"
phone_number = "+#{@processed_params[:messages].first[:from]}"
formatted_phone_number = TelephoneNumber.parse(phone_number).international_number
@contact.name == phone_number || @contact.name == formatted_phone_number
end
@@ -21,7 +21,7 @@ module Whatsapp::IncomingMessageServiceHelpers
end
def message_type
messages_data.first[:type]
@processed_params[:messages].first[:type]
end
def message_content(message)
@@ -70,19 +70,19 @@ module Whatsapp::IncomingMessageServiceHelpers
end
def message_under_process?
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
Redis::Alfred.get(key)
end
def cache_message_source_id_in_redis
return if messages_data.blank?
return if @processed_params.try(:[], :messages).blank?
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
::Redis::Alfred.setex(key, true)
end
def clear_message_source_id_from_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: messages_data.first[:id])
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: @processed_params[:messages].first[:id])
::Redis::Alfred.delete(key)
end
end
+1 -2
View File
@@ -13,8 +13,7 @@ end
# Velma : Determined protector
# used in rack attack
velma_size = ENV.fetch('REDIS_VELMA_SIZE', 10)
$velma = ConnectionPool.new(size: velma_size, timeout: 1) do
$velma = ConnectionPool.new(size: 5, timeout: 1) do
config = Rails.env.test? ? MockRedis.new : Redis.new(Redis::Config.app)
Redis::Namespace.new('velma', redis: config, warning: true)
end
+1 -2
View File
@@ -444,8 +444,7 @@ Rails.application.routes.draw do
get :conversations_summary
get :conversation_traffic
get :bot_metrics
get :inbox_label_matrix
get :first_response_time_distribution
get :outgoing_messages_count
end
end
resource :year_in_review, only: [:show]
@@ -1,11 +0,0 @@
class AddIndexToReportingEventsForResponseDistribution < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :reporting_events,
[:account_id, :name, :inbox_id, :created_at],
name: 'index_reporting_events_for_response_distribution',
algorithm: :concurrently,
if_not_exists: true
end
end
+1 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do
ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -1115,7 +1115,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_30_061021) do
t.datetime "event_start_time", precision: nil
t.datetime "event_end_time", precision: nil
t.index ["account_id", "name", "created_at"], name: "reporting_events__account_id__name__created_at"
t.index ["account_id", "name", "inbox_id", "created_at"], name: "index_reporting_events_for_response_distribution"
t.index ["account_id"], name: "index_reporting_events_on_account_id"
t.index ["conversation_id"], name: "index_reporting_events_on_conversation_id"
t.index ["created_at"], name: "index_reporting_events_on_created_at"
@@ -1,6 +1,4 @@
class Captain::Tools::BaseTool < RubyLLM::Tool
prepend Captain::Tools::Instrumentation
attr_accessor :assistant
def initialize(assistant, user: nil)
@@ -1,10 +0,0 @@
module Captain::Tools::Instrumentation
extend ActiveSupport::Concern
include Integrations::LlmInstrumentation
def execute(**args)
instrument_tool_call(name, args) do
super
end
end
end
@@ -1,42 +0,0 @@
class Captain::Tools::SearchReplyDocumentationService < RubyLLM::Tool
prepend Captain::Tools::Instrumentation
description 'Search and retrieve documentation/FAQs from knowledge base'
param :query, desc: 'Search Query', required: true
def initialize(account:, assistant: nil)
@account = account
@assistant = assistant
super()
end
def name
'search_documentation'
end
def execute(query:)
Rails.logger.info { "#{self.class.name}: #{query}" }
responses = search_responses(query)
return 'No FAQs found for the given query' if responses.empty?
responses.map { |response| format_response(response) }.join
end
private
def search_responses(query)
if @assistant.present?
@assistant.responses.approved.search(query, account_id: @account.id)
else
@account.captain_assistant_responses.approved.search(query, account_id: @account.id)
end
end
def format_response(response)
result = "\nQuestion: #{response.question}\nAnswer: #{response.answer}\n"
result += "Source: #{response.documentable.external_link}\n" if response.documentable.present? && response.documentable.try(:external_link)
result
end
end
@@ -1,24 +0,0 @@
module Enterprise::Captain::ReplySuggestionService
def make_api_call(model:, messages:, tools: [])
return super unless use_search_tool?
super(model: model, messages: messages, tools: [build_search_tool])
end
private
def use_search_tool?
ChatwootApp.chatwoot_cloud? || ChatwootApp.self_hosted_enterprise?
end
def prompt_variables
return super unless use_search_tool?
super.merge('has_search_tool' => true)
end
def build_search_tool
assistant = conversation&.inbox&.captain_assistant
Captain::Tools::SearchReplyDocumentationService.new(account: account, assistant: assistant)
end
end
+16 -24
View File
@@ -1,6 +1,5 @@
class Captain::BaseTaskService
include Integrations::LlmInstrumentation
include Captain::ToolInstrumentation
# gpt-4o-mini supports 128,000 tokens
# 1 token is approx 4 characters
@@ -36,52 +35,44 @@ class Captain::BaseTaskService
"#{endpoint}/v1"
end
def make_api_call(model:, messages:, tools: [])
def make_api_call(model:, messages:)
# Community edition prerequisite checks
# Enterprise module handles these with more specific error messages (cloud vs self-hosted)
return { error: I18n.t('captain.disabled'), error_code: 403 } unless captain_tasks_enabled?
return { error: I18n.t('captain.api_key_missing'), error_code: 401 } unless api_key_configured?
instrumentation_params = build_instrumentation_params(model, messages)
instrumentation_method = tools.any? ? :instrument_tool_session : :instrument_llm_call
response = send(instrumentation_method, instrumentation_params) do
execute_ruby_llm_request(model: model, messages: messages, tools: tools)
response = instrument_llm_call(instrumentation_params) do
execute_ruby_llm_request(model: model, messages: messages)
end
return response unless build_follow_up_context? && response[:message].present?
response.merge(follow_up_context: build_follow_up_context(messages, response))
# Build follow-up context for client-side refinement, when applicable
if build_follow_up_context? && response[:message].present?
response.merge(follow_up_context: build_follow_up_context(messages, response))
else
response
end
end
def execute_ruby_llm_request(model:, messages:, tools: [])
def execute_ruby_llm_request(model:, messages:)
Llm::Config.with_api_key(api_key, api_base: api_base) do |context|
chat = build_chat(context, model: model, messages: messages, tools: tools)
chat = context.chat(model: model)
system_msg = messages.find { |m| m[:role] == 'system' }
chat.with_instructions(system_msg[:content]) if system_msg
conversation_messages = messages.reject { |m| m[:role] == 'system' }
return { error: 'No conversation messages provided', error_code: 400, request_messages: messages } if conversation_messages.empty?
add_messages_if_needed(chat, conversation_messages)
build_ruby_llm_response(chat.ask(conversation_messages.last[:content]), messages)
response = chat.ask(conversation_messages.last[:content])
build_ruby_llm_response(response, messages)
end
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: account).capture_exception
{ error: e.message, request_messages: messages }
end
def build_chat(context, model:, messages:, tools: [])
chat = context.chat(model: model)
system_msg = messages.find { |m| m[:role] == 'system' }
chat.with_instructions(system_msg[:content]) if system_msg
if tools.any?
tools.each { |tool| chat = chat.with_tool(tool) }
chat.on_end_message { |message| record_generation(chat, message, model) }
end
chat
end
def add_messages_if_needed(chat, conversation_messages)
return if conversation_messages.length == 1
@@ -186,4 +177,5 @@ class Captain::BaseTaskService
user_msg ? user_msg[:content] : nil
end
end
Captain::BaseTaskService.prepend_mod_with('Captain::BaseTaskService')
-2
View File
@@ -38,5 +38,3 @@ class Captain::ReplySuggestionService < Captain::BaseTaskService
'reply_suggestion'
end
end
Captain::ReplySuggestionService.prepend_mod_with('Captain::ReplySuggestionService')
-48
View File
@@ -1,48 +0,0 @@
module Captain::ToolInstrumentation
extend ActiveSupport::Concern
private
# Custom instrumentation for tool flows - outputs just the message (not full hash)
def instrument_tool_session(params)
return yield unless ChatwootApp.otel_enabled?
response = nil
executed = false
tracer.in_span(params[:span_name]) do |span|
span.set_attribute('langfuse.user.id', params[:account_id].to_s) if params[:account_id]
span.set_attribute('langfuse.tags', [params[:feature_name]].to_json)
span.set_attribute('langfuse.observation.input', params[:messages].to_json)
response = yield
executed = true
# Output just the message for cleaner Langfuse display
span.set_attribute('langfuse.observation.output', response[:message] || response.to_json)
end
response
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: account).capture_exception
executed ? response : yield
end
def record_generation(chat, message, model)
return unless ChatwootApp.otel_enabled?
return unless message.respond_to?(:role) && message.role.to_s == 'assistant'
tracer.in_span("llm.#{event_name}.generation") do |span|
span.set_attribute('gen_ai.system', 'openai')
span.set_attribute('gen_ai.request.model', model)
span.set_attribute('gen_ai.usage.input_tokens', message.input_tokens)
span.set_attribute('gen_ai.usage.output_tokens', message.output_tokens) if message.respond_to?(:output_tokens)
span.set_attribute('langfuse.observation.input', format_chat_messages(chat))
span.set_attribute('langfuse.observation.output', message.content.to_s) if message.respond_to?(:content)
end
rescue StandardError => e
Rails.logger.warn "Failed to record generation: #{e.message}"
end
def format_chat_messages(chat)
chat.messages[0...-1].map { |m| { role: m.role.to_s, content: m.content.to_s } }.to_json
end
end
-4
View File
@@ -21,10 +21,6 @@ module ChatwootApp
enterprise? && GlobalConfig.get_value('DEPLOYMENT_ENV') == 'cloud'
end
def self.self_hosted_enterprise?
enterprise? && !chatwoot_cloud? && GlobalConfig.get_value('INSTALLATION_PRICING_PLAN') == 'enterprise'
end
def self.custom?
@custom ||= root.join('custom').exist?
end
@@ -31,10 +31,5 @@ General guidelines:
- Move the conversation forward
- Do not invent product details, policies, or links that weren't mentioned
- Reply in the customer's language
{% if has_search_tool %}
**Important**: You have access to a `search_documentation` tool that can search the company's knowledge base for product details, policies, FAQs, and other information.
**Use the search_documentation tool first** to find relevant information before composing your reply. This ensures your response is accurate and based on actual company documentation.
{% endif %}
Output only the reply.
+3 -3
View File
@@ -2,8 +2,8 @@ module Linear::Mutations
def self.graphql_value(value)
case value
when String
# Strings must be enclosed in double quotes and escaped for GraphQL
value.to_json
# Strings must be enclosed in double quotes
"\"#{value.gsub("\n", '\\n')}\""
when Array
# Arrays need to be recursively converted
"[#{value.map { |v| graphql_value(v) }.join(', ')}]"
@@ -47,7 +47,7 @@ module Linear::Mutations
<<~GRAPHQL
mutation {
attachmentLinkURL(url: #{graphql_value(link)}, issueId: #{graphql_value(issue_id)}, title: #{graphql_value(title)}#{user_params_str}) {
attachmentLinkURL(url: "#{link}", issueId: "#{issue_id}", title: "#{title}"#{user_params_str}) {
success
attachment {
id
@@ -1,145 +0,0 @@
require 'rails_helper'
RSpec.describe V2::Reports::FirstResponseTimeDistributionBuilder do
let!(:account) { create(:account) }
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
let!(:email_inbox) { create(:inbox, account: account, channel: create(:channel_email, account: account)) }
let(:params) do
{
since: 1.week.ago.beginning_of_day.to_i.to_s,
until: Time.current.end_of_day.to_i.to_s
}
end
let(:builder) { described_class.new(account: account, params: params) }
describe '#build' do
subject(:report) { builder.build }
context 'when there are first response events across channels and time buckets' do
before do
# Web Widget: 0-1h bucket (30 minutes = 1800 seconds)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
# Web Widget: 1-4h bucket (2 hours = 7200 seconds)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 7_200, created_at: 2.days.ago)
# Web Widget: 4-8h bucket (6 hours = 21600 seconds)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 21_600, created_at: 3.days.ago)
# Email: 8-24h bucket (12 hours = 43200 seconds)
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
value: 43_200, created_at: 2.days.ago)
# Email: 24h+ bucket (48 hours = 172800 seconds)
create(:reporting_event, account: account, inbox: email_inbox, name: 'first_response',
value: 172_800, created_at: 1.day.ago)
end
it 'returns correct distribution for web widget channel' do
expect(report['Channel::WebWidget']).to eq({
'0-1h' => 1,
'1-4h' => 1,
'4-8h' => 1,
'8-24h' => 0,
'24h+' => 0
})
end
it 'returns correct distribution for email channel' do
expect(report['Channel::Email']).to eq({
'0-1h' => 0,
'1-4h' => 0,
'4-8h' => 0,
'8-24h' => 1,
'24h+' => 1
})
end
end
context 'when filtering by date range' do
before do
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.weeks.ago)
end
it 'only counts events within the date range' do
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
end
end
context 'when there are no first response events' do
it 'returns an empty hash' do
expect(report).to eq({})
end
end
context 'when events belong to another account' do
let(:other_account) { create(:account) }
let(:other_inbox) { create(:inbox, account: other_account) }
before do
create(:reporting_event, account: other_account, inbox: other_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
end
it 'does not include events from other accounts' do
expect(report).to eq({})
end
end
context 'when events have different names' do
before do
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'conversation_resolved',
value: 1_800, created_at: 2.days.ago)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'reply_time',
value: 1_800, created_at: 2.days.ago)
end
it 'only counts first_response events' do
expect(report['Channel::WebWidget']['0-1h']).to eq(1)
end
end
context 'when no date range params are provided' do
let(:params) { {} }
before do
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.months.ago)
end
it 'returns all events without date filtering' do
expect(report['Channel::WebWidget']['0-1h']).to eq(2)
end
end
context 'with boundary values for time buckets' do
before do
# Exactly at 1 hour boundary (should be in 1-4h bucket)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 3_600, created_at: 2.days.ago)
# Just under 1 hour (should be in 0-1h bucket)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 3_599, created_at: 2.days.ago)
# Exactly at 24 hour boundary (should be in 24h+ bucket)
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 86_400, created_at: 2.days.ago)
end
it 'correctly assigns boundary values to buckets' do
expect(report['Channel::WebWidget']).to eq({
'0-1h' => 1,
'1-4h' => 1,
'4-8h' => 0,
'8-24h' => 0,
'24h+' => 1
})
end
end
end
end
@@ -1,135 +0,0 @@
require 'rails_helper'
RSpec.describe V2::Reports::InboxLabelMatrixBuilder do
let!(:account) { create(:account) }
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
let!(:inbox_two) { create(:inbox, account: account, name: 'Web Chat') }
let!(:label_one) { create(:label, account: account, title: 'bug') }
let!(:label_two) { create(:label, account: account, title: 'feature') }
let(:params) do
{
since: 1.week.ago.beginning_of_day.to_i.to_s,
until: Time.current.end_of_day.to_i.to_s
}
end
let(:builder) { described_class.new(account: account, params: params) }
describe '#build' do
subject(:report) { builder.build }
context 'when there are conversations with labels across inboxes' do
before do
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
c1.update(label_list: [label_one.title])
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 3.days.ago)
c2.update(label_list: [label_one.title, label_two.title])
c3 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
c3.update(label_list: [label_two.title])
end
it 'returns inboxes ordered by name' do
expect(report[:inboxes]).to eq([
{ id: inbox_one.id, name: 'Email Support' },
{ id: inbox_two.id, name: 'Web Chat' }
])
end
it 'returns labels ordered by title' do
expect(report[:labels]).to eq([
{ id: label_one.id, title: 'bug' },
{ id: label_two.id, title: 'feature' }
])
end
it 'returns correct conversation counts in the matrix' do
# Email Support: bug=2, feature=1
# Web Chat: bug=0, feature=1
expect(report[:matrix]).to eq([[2, 1], [0, 1]])
end
end
context 'when filtering by inbox_ids' do
let(:params) do
{
since: 1.week.ago.beginning_of_day.to_i.to_s,
until: Time.current.end_of_day.to_i.to_s,
inbox_ids: [inbox_one.id]
}
end
before do
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
c1.update(label_list: [label_one.title])
c2 = create(:conversation, account: account, inbox: inbox_two, created_at: 1.day.ago)
c2.update(label_list: [label_one.title])
end
it 'only includes the specified inboxes and their counts' do
expect(report[:inboxes]).to eq([{ id: inbox_one.id, name: 'Email Support' }])
expect(report[:matrix]).to eq([[1, 0]])
end
end
context 'when filtering by label_ids' do
let(:params) do
{
since: 1.week.ago.beginning_of_day.to_i.to_s,
until: Time.current.end_of_day.to_i.to_s,
label_ids: [label_one.id]
}
end
before do
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
c1.update(label_list: [label_one.title, label_two.title])
end
it 'only includes the specified labels and their counts' do
expect(report[:labels]).to eq([{ id: label_one.id, title: 'bug' }])
expect(report[:matrix]).to eq([[1], [0]])
end
end
context 'when conversations are outside the date range' do
before do
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
c1.update(label_list: [label_one.title])
c2 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.weeks.ago)
c2.update(label_list: [label_one.title])
end
it 'only counts conversations within the date range' do
expect(report[:matrix]).to eq([[1, 0], [0, 0]])
end
end
context 'when there are no conversations with labels' do
before do
create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
end
it 'returns a matrix of zeros' do
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
end
end
context 'when conversations belong to another account' do
let(:other_account) { create(:account) }
let(:other_inbox) { create(:inbox, account: other_account) }
before do
c1 = create(:conversation, account: other_account, inbox: other_inbox, created_at: 2.days.ago)
other_label = create(:label, account: other_account, title: 'bug')
c1.update(label_list: [other_label.title])
end
it 'does not include conversations from other accounts' do
expect(report[:matrix]).to eq([[0, 0], [0, 0]])
end
end
end
end
@@ -197,101 +197,121 @@ RSpec.describe Api::V2::Accounts::ReportsController, type: :request do
end
end
describe 'GET /api/v2/accounts/{account.id}/reports/inbox_label_matrix' do
let!(:inbox_one) { create(:inbox, account: account, name: 'Email Support') }
let!(:label_one) { create(:label, account: account, title: 'bug') }
describe 'GET /api/v2/accounts/{account.id}/reports/outgoing_messages_count' do
let(:since_epoch) { 1.week.ago.to_i.to_s }
let(:until_epoch) { 1.day.from_now.to_i.to_s }
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix"
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as admin' do
let(:agent2) { create(:user, account: account, role: :agent) }
let(:team) { create(:team, account: account) }
let(:inbox2) { create(:inbox, account: account) }
# Separate conversations for agent and team grouping because
# model callbacks clear assignee_id when team is set.
before do
c1 = create(:conversation, account: account, inbox: inbox_one, created_at: 2.days.ago)
c1.update(label_list: [label_one.title])
conv_agent = create(:conversation, account: account, inbox: inbox, assignee: agent)
conv_agent2 = create(:conversation, account: account, inbox: inbox2, assignee: agent2)
conv_team = create(:conversation, account: account, inbox: inbox, team: team)
create_list(:message, 3, account: account, conversation: conv_agent, inbox: inbox, message_type: :outgoing)
create_list(:message, 2, account: account, conversation: conv_agent2, inbox: inbox2, message_type: :outgoing)
create_list(:message, 4, account: account, conversation: conv_team, inbox: inbox, message_type: :outgoing)
# incoming message should not be counted
create(:message, account: account, conversation: conv_agent, inbox: inbox, message_type: :incoming)
end
it 'returns the inbox label matrix' do
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
it 'returns outgoing message counts grouped by agent' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
body = response.parsed_body
expect(body['inboxes']).to be_an(Array)
expect(body['labels']).to be_an(Array)
expect(body['matrix']).to be_an(Array)
agent_entry = data.find { |e| e['id'] == agent.id }
agent2_entry = data.find { |e| e['id'] == agent2.id }
expect(agent_entry['outgoing_messages_count']).to eq(3)
expect(agent2_entry['outgoing_messages_count']).to eq(2)
end
it 'filters by inbox_ids and label_ids' do
get "/api/v2/accounts/#{account.id}/reports/inbox_label_matrix",
params: { inbox_ids: [inbox_one.id], label_ids: [label_one.id] },
it 'returns outgoing message counts grouped by team' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'team', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
body = response.parsed_body
expect(body['inboxes'].length).to eq(1)
expect(body['labels'].length).to eq(1)
end
end
end
describe 'GET /api/v2/accounts/{account.id}/reports/first_response_time_distribution' do
let!(:web_widget_inbox) { create(:inbox, account: account, channel: create(:channel_widget, account: account)) }
context 'when unauthenticated' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as agent' do
it 'returns unauthorized' do
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when authenticated as admin' do
before do
create(:reporting_event, account: account, inbox: web_widget_inbox, name: 'first_response',
value: 1_800, created_at: 2.days.ago)
data = response.parsed_body
expect(data).to be_an(Array)
expect(data.length).to eq(1)
expect(data.first['id']).to eq(team.id)
expect(data.first['outgoing_messages_count']).to eq(4)
end
it 'returns the first response time distribution' do
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
it 'returns outgoing message counts grouped by inbox' do
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'inbox', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
body = response.parsed_body
expect(body).to be_a(Hash)
expect(body['Channel::WebWidget']).to include('0-1h', '1-4h', '4-8h', '8-24h', '24h+')
inbox_entry = data.find { |e| e['id'] == inbox.id }
inbox2_entry = data.find { |e| e['id'] == inbox2.id }
expect(inbox_entry['outgoing_messages_count']).to eq(7)
expect(inbox2_entry['outgoing_messages_count']).to eq(2)
end
it 'returns correct counts in buckets' do
get "/api/v2/accounts/#{account.id}/reports/first_response_time_distribution",
params: { since: 1.week.ago.to_i.to_s, until: Time.current.to_i.to_s },
it 'returns outgoing message counts grouped by label' do
label = create(:label, account: account, title: 'support')
conversation = account.conversations.first
conversation.label_list.add('support')
conversation.save!
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'label', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
body = response.parsed_body
expect(body['Channel::WebWidget']['0-1h']).to eq(1)
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data).to be_an(Array)
expect(data.length).to eq(1)
expect(data.first['id']).to eq(label.id)
expect(data.first['name']).to eq('support')
end
it 'excludes bot messages when grouped by agent' do
bot = create(:agent_bot)
bot_conversation = create(:conversation, account: account, inbox: inbox, assignee: agent)
create(:message, account: account, conversation: bot_conversation, inbox: inbox,
message_type: :outgoing, sender: bot)
get "/api/v2/accounts/#{account.id}/reports/outgoing_messages_count",
params: { group_by: 'agent', since: since_epoch, until: until_epoch },
headers: admin.create_new_auth_token, as: :json
data = response.parsed_body
agent_entry = data.find { |e| e['id'] == agent.id }
# 3 from before block; bot message excluded
expect(agent_entry['outgoing_messages_count']).to eq(3)
end
end
end
@@ -161,6 +161,11 @@ RSpec.describe Captain::BaseTaskService do
end
end
it 'calls execute_ruby_llm_request with correct parameters' do
expect(service).to receive(:execute_ruby_llm_request).with(model: model, messages: messages).and_call_original
service.send(:make_api_call, model: model, messages: messages)
end
it 'instruments the LLM call' do
expect(service).to receive(:instrument_llm_call).and_call_original
service.send(:make_api_call, model: model, messages: messages)
@@ -19,8 +19,6 @@ RSpec.describe Captain::ReplySuggestionService do
mock_context = instance_double(RubyLLM::Context, chat: mock_chat)
allow(Llm::Config).to receive(:with_api_key).and_yield(mock_context)
allow(mock_chat).to receive(:with_tool).and_return(mock_chat)
allow(mock_chat).to receive(:on_end_message).and_return(mock_chat)
allow(mock_chat).to receive(:with_instructions) { |msg| captured_messages << { role: 'system', content: msg } }
allow(mock_chat).to receive(:add_message) { |args| captured_messages << args }
allow(mock_chat).to receive(:ask) do |msg|
@@ -164,8 +164,7 @@ describe Whatsapp::FacebookApiClient do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes] }.to_json
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
)
.to_return(
status: 200,
@@ -185,8 +184,7 @@ describe Whatsapp::FacebookApiClient do
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: { override_callback_uri: callback_url, verify_token: verify_token,
subscribed_fields: %w[messages smb_message_echoes] }.to_json
body: { override_callback_uri: callback_url, verify_token: verify_token }.to_json
)
.to_return(status: 400, body: { error: 'Webhook subscription failed' }.to_json)
end
-10
View File
@@ -225,16 +225,6 @@ agent_conversation_metrics:
$ref: './resource/reports/conversation/agent.yml'
channel_summary:
$ref: './resource/reports/channel_summary.yml'
first_response_time_distribution:
$ref: './resource/reports/first_response_time_distribution.yml'
inbox_label_matrix:
$ref: './resource/reports/inbox_label_matrix.yml'
inbox_summary:
$ref: './resource/reports/inbox_summary.yml'
agent_summary:
$ref: './resource/reports/agent_summary.yml'
team_summary:
$ref: './resource/reports/team_summary.yml'
contact_detail:
$ref: ./resource/contact_detail.yml
@@ -1,39 +0,0 @@
type: array
description: Agent summary report containing conversation statistics grouped by agent.
items:
type: object
properties:
id:
type: number
description: The agent (user) ID
conversations_count:
type: number
description: Number of conversations assigned to the agent during the date range
resolved_conversations_count:
type: number
description: Number of conversations resolved by the agent during the date range
avg_resolution_time:
type: number
nullable: true
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
conversations_count: 150
resolved_conversations_count: 120
avg_resolution_time: 3600
avg_first_response_time: 300
avg_reply_time: 600
- id: 2
conversations_count: 75
resolved_conversations_count: 60
avg_resolution_time: 1800
avg_first_response_time: 180
avg_reply_time: 420
@@ -1,34 +0,0 @@
type: object
description: First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.
additionalProperties:
type: object
description: First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)
properties:
0-1h:
type: number
description: Number of conversations with first response time less than 1 hour
1-4h:
type: number
description: Number of conversations with first response time between 1-4 hours
4-8h:
type: number
description: Number of conversations with first response time between 4-8 hours
8-24h:
type: number
description: Number of conversations with first response time between 8-24 hours
24h+:
type: number
description: Number of conversations with first response time greater than 24 hours
example:
Channel::WebWidget:
0-1h: 150
1-4h: 80
4-8h: 45
8-24h: 30
24h+: 15
Channel::Api:
0-1h: 75
1-4h: 40
4-8h: 20
8-24h: 10
24h+: 5
@@ -1,50 +0,0 @@
type: object
description: Inbox-label matrix report showing the count of conversations for each inbox-label combination.
properties:
inboxes:
type: array
description: List of inboxes included in the report
items:
type: object
properties:
id:
type: number
description: The inbox ID
name:
type: string
description: The inbox name
labels:
type: array
description: List of labels included in the report
items:
type: object
properties:
id:
type: number
description: The label ID
title:
type: string
description: The label title
matrix:
type: array
description: 2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]
items:
type: array
items:
type: number
example:
inboxes:
- id: 1
name: Website Chat
- id: 2
name: Email Support
labels:
- id: 1
title: bug
- id: 2
title: feature-request
- id: 3
title: urgent
matrix:
- [10, 5, 3]
- [8, 12, 2]
@@ -1,39 +0,0 @@
type: array
description: Inbox summary report containing conversation statistics grouped by inbox.
items:
type: object
properties:
id:
type: number
description: The inbox ID
conversations_count:
type: number
description: Number of conversations created in the inbox during the date range
resolved_conversations_count:
type: number
description: Number of conversations resolved in the inbox during the date range
avg_resolution_time:
type: number
nullable: true
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
conversations_count: 150
resolved_conversations_count: 120
avg_resolution_time: 3600
avg_first_response_time: 300
avg_reply_time: 600
- id: 2
conversations_count: 75
resolved_conversations_count: 60
avg_resolution_time: 1800
avg_first_response_time: 180
avg_reply_time: 420
@@ -1,39 +0,0 @@
type: array
description: Team summary report containing conversation statistics grouped by team.
items:
type: object
properties:
id:
type: number
description: The team ID
conversations_count:
type: number
description: Number of conversations assigned to the team during the date range
resolved_conversations_count:
type: number
description: Number of conversations resolved by the team during the date range
avg_resolution_time:
type: number
nullable: true
description: Average time (in seconds) to resolve conversations. Null if no data available.
avg_first_response_time:
type: number
nullable: true
description: Average time (in seconds) for the first response. Null if no data available.
avg_reply_time:
type: number
nullable: true
description: Average time (in seconds) between replies. Null if no data available.
example:
- id: 1
conversations_count: 250
resolved_conversations_count: 200
avg_resolution_time: 2800
avg_first_response_time: 240
avg_reply_time: 500
- id: 2
conversations_count: 180
resolved_conversations_count: 150
avg_resolution_time: 2400
avg_first_response_time: 200
avg_reply_time: 450
+1 -1
View File
@@ -18,6 +18,6 @@
</head>
<body>
<redoc spec-url='/swagger/swagger.json'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2.1.5/bundles/redoc.standalone.js"> </script>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>
@@ -1,23 +0,0 @@
tags:
- Reports
operationId: get-agent-summary-report
summary: Get conversation statistics grouped by agent
security:
- userApiKey: []
description: |
Get conversation statistics grouped by agent for a given date range.
Returns metrics for each agent including conversation counts, resolution counts,
average first response time, average resolution time, and average reply time.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/agent_summary'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -1,24 +0,0 @@
tags:
- Reports
operationId: get-first-response-time-distribution
summary: Get first response time distribution by channel
security:
- userApiKey: []
description: |
Get the distribution of first response times grouped by channel type.
Returns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/first_response_time_distribution'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -1,25 +0,0 @@
tags:
- Reports
operationId: get-inbox-label-matrix
summary: Get inbox-label matrix report
security:
- userApiKey: []
description: |
Get a matrix showing the count of conversations for each inbox-label combination.
Returns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations
in a specific inbox that have a specific label applied.
**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/inbox_label_matrix'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -1,23 +0,0 @@
tags:
- Reports
operationId: get-inbox-summary-report
summary: Get conversation statistics grouped by inbox
security:
- userApiKey: []
description: |
Get conversation statistics grouped by inbox for a given date range.
Returns metrics for each inbox including conversation counts, resolution counts,
average first response time, average resolution time, and average reply time.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/inbox_summary'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
@@ -1,23 +0,0 @@
tags:
- Reports
operationId: get-team-summary-report
summary: Get conversation statistics grouped by team
security:
- userApiKey: []
description: |
Get conversation statistics grouped by team for a given date range.
Returns metrics for each team including conversation counts, resolution counts,
average first response time, average resolution time, and average reply time.
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/team_summary'
'403':
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+5 -114
View File
@@ -653,123 +653,14 @@
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
- in: query
name: business_hours
schema:
type: boolean
description: Whether to filter by business hours.
get:
$ref: './application/reports/channel_summary.yml'
# Inbox summary report
/api/v2/accounts/{account_id}/summary_reports/inbox:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
- in: query
name: business_hours
schema:
type: boolean
description: Whether to calculate metrics using business hours only.
get:
$ref: './application/reports/inbox_summary.yml'
# Agent summary report
/api/v2/accounts/{account_id}/summary_reports/agent:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
- in: query
name: business_hours
schema:
type: boolean
description: Whether to calculate metrics using business hours only.
get:
$ref: './application/reports/agent_summary.yml'
# Team summary report
/api/v2/accounts/{account_id}/summary_reports/team:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
- in: query
name: business_hours
schema:
type: boolean
description: Whether to calculate metrics using business hours only.
get:
$ref: './application/reports/team_summary.yml'
# First response time distribution report
/api/v2/accounts/{account_id}/reports/first_response_time_distribution:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
get:
$ref: './application/reports/first_response_time_distribution.yml'
# Inbox-label matrix report
/api/v2/accounts/{account_id}/reports/inbox_label_matrix:
parameters:
- $ref: '#/components/parameters/account_id'
- in: query
name: since
schema:
type: string
description: The timestamp from where report should start (Unix timestamp).
- in: query
name: until
schema:
type: string
description: The timestamp from where report should stop (Unix timestamp).
- in: query
name: inbox_ids
schema:
type: array
items:
type: integer
description: Filter by specific inbox IDs.
- in: query
name: label_ids
schema:
type: array
items:
type: integer
description: Filter by specific label IDs.
get:
$ref: './application/reports/inbox_label_matrix.yml'
# Conversations Messages
/accounts/{account_id}/conversations/{conversation_id}/messages:
parameters:
+8 -632
View File
@@ -7890,6 +7890,14 @@
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to filter by business hours."
}
],
"get": {
@@ -7938,342 +7946,6 @@
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/inbox": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-inbox-summary-report",
"summary": "Get conversation statistics grouped by inbox",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by inbox for a given date range.\nReturns metrics for each inbox including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/agent": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-agent-summary-report",
"summary": "Get conversation statistics grouped by agent",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by agent for a given date range.\nReturns metrics for each agent including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/agent_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/team": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-team-summary-report",
"summary": "Get conversation statistics grouped by team",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by team for a given date range.\nReturns metrics for each team including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/team_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/reports/first_response_time_distribution": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-first-response-time-distribution",
"summary": "Get first response time distribution by channel",
"security": [
{
"userApiKey": []
}
],
"description": "Get the distribution of first response times grouped by channel type.\nReturns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/first_response_time_distribution"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/reports/inbox_label_matrix": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "inbox_ids",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"description": "Filter by specific inbox IDs."
},
{
"in": "query",
"name": "label_ids",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"description": "Filter by specific label IDs."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-inbox-label-matrix",
"summary": "Get inbox-label matrix report",
"security": [
{
"userApiKey": []
}
],
"description": "Get a matrix showing the count of conversations for each inbox-label combination.\nReturns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations\nin a specific inbox that have a specific label applied.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_label_matrix"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/accounts/{account_id}/conversations/{conversation_id}/messages": {
"parameters": [
{
@@ -12109,302 +11781,6 @@
}
}
},
"first_response_time_distribution": {
"type": "object",
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
"additionalProperties": {
"type": "object",
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
"properties": {
"0-1h": {
"type": "number",
"description": "Number of conversations with first response time less than 1 hour"
},
"1-4h": {
"type": "number",
"description": "Number of conversations with first response time between 1-4 hours"
},
"4-8h": {
"type": "number",
"description": "Number of conversations with first response time between 4-8 hours"
},
"8-24h": {
"type": "number",
"description": "Number of conversations with first response time between 8-24 hours"
},
"24h+": {
"type": "number",
"description": "Number of conversations with first response time greater than 24 hours"
}
}
},
"example": {
"Channel::WebWidget": {
"0-1h": 150,
"1-4h": 80,
"4-8h": 45,
"8-24h": 30,
"24h+": 15
},
"Channel::Api": {
"0-1h": 75,
"1-4h": 40,
"4-8h": 20,
"8-24h": 10,
"24h+": 5
}
}
},
"inbox_label_matrix": {
"type": "object",
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
"properties": {
"inboxes": {
"type": "array",
"description": "List of inboxes included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"name": {
"type": "string",
"description": "The inbox name"
}
}
}
},
"labels": {
"type": "array",
"description": "List of labels included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The label ID"
},
"title": {
"type": "string",
"description": "The label title"
}
}
}
},
"matrix": {
"type": "array",
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"example": {
"inboxes": [
{
"id": 1,
"name": "Website Chat"
},
{
"id": 2,
"name": "Email Support"
}
],
"labels": [
{
"id": 1,
"title": "bug"
},
{
"id": 2,
"title": "feature-request"
},
{
"id": 3,
"title": "urgent"
}
],
"matrix": [
[
10,
5,
3
],
[
8,
12,
2
]
]
}
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations created in the inbox during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved in the inbox during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"agent_summary": {
"type": "array",
"description": "Agent summary report containing conversation statistics grouped by agent.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The agent (user) ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the agent during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the agent during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"team_summary": {
"type": "array",
"description": "Team summary report containing conversation statistics grouped by team.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The team ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the team during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the team during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 250,
"resolved_conversations_count": 200,
"avg_resolution_time": 2800,
"avg_first_response_time": 240,
"avg_reply_time": 500
},
{
"id": 2,
"conversations_count": 180,
"resolved_conversations_count": 150,
"avg_resolution_time": 2400,
"avg_first_response_time": 200,
"avg_reply_time": 450
}
]
},
"contact_detail": {
"type": "object",
"properties": {
+8 -632
View File
@@ -6433,6 +6433,14 @@
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to filter by business hours."
}
],
"get": {
@@ -6480,342 +6488,6 @@
}
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/inbox": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-inbox-summary-report",
"summary": "Get conversation statistics grouped by inbox",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by inbox for a given date range.\nReturns metrics for each inbox including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/agent": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-agent-summary-report",
"summary": "Get conversation statistics grouped by agent",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by agent for a given date range.\nReturns metrics for each agent including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/agent_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/summary_reports/team": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "business_hours",
"schema": {
"type": "boolean"
},
"description": "Whether to calculate metrics using business hours only."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-team-summary-report",
"summary": "Get conversation statistics grouped by team",
"security": [
{
"userApiKey": []
}
],
"description": "Get conversation statistics grouped by team for a given date range.\nReturns metrics for each team including conversation counts, resolution counts,\naverage first response time, average resolution time, and average reply time.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/team_summary"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/reports/first_response_time_distribution": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-first-response-time-distribution",
"summary": "Get first response time distribution by channel",
"security": [
{
"userApiKey": []
}
],
"description": "Get the distribution of first response times grouped by channel type.\nReturns conversation counts in different time buckets (0-1h, 1-4h, 4-8h, 8-24h, 24h+) for each channel type.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/first_response_time_distribution"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v2/accounts/{account_id}/reports/inbox_label_matrix": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
},
{
"in": "query",
"name": "since",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should start (Unix timestamp)."
},
{
"in": "query",
"name": "until",
"schema": {
"type": "string"
},
"description": "The timestamp from where report should stop (Unix timestamp)."
},
{
"in": "query",
"name": "inbox_ids",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"description": "Filter by specific inbox IDs."
},
{
"in": "query",
"name": "label_ids",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"description": "Filter by specific label IDs."
}
],
"get": {
"tags": [
"Reports"
],
"operationId": "get-inbox-label-matrix",
"summary": "Get inbox-label matrix report",
"security": [
{
"userApiKey": []
}
],
"description": "Get a matrix showing the count of conversations for each inbox-label combination.\nReturns a list of inboxes, labels, and a 2D matrix where each cell contains the count of conversations\nin a specific inbox that have a specific label applied.\n\n**Note:** This API endpoint is available only in Chatwoot version 4.11.0 and above.\n",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_label_matrix"
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
}
},
"components": {
@@ -10616,302 +10288,6 @@
}
}
},
"first_response_time_distribution": {
"type": "object",
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
"additionalProperties": {
"type": "object",
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
"properties": {
"0-1h": {
"type": "number",
"description": "Number of conversations with first response time less than 1 hour"
},
"1-4h": {
"type": "number",
"description": "Number of conversations with first response time between 1-4 hours"
},
"4-8h": {
"type": "number",
"description": "Number of conversations with first response time between 4-8 hours"
},
"8-24h": {
"type": "number",
"description": "Number of conversations with first response time between 8-24 hours"
},
"24h+": {
"type": "number",
"description": "Number of conversations with first response time greater than 24 hours"
}
}
},
"example": {
"Channel::WebWidget": {
"0-1h": 150,
"1-4h": 80,
"4-8h": 45,
"8-24h": 30,
"24h+": 15
},
"Channel::Api": {
"0-1h": 75,
"1-4h": 40,
"4-8h": 20,
"8-24h": 10,
"24h+": 5
}
}
},
"inbox_label_matrix": {
"type": "object",
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
"properties": {
"inboxes": {
"type": "array",
"description": "List of inboxes included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"name": {
"type": "string",
"description": "The inbox name"
}
}
}
},
"labels": {
"type": "array",
"description": "List of labels included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The label ID"
},
"title": {
"type": "string",
"description": "The label title"
}
}
}
},
"matrix": {
"type": "array",
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"example": {
"inboxes": [
{
"id": 1,
"name": "Website Chat"
},
{
"id": 2,
"name": "Email Support"
}
],
"labels": [
{
"id": 1,
"title": "bug"
},
{
"id": 2,
"title": "feature-request"
},
{
"id": 3,
"title": "urgent"
}
],
"matrix": [
[
10,
5,
3
],
[
8,
12,
2
]
]
}
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations created in the inbox during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved in the inbox during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"agent_summary": {
"type": "array",
"description": "Agent summary report containing conversation statistics grouped by agent.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The agent (user) ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the agent during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the agent during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"team_summary": {
"type": "array",
"description": "Team summary report containing conversation statistics grouped by team.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The team ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the team during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the team during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 250,
"resolved_conversations_count": 200,
"avg_resolution_time": 2800,
"avg_first_response_time": 240,
"avg_reply_time": 500
},
{
"id": 2,
"conversations_count": 180,
"resolved_conversations_count": 150,
"avg_resolution_time": 2400,
"avg_first_response_time": 200,
"avg_reply_time": 450
}
]
},
"contact_detail": {
"type": "object",
"properties": {
-296
View File
@@ -4424,302 +4424,6 @@
}
}
},
"first_response_time_distribution": {
"type": "object",
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
"additionalProperties": {
"type": "object",
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
"properties": {
"0-1h": {
"type": "number",
"description": "Number of conversations with first response time less than 1 hour"
},
"1-4h": {
"type": "number",
"description": "Number of conversations with first response time between 1-4 hours"
},
"4-8h": {
"type": "number",
"description": "Number of conversations with first response time between 4-8 hours"
},
"8-24h": {
"type": "number",
"description": "Number of conversations with first response time between 8-24 hours"
},
"24h+": {
"type": "number",
"description": "Number of conversations with first response time greater than 24 hours"
}
}
},
"example": {
"Channel::WebWidget": {
"0-1h": 150,
"1-4h": 80,
"4-8h": 45,
"8-24h": 30,
"24h+": 15
},
"Channel::Api": {
"0-1h": 75,
"1-4h": 40,
"4-8h": 20,
"8-24h": 10,
"24h+": 5
}
}
},
"inbox_label_matrix": {
"type": "object",
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
"properties": {
"inboxes": {
"type": "array",
"description": "List of inboxes included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"name": {
"type": "string",
"description": "The inbox name"
}
}
}
},
"labels": {
"type": "array",
"description": "List of labels included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The label ID"
},
"title": {
"type": "string",
"description": "The label title"
}
}
}
},
"matrix": {
"type": "array",
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"example": {
"inboxes": [
{
"id": 1,
"name": "Website Chat"
},
{
"id": 2,
"name": "Email Support"
}
],
"labels": [
{
"id": 1,
"title": "bug"
},
{
"id": 2,
"title": "feature-request"
},
{
"id": 3,
"title": "urgent"
}
],
"matrix": [
[
10,
5,
3
],
[
8,
12,
2
]
]
}
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations created in the inbox during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved in the inbox during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"agent_summary": {
"type": "array",
"description": "Agent summary report containing conversation statistics grouped by agent.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The agent (user) ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the agent during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the agent during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"team_summary": {
"type": "array",
"description": "Team summary report containing conversation statistics grouped by team.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The team ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the team during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the team during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 250,
"resolved_conversations_count": 200,
"avg_resolution_time": 2800,
"avg_first_response_time": 240,
"avg_reply_time": 500
},
{
"id": 2,
"conversations_count": 180,
"resolved_conversations_count": 150,
"avg_resolution_time": 2400,
"avg_first_response_time": 200,
"avg_reply_time": 450
}
]
},
"contact_detail": {
"type": "object",
"properties": {
-296
View File
@@ -3839,302 +3839,6 @@
}
}
},
"first_response_time_distribution": {
"type": "object",
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
"additionalProperties": {
"type": "object",
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
"properties": {
"0-1h": {
"type": "number",
"description": "Number of conversations with first response time less than 1 hour"
},
"1-4h": {
"type": "number",
"description": "Number of conversations with first response time between 1-4 hours"
},
"4-8h": {
"type": "number",
"description": "Number of conversations with first response time between 4-8 hours"
},
"8-24h": {
"type": "number",
"description": "Number of conversations with first response time between 8-24 hours"
},
"24h+": {
"type": "number",
"description": "Number of conversations with first response time greater than 24 hours"
}
}
},
"example": {
"Channel::WebWidget": {
"0-1h": 150,
"1-4h": 80,
"4-8h": 45,
"8-24h": 30,
"24h+": 15
},
"Channel::Api": {
"0-1h": 75,
"1-4h": 40,
"4-8h": 20,
"8-24h": 10,
"24h+": 5
}
}
},
"inbox_label_matrix": {
"type": "object",
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
"properties": {
"inboxes": {
"type": "array",
"description": "List of inboxes included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"name": {
"type": "string",
"description": "The inbox name"
}
}
}
},
"labels": {
"type": "array",
"description": "List of labels included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The label ID"
},
"title": {
"type": "string",
"description": "The label title"
}
}
}
},
"matrix": {
"type": "array",
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"example": {
"inboxes": [
{
"id": 1,
"name": "Website Chat"
},
{
"id": 2,
"name": "Email Support"
}
],
"labels": [
{
"id": 1,
"title": "bug"
},
{
"id": 2,
"title": "feature-request"
},
{
"id": 3,
"title": "urgent"
}
],
"matrix": [
[
10,
5,
3
],
[
8,
12,
2
]
]
}
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations created in the inbox during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved in the inbox during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"agent_summary": {
"type": "array",
"description": "Agent summary report containing conversation statistics grouped by agent.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The agent (user) ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the agent during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the agent during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"team_summary": {
"type": "array",
"description": "Team summary report containing conversation statistics grouped by team.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The team ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the team during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the team during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 250,
"resolved_conversations_count": 200,
"avg_resolution_time": 2800,
"avg_first_response_time": 240,
"avg_reply_time": 500
},
{
"id": 2,
"conversations_count": 180,
"resolved_conversations_count": 150,
"avg_resolution_time": 2400,
"avg_first_response_time": 200,
"avg_reply_time": 450
}
]
},
"contact_detail": {
"type": "object",
"properties": {
-296
View File
@@ -4600,302 +4600,6 @@
}
}
},
"first_response_time_distribution": {
"type": "object",
"description": "First response time distribution report grouped by channel type. Shows the count of conversations with first response times in different time buckets.",
"additionalProperties": {
"type": "object",
"description": "First response time distribution for a specific channel type (e.g., Channel::WebWidget, Channel::Api)",
"properties": {
"0-1h": {
"type": "number",
"description": "Number of conversations with first response time less than 1 hour"
},
"1-4h": {
"type": "number",
"description": "Number of conversations with first response time between 1-4 hours"
},
"4-8h": {
"type": "number",
"description": "Number of conversations with first response time between 4-8 hours"
},
"8-24h": {
"type": "number",
"description": "Number of conversations with first response time between 8-24 hours"
},
"24h+": {
"type": "number",
"description": "Number of conversations with first response time greater than 24 hours"
}
}
},
"example": {
"Channel::WebWidget": {
"0-1h": 150,
"1-4h": 80,
"4-8h": 45,
"8-24h": 30,
"24h+": 15
},
"Channel::Api": {
"0-1h": 75,
"1-4h": 40,
"4-8h": 20,
"8-24h": 10,
"24h+": 5
}
}
},
"inbox_label_matrix": {
"type": "object",
"description": "Inbox-label matrix report showing the count of conversations for each inbox-label combination.",
"properties": {
"inboxes": {
"type": "array",
"description": "List of inboxes included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"name": {
"type": "string",
"description": "The inbox name"
}
}
}
},
"labels": {
"type": "array",
"description": "List of labels included in the report",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The label ID"
},
"title": {
"type": "string",
"description": "The label title"
}
}
}
},
"matrix": {
"type": "array",
"description": "2D array where matrix[i][j] represents the count of conversations in inboxes[i] with labels[j]",
"items": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"example": {
"inboxes": [
{
"id": 1,
"name": "Website Chat"
},
{
"id": 2,
"name": "Email Support"
}
],
"labels": [
{
"id": 1,
"title": "bug"
},
{
"id": 2,
"title": "feature-request"
},
{
"id": 3,
"title": "urgent"
}
],
"matrix": [
[
10,
5,
3
],
[
8,
12,
2
]
]
}
},
"inbox_summary": {
"type": "array",
"description": "Inbox summary report containing conversation statistics grouped by inbox.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The inbox ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations created in the inbox during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved in the inbox during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"agent_summary": {
"type": "array",
"description": "Agent summary report containing conversation statistics grouped by agent.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The agent (user) ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the agent during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the agent during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 150,
"resolved_conversations_count": 120,
"avg_resolution_time": 3600,
"avg_first_response_time": 300,
"avg_reply_time": 600
},
{
"id": 2,
"conversations_count": 75,
"resolved_conversations_count": 60,
"avg_resolution_time": 1800,
"avg_first_response_time": 180,
"avg_reply_time": 420
}
]
},
"team_summary": {
"type": "array",
"description": "Team summary report containing conversation statistics grouped by team.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "number",
"description": "The team ID"
},
"conversations_count": {
"type": "number",
"description": "Number of conversations assigned to the team during the date range"
},
"resolved_conversations_count": {
"type": "number",
"description": "Number of conversations resolved by the team during the date range"
},
"avg_resolution_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) to resolve conversations. Null if no data available."
},
"avg_first_response_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) for the first response. Null if no data available."
},
"avg_reply_time": {
"type": "number",
"nullable": true,
"description": "Average time (in seconds) between replies. Null if no data available."
}
}
},
"example": [
{
"id": 1,
"conversations_count": 250,
"resolved_conversations_count": 200,
"avg_resolution_time": 2800,
"avg_first_response_time": 240,
"avg_reply_time": 500
},
{
"id": 2,
"conversations_count": 180,
"resolved_conversations_count": 150,
"avg_resolution_time": 2400,
"avg_first_response_time": 200,
"avg_reply_time": 450
}
]
},
"contact_detail": {
"type": "object",
"properties": {