joinVoice() mints a credential and stops. It sends and receives no media. Audio is the separate, opt-in layer described in Publishing audio, which takes conn.url and conn.token as its input.Permissions
Two grants get the bot in, and the second is not the one you would guess.1
voice_connect, server-wide
Checked across the whole server.
joinVoice() pre-checks it locally when a verdict has already arrived and refuses early, but the server stays authoritative.2
Channel-level view
The view check for a voice channel runs through the backend’s permission ladder, whose server baseline column is
view_text, not view_voice. Declaring view_voice alone is not enough.joinVoice
string
required
The server the voice channel belongs to.
joinVoice() moves the server cursor for you.string
required
The voice channel to join.
string
The channel’s group. Resolved from the server’s channel list when omitted.
boolean
default:"false"
Join with the self-mute flag set.
boolean
default:"false"
Join with the self-deafen flag set. Deafened implies muted, mirroring both shipped Cloak clients. Join deafened when the bot only publishes: the SFU then forwards it nothing at all.
CloakActionError carrying the backend’s code on any failure. See Handling errors.
VoiceConnection
url defaults to the same host as the transport on port 7880, derived by the exported livekitUrlFor() helper. Override it with ClientOptions.livekitUrl when your deployment puts the SFU somewhere else.
leaveVoice
voiceConnection
null. A synchronous cache read.
setVoiceSelfState
voiceConnection() is null. The backend broadcasts this and never acknowledges it, so the local state updates optimistically. Deafening implies muting.
voiceRoster
VoiceParticipant
refreshVoiceRoster
The voice events
Five events, all of which fire for the bot’s own actions too.Filter the bot’s own echo with
p.userId === client.user?.id where it matters. The voiceJoin payload is the live roster entry, so later updates mutate it in place.voiceDisconnect fires, voiceConnection() is already cleared and the SDK does not auto-rejoin. Decide for yourself whether rejoining is right.
Finding which channel a member is in
Because the roster is keyed by channel, keep your own reverse index offvoiceJoin and voiceLeave.
A complete control-plane bot
This bot runs with no native dependency at all. It answers commands in any text channel it can see.msg.channel.send() rather than msg.reply(), because these are plain answers and a true reply fires a mute-piercing “replied to you” notification. And it does not declare voice_listen, so it is deaf: it can publish (with the audio layer) and can hear nothing.
send() and channel.send() are fire-and-forget. A server-side denial does not reject the promise, it arrives on the sendRejected event. See Sending messages.Through the guild handle
Both server-scoped voice methods are bound onclient.guild(serverId).
leaveVoice(), voiceConnection(), voiceRoster(), and setVoiceSelfState() stay on the client, because a bot holds at most one voice membership at a time. See The guild handle.
Next
Voice keys and scopes
Where the frame key comes from, and why installing the wrong one is silent.
Publishing audio
Turn
conn.url and conn.token into sound in the channel.Voice reference
Every voice export, type, and constant.
Events
The complete client event map.