A publish-only bot, end to end
The five steps, assuming a logged-inclient. A complete runnable bot is further down the page.
VoiceMediaSession.attach
string
required
VoiceConnection.url.string
required
VoiceConnection.token. A live credential. Never log it.string
required
Your bot’s Cloak user id, from
client.user.id. The media identity is derived from it, and attach() asserts the SFU agrees.string
required
The server the voice channel belongs to. The key derivation binds it.
VoiceKey
required
The scope-tagged key from
acquireVoiceKey(). Used for the scope and the 32-byte length check. Its bytes are not what gets installed.VoiceKeyAccess
required
Where frame keys come from. Pass
client.voiceKeys.boolean
Whether the bot joined deafened. Purely diagnostic: it lets
attach() warn about the one combination that silently breaks a listening bot.number
default:"48000"
number
default:"2"
boolean
default:"false"
Logs which key fingerprint was installed under which identity. Never logs key material.
What attach enforces
Every step fails closed, because every failure mode in this path is otherwise silent.1
Load the engine
An actionable environment error before anything else happens.
2
Validate the key
Exactly 32 bytes, and a scope that came from the authenticated tag. A key that carries no scope is refused rather than defaulted.
3
Connect
Subscription is derived from the scope, never from a free-standing flag.
4
Assert the identity
If the SFU knows this session by a different name than the one the key was sealed for,
attach() fails. Publishing under a name nobody derives a key for produces audio nobody can decrypt.5
Install the bot's own key first
In both scopes.
6
Room scope only: install every other identity
Everyone already in the channel, plus a hook so late joiners get one too.
7
Publish, only now
The local frame encryptor is created at publish time. With no key filed for the bot’s identity, frames are dropped rather than sent in the clear.
Session surface
Playing audio
play() resolves when the resource ends or when stop() is called, and rejects when the source throws. It refuses to start a second resource while one is playing: call stop() first to switch tracks.
Player events: start, end, and error.
A successful
play() proves the bytes reached the encoder. It proves nothing about whether any member decrypted them. See the blind spot.Audio resources
AnAudioResource is any async iterable of interleaved 16-bit PCM, plus its format.
pcmChunks
ffmpegResource
A failed decode is an error, not a short song. An unreadable input makes ffmpeg write nothing to standard output and exit non-zero, which to a naive pump looks exactly like a clean end of stream.
play() checks the exit code and throws a CloakEnvironmentError carrying the head of ffmpeg’s own stderr as its diagnosis. A deliberate stop() is exempt. The error also distinguishes “no audio at all” from “truncated N bytes in”, which usually means a dropped source rather than a wrong argument.
ytdlpResource and resolveMediaUrl
ytdlpResource() is resolveMediaUrl() followed by ffmpegResource(), so it accepts every FfmpegResourceOptions field plus:
--no-playlist is passed by default, so a link to a track that happens to sit in a playlist resolves that one track. When yt-dlp fails, the error’s remedy names the usual suspects in order: it is out of date (it breaks whenever a site changes), the media is private, region-locked or age-gated, or the URL is not a media page at all.
Stopping and tearing down
detach() does not clear keys as a security measure. The process already held them, and forgetting a key it has already used proves nothing. Tearing the room down is, however, the only honest response to a voice_listen revoke, because the engine has no key-removal call.
Listening to members
When a server grantsvoice_listen, the key lane hands your bot the room key and attach() subscribes and installs a derived key for every participant. Two rules change:
- Join non-deafened. The subscribe grant is fixed at join time, so a deafened listening bot hears silence regardless of its keys.
- Handle the scope in your key update handler. A grant or revoke arrives at the same key version, and
setVoiceKey()detaches and throws on a scope change.
A complete music bot
A slash command that joins whichever voice channel the invoker is sitting in.The SDK repo ships this as
examples/dj-bot.ts. Run it with npx tsx examples/dj-bot.ts. The older examples/music-bot.ts (npm run example:music) predates client.voiceKeys and reaches the key lane through an internals seam that is explicitly not stable across versions. Prefer the shape above.Next
Voice runtime
The engine patch, the executables, and a symptom-to-cause table.
Voice keys and scopes
Re-pinning, scope changes, and what the keys do not buy.
Voice reference
Every option type and constant on this page.
Joining voice
The control plane that produces
conn.