Keys are delivered, never derived
Your bot is never given a server’s root key, so it cannot compute a voice key at all. A human member seals one to the bot’s identity and the bot fetches it. That is the whole model, and two practical consequences follow from it:- A key can take a moment to arrive. On a cold server the first ask returns “not yet” and the backend arms an online human member to seal one. There is no push notification when that happens. The bot re-asking is the wake path, which
acquireVoiceKey()does for you on a timer. - If nobody with the server key is online, no key arrives.
acquireVoiceKey()rejects after its timeout rather than hanging.
client.voiceKeys
client.voiceKeys is the supported handle on the voice key lane, and it exposes the voice lane and nothing else. That narrow surface is what keeps a media session structurally unable to reach your bot’s message keys.
The interface is named
VoiceKeyAccess in the source, but it is not exported from the package. Do not write an import for it. Reach it through client.voiceKeys and let TypeScript infer the type, or pass it straight into VoiceMediaSession.attach({ keys }).VoiceKey
version is not a message epoch. The voice key descends from the server root, so it survives a bot message-key rotation and changes only when the server owner cycles the root.
acquireVoiceKey failures
acquireVoiceKey() rejects, rather than resolving with an empty key, in two cases:
- Denied. The bot is not a member, or lacks
voice_connect, or holds neithervoice_speaknorvoice_listen. A denial mid-wait (someone revoked the permission while you were waiting) fails the same way. - Timed out. No online human member sealed a key within the window.
The two scopes
Which key your bot gets is the server’s decision, and it decides what the bot can hear. The scope travels with the key: it is an authenticated tag inside the sealed blob, so neither the server nor a relay can lie about it. The SDK never computes a scope, never infers one from a permission verdict, and never defaults one.Always install what voiceFrameKeyFor returns
voiceFrameKeyFor() resolves the scope internally, which is the point: the one place a scope mistake would be silent is the publish path, and scope does not appear there.
- In room scope it derives the per-identity key for any identity, including the bot’s own.
- In participant scope only the bot’s own identity resolves. Asking for anyone else throws, not because a check said no but because there is no material to derive from.
VoiceMediaSession.attach() does this for you correctly. You only touch voiceFrameKeyFor() directly if you are driving a key provider yourself.
Re-pinning: voiceKeyUpdate, never polling
- A root-key cycle replaces the key at a higher version.
- A
voice_listengrant or revoke replaces the scope at the same version, because the backend purges the stored wrap and a member re-seals at the new scope without the version moving.
getVoiceKey() on a timer. The event is the signal.
What per-participant keys do not buy
The accurate list, not the flattering one.- No isolation among humans. Every member holds the room key, so any human client compromise exposes everyone’s audio.
- No sender authentication. Anyone who can decrypt a participant’s frames can also produce frames under that participant’s key. Identity is bound in the voice credential, not in the frame key.
- No traffic-analysis protection. A bot in the channel still learns who speaks, when, and for how long, even holding no usable key.
- Revocation is delivery-gated, not cryptographic. Removing a bot, or revoking its
voice_listen, stops the next delivery. A bot that already unwrapped a key keeps it until the server owner cycles the root key. Do not treat a permission change as a key change.
forgetVoiceKey() is hygiene, not revocation. It drops what this process holds so a rejoin reprovisions rather than reusing a possibly-stale scope.
When a server grants voice_listen
The bot is sealed the room key and derives every participant’s frame key, exactly as a human client does.VoiceMediaSession.attach() then subscribes, installs a derived key for everyone already in the channel and for everyone who arrives later, and reinstalls the whole set on a re-pin. The room key itself is never installed as a frame key, including under the bot’s own identity.
The SDK ships no recorder, no file sink, and no transcription helper. Subscribing is in scope. What your bot does with decrypted member audio is your responsibility and your users’ consent problem.
deriveParticipantVoiceKey
voiceFrameKeyFor() calls it for you in room scope, and it is the only caller. The derivation runs one way only, downward from the room key. There is no function that walks back up, and that absence is exactly what makes a publish-only bot structurally deaf.
Next
Publishing audio
Hand the key to a media session and pump sound into the channel.
Voice runtime
Why a wrong key produces silence and nothing else.
Keys and the keystore
Where your bot’s identity and keys live between runs.
Permission names
All 31 permissions, including
voice_listen.