Discord Voice Channels: Realtime Voice and Voice Messages

Covers realtime Discord voice channels, auto-join, realtime modes, and voice message attachments. Useful for operators setting up the bot to join and speak in voice channels.

Read this when

  • Setting up a realtime voice conversation in a Discord voice channel
  • Handling inbound Discord voice message attachments

Realtime voice conversations inside Discord voice channels, along with the separate voice message attachment format.

Voice

Discord offers two different voice surfaces: realtime voice channels (continuous conversations) and voice message attachments (the waveform preview format). Both are supported by the gateway.

Voice channels

Setup checklist:

  1. Turn on Message Content Intent in the Discord Developer Portal.
  2. Turn on Server Members Intent when role/user allowlists are in use.
  3. Invite the bot using the bot and applications.commands scopes.
  4. In the target voice channel, grant Connect, Speak, Send Messages, and Read Message History.
  5. Turn on native commands (commands.native or channels.discord.commands.native).
  6. Configure channels.discord.voice.

Sessions are controlled with /vc join|leave|status. This command runs on the account default agent and obeys the same allowlist and group policy rules as every other Discord command. Enter these as slash commands in a Discord message box, not in a shell:

/vc join channel:<voice-channel-id>
/vc status
/vc leave

To check which permissions the bot effectively holds before it joins:

openclaw channels capabilities --channel discord --target channel:<voice-channel-id>

Auto-join example:

{
  channels: {
    discord: {
      voice: {
        enabled: true,
        model: "openai/gpt-6-astra",
        autoJoin: [
          {
            guildId: "123456789012345678",
            channelId: "234567890123456789",
            whenOccupied: true,
          },
        ],
        allowedChannels: [
          {
            guildId: "123456789012345678",
            channelId: "234567890123456789",
          },
        ],
        daveEncryption: true,
        decryptionFailureTolerance: 24,
        connectTimeoutMs: 30000,
        reconnectGraceMs: 15000,
        realtime: {
          provider: "openai",
          model: "gpt-realtime-2.1",
          speakerVoice: "cedar",
        },
      },
    },
  },
}

GPT-Live in Discord

The same GPT-Live model and voice used by Talk can also serve Discord, alongside Google Meet and Voice Call. Provider resolution, native delegation, and interruption policy are shared across these surfaces, though each keeps its own audio transport. Discord configurations that are not pinned retain the provider's existing default; choose GPT-Live explicitly. For the Codex GPT-Live route with cove, sign in using openclaw models auth login --provider openai, then configure:

{
  channels: {
    discord: {
      voice: {
        enabled: true,
        realtime: {
          provider: "openai",
          model: "gpt-live-1-codex",
          speakerVoice: "cove",
        },
      },
    },
  },
}

That route reuses Talk's Gateway-owned WebRTC bridge, trying ChatGPT OAuth first and falling back to a Platform API key. For the public API, pick gpt-live-1 plus a supported public voice such as marin; Discord then uses the Gateway's direct Platform-key WebSocket bridge. Model and voice have to come from the same route: cove is a Codex GPT-Live voice, so pairing it with gpt-live-1 falls back to marin. See OpenAI voice and speech.

Response timing and interruption are owned by GPT-Live. Discord plays its continuous audio without waiting for a completed-response event, and adds no local speaker-start interruption. Microphone audio stays admitted during playback so that GPT-Live itself can hear and handle interruptions. Delegated tasks run on the routed OpenClaw agent with the originating speaker's Discord identity and tool permissions. Room controls clear buffered local playback before they request their spoken result; the continuous provider stream is left open so it can deliver that result. Microphone input is paced continuously by the Gateway, including silence between speaker captures. Playback keeps quiet PCM inside an active stream, including pauses delivered after earlier speech has already played. Once no unheard speech remains for the player's two-second idle grace, accepted audio drains before the room player is released. Speech that arrives during that retirement queues under a fresh resource, so it is not discarded with the old stream. Continuous playback builds a 120 ms startup buffer to absorb brief delivery gaps. A 120 ms startup deadline keeps short replies from waiting for a completed response event.

Host-enforced wake-name gating and a forced consult before every reply are not supported by GPT-Live. Those policies are off in its defaults. Setting requireWakeName: true or consultPolicy: "always" explicitly fails at startup with a configuration error; remove those settings, or choose gpt-realtime-2.1 when host-controlled turns are needed in a shared meeting. Every speaker still gets a separate voice-model connection; shared room context comes from the OpenClaw agent conversation.

Notes:

  • The OpenAI agent-proxy response and wake-name rules listed below need a GA realtime model, for example gpt-realtime-2.1. GPT-Live instead uses the provider-owned response and delegation flow covered earlier.
  • Text-only configurations keep Discord voice off by default. To turn on /vc commands, the voice runtime, and the GuildVoiceStates gateway intent, set channels.discord.voice.enabled=true, or leave an existing channels.discord.voice block in place. channels.discord.intents.voiceStates can override the intent subscription outright; when it is left unset, the effective voice enablement decides.
  • Which conversation path runs is governed by voice.mode. With the default, agent-proxy, a realtime voice front end owns turn timing, interruption, and playback, hands substantive work to the routed OpenClaw agent via openclaw_agent_consult, and treats the outcome as if that speaker had typed a Discord prompt. The older batch STT plus TTS flow is kept by stt-tts. With bidi, the realtime model talks directly while openclaw_agent_consult is exposed for the OpenClaw brain.
  • Because realtime voice gives every speaker a separate provider connection, delayed transcripts and tool calls still carry that speaker's Discord identity. The routed OpenClaw agent conversation and a single room playback queue are shared by everyone. History for direct bidi conversation sits with each speaker's realtime connection; shared room history comes from the OpenClaw agent consult. More speakers means more provider connections may be consumed, and provider account limits still apply. A room holds no more than eight speaker connections, and idle ones are reclaimed after their captures, requests, and playback complete.
  • The OpenClaw conversation that receives voice turns is selected by voice.agentSession. When it is unset, the voice channel's own session is used; setting { mode: "target", target: "channel:<text-channel-id>" } instead makes the voice channel serve as the microphone and speaker extension of an existing Discord text channel session, for example #maintainers.
  • Suppose a realtime voice call closes while an agent reply is still in progress. That reply then completes in the voice channel's built-in text chat, and voice.agentSession.target does not alter where it lands. Cancelling a task explicitly delivers nothing. Should text-in-voice be unavailable, or the bot lack posting permission, OpenClaw writes a warning and discards the reply rather than redirecting it to another channel.
  • To steer a running agent from Discord voice, the backend must be able to recheck the speaker's permissions at dispatch time. Copilot does not have this guarded input capability yet; status and cancellation still work, and a new request can be started.
  • For Discord voice responses and realtime consults, voice.model overrides the OpenClaw agent brain. Inheriting the routed agent model happens when it is left unset. This setting is distinct from voice.realtime.model.
  • With voice.followUsers, the bot can join, move, and leave Discord voice alongside selected users. Refer to Follow users in voice.
  • Speech is routed through discord-voice by agent-proxy, which keeps normal owner/tool authorization for the speaker and the target session while hiding the agent tts tool, since Discord voice handles playback. By default, agent-proxy grants the consult full owner-equivalent tool access for owner speakers (voice.realtime.toolPolicy: "owner"). For models with host-controlled turns, consulting the OpenClaw agent before substantive answers is strongly preferred (voice.realtime.consultPolicy: "always"); GPT-Live relies on provider-owned delegation with consultPolicy: "auto". Under always mode, the realtime layer captures and transcribes speech without auto-speaking filler ahead of the consult answer, then speaks the routed OpenClaw answer. Should several forced consult answers complete while Discord is still playing the first one, the later exact-speech answers are queued until playback goes idle, so speech is never replaced mid-sentence.
  • When Discord playback lags temporarily, realtime voice buffers the generated audio and rides out short provider or network gaps. Buffered audio stays with each provider response, native tool continuations included. Ordinary backpressure will not cancel a response, and queued answers wait for Discord to finish the previous answer even if that answer's provider response or audio encoder has already completed.
  • For GA OpenAI Realtime and xAI, an interruption truncates every retained native audio item at the amount Discord consumed. Items in the queue are dropped at zero, while replies that already finished playing stay untouched. Playback progress carries across temporary gaps within the same response; the echo guard in GA OpenAI Realtime relies on the combined consumed duration of the retained items. GPT-Live interrupts natively and does not truncate items.
  • A failed realtime connection for one speaker leaves the other speakers connected. Look at the realtime speaker failed log, then speak again to establish a new connection. If the initial provider connection fails during /vc join, the join fails, though a recorder that is already connected keeps recording; consult the realtime session failed terminally log and retry /vc join. The Discord voice session is not ended by temporary provider reconnects.
  • Under stt-tts mode, transcription is handled by tools.media.audio; voice.model has no effect on it.
  • stt-tts replies stay active until Discord has played them out, so lengthy responses are not truncated by a fixed one-minute playback limit.
  • For realtime modes, the realtime audio session is configured by voice.realtime.provider, voice.realtime.model, and voice.realtime.speakerVoice. When using OpenAI Realtime 2.1 with the Codex brain, set voice.realtime.model: "gpt-realtime-2.1" and voice.model: "openai/gpt-6-astra".
  • Where the host offers a shared agent-context resolver, realtime voice modes use it. It informs the model that it speaks for an OpenClaw agent with multiple sessions and, by default, includes the small profile files IDENTITY.md, USER.md, and SOUL.md, so quick direct turns preserve the same identity, user grounding, and persona as the routed agent. Customize the profile files by setting voice.realtime.bootstrapContextFiles to a subset, or omit them with [] while retaining the agent-context paragraph. On OpenClaw 2026.9.6 the plugin keeps that host's profile-only context behavior; [] omits the context, and the newer multi-session paragraph is not available. Both paths rely on the host's bootstrap hooks and workspace access. This setting supports only those profile files; AGENTS.md remains in the normal agent context. For workspace work, current facts, memory lookup, or tool-backed actions, the injected profile context is no substitute for openclaw_agent_consult.
  • Wake-name gating in OpenAI agent-proxy realtime mode adapts to the room by default: a single human may talk without a wake name, but two or more humans must begin or end a turn with one. Other bots are not counted as people. Choose voice.realtime.requireWakeName: true to always require a wake name, or false to never require one. A configured wake name must be one or two words. When voice.realtime.wakeNames is unset, OpenClaw uses the routed agent name plus OpenClaw, falling back to the agent id plus OpenClaw. While a wake-name gate is active, realtime provider auto-response is disabled, accepted turns go through the OpenClaw agent consult path, and a short spoken acknowledgement is given when an exact leading wake name is recognized from partial transcription before the final transcript arrives. Fuzzy name matching waits for the final transcript, so an unfinished ordinary word never triggers an acknowledgement. Live joins and leaves are followed without reconnecting voice.
  • Current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events are both accepted by the OpenAI realtime provider, which lets compatible provider snapshots drift without losing assistant audio.
  • With response-based models, voice.realtime.bargeIn decides whether audible microphone input interrupts active realtime playback. Silent packets and speaker-start notifications by themselves do not interrupt it. When unset, the realtime provider's input-audio interruption setting applies. GPT-Live ignores this setting, as it owns interruption.
  • voice.realtime.minBargeInAudioEndMs sets the shortest assistant playback time before a barge-in from GA OpenAI Realtime cuts the audio off. Default: 250. Use 0 to allow instant interruption in rooms with little echo, or increase it where speakers produce heavy echo. GPT-Live is unaffected by this setting.
  • voice.tts takes precedence over tts, but only for stt-tts voice playback; realtime modes rely on voice.realtime.speakerVoice instead. To get an OpenAI voice during Discord playback, configure voice.tts.provider: "openai" and pick a Text-to-speech voice in voice.tts.providers.openai.speakerVoice. On the current OpenAI TTS model, cedar works well when you want a masculine-sounding voice.
  • Discord systemPrompt overrides set per channel apply to the voice transcript turns of that channel.
  • On joining a voice channel, OpenClaw delivers a silent system event to the routed agent session containing the current participant roster. Participants who join or leave afterward update that session without causing an unprompted spoken reply; Discord display names count as untrusted labels. Each authorized voice turn also gets a fresh roster snapshot.
  • Owner status for voice transcript turns and /vc commands comes from Discord entries in commands.ownerAllowFrom. If no Discord command owner exists, the selected Discord account's allowFrom (or the legacy dm.allowFrom) can still grant voice access, though it does not confer owner status. Which tools the agent can see follows the tool policy configured for the routed session.
  • Without any command-owner configuration, authorized speakers may ask the agent to list or change the voice of the current realtime call using talk_voice. The switch affects the shared room and leaves saved voice defaults as they are. Voice control stays tied to the admitted turn and ends when the call finishes, the turn completes or is canceled, or its access policy changes; other owner-only tools keep their normal permissions.
  • Voice turns that were canceled or revoked are never retried as new agent requests. Once a user leaves, pending batch turns cannot begin agent work or speech synthesis. Work already in progress may complete; transcript recording is handled separately.
  • Where voice.autoJoin lists several entries for one guild, OpenClaw joins the last channel configured for that guild.
  • Voice-state changes from other members never overwrite the bot's channel, mute, or deaf settings. Such updates do still feed participant rosters, occupancy-based auto-join, and follow-user behavior.
  • voice.autoJoin[].whenOccupied defaults to false. For an auto-managed room that should hold the bot only while at least one human is present, set it to true. OpenClaw joins when the first human arrives and leaves once the last human departs; the OpenClaw bot and other bots are not counted. Startup, fresh gateway sessions, and resumed gateway sessions all reconcile against Discord's voice-state roster.
  • Occupancy management only owns the sessions it joined itself. A manual /vc join, a standalone transcript-only session, a follow-user session, an active session in a different channel, or any other ad-hoc join is neither moved nor disconnected when the configured room empties. If transcript capture is attached to an occupancy-managed session, that ownership is preserved.
  • voice.allowedChannels is an optional allowlist of rooms. Leaving it unset lets /vc join join any authorized Discord voice channel. Once set, /vc join, startup auto-join, and bot voice-state moves are limited to the listed { guildId, channelId } entries. An empty array denies every Discord voice join. Should Discord move the bot outside the allowlist, OpenClaw exits that channel and, when an auto-join target is available, rejoins it.
  • voice.daveEncryption and voice.decryptionFailureTolerance are forwarded to @discordjs/voice join options; their upstream defaults are daveEncryption=true and decryptionFailureTolerance=24.
  • For Discord voice receive and realtime raw PCM playback, OpenClaw relies on the bundled libopus-wasm codec. A pinned libopus WebAssembly build is included, so no native opus addons are needed. Voice sockets, codecs, playback conversion, and packet pacing for Discord all execute in a worker thread. GPT-Live continuous output goes straight from its media worker to the Discord playback worker instead of passing every audio chunk through the Gateway event loop. Speaker admission, agent work, and transcripts stay on the Gateway, so a busy Gateway can still delay those control operations.
  • DAVE's native encryption pool spawns one Rayon thread per CPU on the first membership rekey and sits outside JavaScript Worker counts. To bound it, operators can set RAYON_NUM_THREADS in the Gateway service environment; OpenClaw does not define this variable. Child processes that inherit that environment are subject to the same limit.
  • voice.connectTimeoutMs sets how long the initial @discordjs/voice Ready wait lasts for /vc join and auto-join attempts. Default: 30000.
  • voice.reconnectGraceMs sets the wait OpenClaw allows for a disconnected voice session to start reconnecting before it destroys that session. Default: 15000.
  • Under stt-tts mode, another user starting to speak does not by itself halt voice playback. To prevent feedback loops, OpenClaw admits no new conversational turns while TTS is playing; a capture that was started explicitly will still record that speech. Wait for playback to end before taking the next conversational turn. When interruption is enabled, response-based realtime models treat audible authorized microphone input as barge-in signals; GPT-Live processes incoming audio on its own.
  • With GA OpenAI Realtime, speaker echo picked up by an open mic can register as barge-in and cut playback. In echo-heavy Discord rooms, set voice.realtime.providers.openai.interruptResponseOnInputAudio: false so the provider does not auto-interrupt on input audio. Add voice.realtime.bargeIn: true if audible Discord microphone input should still interrupt active playback. Playback truncations shorter than voice.realtime.minBargeInAudioEndMs are treated by the GA OpenAI realtime bridge as probable echo or noise: it logs them as skipped rather than clearing Discord playback. None of these controls override GPT-Live's native interruption.
  • voice.captureSilenceGraceMs sets the delay after Discord reports a speaker stopped before OpenClaw finalizes that audio segment for STT. Default: 2000; increase it when Discord breaks ordinary pauses into choppy partial transcripts.
  • When ElevenLabs is the chosen TTS provider, Discord voice playback streams TTS and begins from the provider response stream. Providers lacking streaming support use the synthesized temp-file path instead.
  • OpenClaw monitors receive decrypt failures and recovers automatically: after repeated failures within a short window, it leaves and rejoins the voice channel.
  • Should receive logs keep showing DecryptionFailed(UnencryptedWhenPassthroughDisabled) after an update, gather a dependency report and logs. The bundled @discordjs/voice line carries the upstream padding fix from discord.js PR #11449, which resolved discord.js issue #11419.
  • The operation was aborted receive events are normal when OpenClaw finalizes a captured speaker segment; treat them as verbose diagnostics rather than warnings.
  • Verbose Discord voice logs carry a bounded one-line STT transcript preview for every accepted speaker segment, so debugging reveals both the user side and the agent reply side without dumping unbounded transcript text.
  • Under agent-proxy mode, forced consult fallback discards likely incomplete transcript fragments, such as text ending in ... or a trailing connector like "and", along with complete non-actionable closings like "I'll be right back" or "bye". A request that mentions a closing, such as "write a goodbye email", still reaches the agent. Closing detection relies on a bounded English-language heuristic; wording it does not recognize continues to the agent. When this blocks a stale queued answer, logs show forced agent consult skipped reason=....

Voice messages

Discord voice messages require OGG/Opus audio and display a waveform preview. OpenClaw builds the waveform on its own, but ffmpeg and ffprobe must be present on the gateway host to inspect and convert.

  • Supply a local file path (URLs are refused).
  • Leave out text content (Discord refuses text plus a voice message in one payload).
  • Any audio format is fine; OpenClaw converts it to OGG/Opus when needed.

The agent sends a voice message through the message tool, not from a shell:

message(action="send", channel="discord", target="channel:123", path="/path/to/audio.mp3", asVoice=true)
3,163 words · updated Oct 26, 2026