Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jAtInn71/chatwoot-costom/llms.txt

Use this file to discover all available pages before exploring further.

The backend overlay adds per-inbox voice agent configuration to Chatwoot’s Rails application. Two migrations extend the channel_web_widgets table, a model override registers the new attributes and feature flag, two controller overrides add voice-specific endpoints, and view files expose the new fields in the JSON API consumed by the widget and the dashboard.

Database migrations

Two migrations are shipped in custom/backend/migrations/ and copied into /app/db/migrate/ by the Dockerfile. Run them with the standard rails db:migrate (or allow the Rails boot sequence to run them on first start).

20260408000001_add_elevenlabs_to_channel_web_widgets.rb

Adds the elevenlabs_agent_id string column:
class AddElevenlabsToChannelWebWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :channel_web_widgets, :elevenlabs_agent_id, :string, default: nil
  end
end

20260409000001_add_voice_agent_config_to_channel_web_widgets.rb

Adds three columns for provider-agnostic voice configuration:
class AddVoiceAgentConfigToChannelWebWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :channel_web_widgets, :voice_agent_provider, :string, default: 'elevenlabs'
    add_column :channel_web_widgets, :voice_agent_api_key,  :string, default: nil
    add_column :channel_web_widgets, :voice_agent_config_data, :jsonb, default: {}
  end
end
The four columns together form the complete per-inbox voice configuration surface:
ColumnTypePurpose
elevenlabs_agent_idstringElevenLabs public agent ID — also mirrored inside voice_agent_config_data.agent_id
voice_agent_providerstringProvider name, e.g. "elevenlabs"
voice_agent_api_keystringProvider API key, stored server-side only
voice_agent_config_datajsonbFlexible JSON blob: agent_id, voice_id, agent_name, and any future fields

Model — custom/backend/models/web_widget.rb

The model override registers all four columns in EDITABLE_ATTRS and declares the elevenlabs_voice feature flag at bit 5 using FlagShihTzu:
EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline,
                  :reply_time, :pre_chat_form_enabled, :continuity_via_email,
                  :hmac_mandatory, :allowed_domains,
                  { pre_chat_form_options: [...] },
                  { selected_feature_flags: [] }, :elevenlabs_agent_id,
                  :voice_agent_provider, :voice_agent_api_key,
                  :voice_agent_config_data].freeze

has_flags 1 => :attachments,
          2 => :emoji_picker,
          3 => :end_conversation,
          4 => :use_inbox_avatar_for_bot,
          5 => :elevenlabs_voice,
          :column => 'feature_flags',
          :check_for_column => false
The feature flag name is elevenlabs_voice. Toggle it from the dashboard Voice Agent configuration section — the widget checks selected_feature_flags.includes('elevenlabs_voice') to decide whether to show the call button.
voice_agent_config_data is listed as a bare :voice_agent_config_data symbol in EDITABLE_ATTRS — not as a hash with nested keys. Rails strong params silently drops nested hashes for bare-symbol entries, so the dashboard must serialize this field as a JSON string before submitting. The controller re-parses the string back to a hash (see below).

Controllers

custom/backend/controllers/conversations_controller.rb

Overrides Api::V1::Widget::ConversationsController to add three voice-specific actions. The two that do not need a contact record skip the set_contact before-action:
skip_before_action :set_contact, only: [:inbox_config, :voice_signed_url]

inbox_configGET /api/v1/widget/conversations/inbox_config

Returns all voice configuration for the current inbox, keyed by website_token. The widget calls this endpoint every time the iframe panel opens to pick up dashboard changes without a full page reload.
def inbox_config
  @inbox = @web_widget.inbox
  render json: {
    payload: {
      inbox: {
        id: @inbox.id,
        name: @inbox.name,
        selected_feature_flags: @web_widget.selected_feature_flags || [],
        voice_agent_provider:   @web_widget.voice_agent_provider || 'elevenlabs',
        voice_agent_api_key:    @web_widget.voice_agent_api_key || '',
        voice_agent_config_data: @web_widget.voice_agent_config_data || {},
        elevenlabs_agent_id:    @web_widget.elevenlabs_agent_id || ''
      }
    }
  }
end

voice_transcriptPOST /api/v1/widget/conversations/voice_transcript

Appends a single spoken turn from the live ElevenLabs call to the visitor’s Chatwoot conversation. The source parameter is "user" or "ai"; content is the transcribed text. If the visitor starts a voice call before sending any text message, the controller creates a new conversation tagged initiated_from: 'voice_agent':
def voice_transcript
  source  = params[:source].to_s
  content = params[:content].to_s.strip

  return render json: { error: 'invalid source' }, status: :bad_request \
    unless %w[user ai].include?(source)
  return render json: { error: 'empty content' }, status: :bad_request \
    if content.blank?

  conv     = conversation || build_conversation_for_voice
  msg_type = source == 'user' ? :incoming : :outgoing

  msg = conv.messages.create!(
    message_type: msg_type,
    content:      content,
    sender:       source == 'user' ? @contact : nil,
    content_attributes: { voice_transcript: true, role: source }
  )

  render json: { id: msg.id, conversation_id: conv.id }
end
Each message carries content_attributes: { voice_transcript: true, role: source } so the dashboard and reports can style voice turns differently from text messages.

voice_signed_urlGET /api/v1/widget/conversations/voice_signed_url

Exchanges the inbox’s stored ElevenLabs API key for a short-lived signed WebSocket URL. The key never leaves the server — the widget receives only the signed URL, which it passes to ElevenLabs for private-agent sessions.

custom/backend/controllers/inboxes_controller.rb

Overrides Api::V1::Accounts::InboxesController to handle two edge cases in the channel update flow. Feature flags as array. The dashboard may submit selected_feature_flags as a single string instead of an array; the controller normalises it:
if channel_params[:selected_feature_flags].is_a?(String)
  channel_params[:selected_feature_flags] = [channel_params[:selected_feature_flags]]
end
JSON string re-parsing for voice_agent_config_data. Because EDITABLE_ATTRS lists :voice_agent_config_data as a bare symbol, strong params drops any nested hash. The dashboard serialises the value as a JSON string; the controller re-parses it at lines 150–157:
if channel_params[:voice_agent_config_data].is_a?(String)
  begin
    channel_params[:voice_agent_config_data] = JSON.parse(channel_params[:voice_agent_config_data])
  rescue JSON::ParsingError => e
    Rails.logger.error "[VOICE-AGENT] Invalid JSON in config_data: #{e.message}"
    channel_params[:voice_agent_config_data] = {}
  end
end
If you add new nested attributes to voice_agent_config_data, you must keep this round-trip (dashboard serialises → controller deserialises) intact. Sending a raw hash from the dashboard will cause strong params to silently drop the field.

Views

custom/backend/views/_inbox.json.jbuilder

Extends the shared inbox JSON response to include all four voice agent fields. The view also handles the case where voice_agent_config_data is stored as a string rather than a parsed object:
json.selected_feature_flags resource.channel.try(:selected_feature_flags)
json.elevenlabs_agent_id    resource.channel.try(:elevenlabs_agent_id)
json.voice_agent_provider   resource.channel.try(:voice_agent_provider)
json.voice_agent_api_key    resource.channel.try(:voice_agent_api_key)

voice_config = resource.channel.try(:voice_agent_config_data) || {}
if voice_config.is_a?(String)
  begin
    voice_config = JSON.parse(voice_config)
  rescue
    voice_config = {}
  end
end
json.voice_agent_config_data voice_config

custom/backend/views/show.html.erb

A custom widget embedding template at /app/app/views/widgets/show.html.erb. It provides the HTML shell that hosts the Vue widget bundle for the /custom-widget route.

Helper

custom/backend/controllers/concerns/website_token_helper.rb overrides the upstream concern of the same name. It provides token generation and validation for web widget authentication — every widget API request is scoped to a website_token query parameter that this concern resolves to the correct inbox and account.

Routes

custom/backend/routes.rb replaces the upstream routes file in full. The relevant additions are in the widget namespace:
namespace :widget do
  resources :conversations, only: [:index, :create, :update] do
    collection do
      get  :inbox_config
      post :voice_transcript
      get  :voice_signed_url
    end
  end
end
These three collection routes are what the widget calls from ElevenLabsVoiceButton.vue and voiceAgentConfig.js.

Build docs developers (and LLMs) love