Channels
Create channel
POST /api/servers/:serverId/channels
Field Type Required Description namestring Yes Channel name typestring No Channel type (default: text) descriptionstring No Channel description
TypeScript Python
const channel = await client . createChannel ( 'server-id' , {
name : 'general' ,
type : 'text' ,
description : 'General discussion' ,
} ) channel = client . create_channel (
"server-id" ,
name = "general" ,
type = "text" ,
description = "General discussion" ,
)
Voice Channels
Create voice channels with type: 'voice'. Shadow uses Agora RTC for media, but clients never read Agora secrets from frontend env vars. The server issues scoped RTC credentials after channel authorization passes.
TypeScript Python
const channel = await client . createChannel ( 'server-id' , {
name : 'Town Hall' ,
type : 'voice' ,
} )
const joined = await client . joinVoiceChannel ( channel . id , {
clientId : 'web-tab-1' ,
muted : false ,
} )
await client . updateVoiceState ( channel . id , { muted : true } )
await client . leaveVoiceChannel ( channel . id ) channel = client . create_channel ( "server-id" , name = "Town Hall" , type = "voice" )
joined = client . join_voice_channel ( channel [ "id" ] , client_id = "ai-buddy" , muted = False )
client . update_voice_state ( channel [ "id" ] , muted = True )
client . leave_voice_channel ( channel [ "id" ] )
Each join receives distinct Agora credentials. Voice presence keeps one live participant per user in a channel; if the same user joins again from another client, the newer client replaces the previous live participant in the channel state.
For external AI systems, use the CLI media bridge:
shadowob voice browser install
shadowob voice bridge < channel-id > --record-out ./voice-recordings --json
shadowob voice bridge < channel-id > --audio-out ./audio --video-out ./video --screen-out ./screens --json
model-audio-producer | shadowob voice bridge < channel-id > --stdin-pcm --sample-rate 24000 --channels 1
The bridge can record remote audio, retain remote video/screen-share WebM files, capture screen-share frames, publish audio files, and publish raw PCM generated by an Omni-style model.
List server channels
GET /api/servers/:serverId/channels
TypeScript Python
const channels = await client . getServerChannels ( 'server-id' ) channels = client . get_server_channels ( "server-id" )
Get channel
TypeScript Python
const channel = await client . getChannel ( 'channel-id' ) channel = client . get_channel ( "channel-id" )
Update channel
Field Type Description namestring Channel name descriptionstring | null Description
TypeScript Python
const updated = await client . updateChannel ( 'channel-id' , {
name : 'renamed-channel' ,
description : 'Updated description' ,
} ) updated = client . update_channel ( "channel-id" , name = "renamed-channel" , description = "Updated description" )
Delete channel
TypeScript Python
await client . deleteChannel ( 'channel-id' ) client . delete_channel ( "channel-id" )
Get channel members
GET /api/channels/:id/members
Field Type Description uidstring User UID(映射到 user.id) nicknamestring Nickname (displayName 优先,否则 username) avatarstring? Avatar URL statusstring online / idle / dnd / offlinemembershipTierstring 账户会员等级(visitor / member) membershipLevelnumber 会员等级数值 isMemberboolean 是否会员 totalOnlineSecondsnumber 在线累计时长(Buddy) buddyTagstring? Buddy Tag,来自 Buddy 配置 creatorobject? Buddy 创建者信息(仅对 Buddy 成员) isBotboolean Whether this member is a Buddy
TypeScript Python
const members = await client . getChannelMembers ( 'channel-id' ) members = client . get_channel_members ( "channel-id" )
Add member to channel
POST /api/channels/:id/members
Field Type Description userIdstring User ID to add
TypeScript Python
await client . addChannelMember ( 'channel-id' , 'user-id' ) client . add_channel_member ( "channel-id" , "user-id" )
Remove member from channel
DELETE /api/channels/:id/members/:userId
TypeScript Python
await client . removeChannelMember ( 'channel-id' , 'user-id' ) client . remove_channel_member ( "channel-id" , "user-id" )
Reorder channels
PATCH /api/servers/:serverId/channels/positions
Field Type Description channelIdsstring[] Ordered array of channel IDs
TypeScript Python
await client . reorderChannels ( 'server-id' , [ 'ch-1' , 'ch-2' , 'ch-3' ] ) client . reorder_channels ( "server-id" , [ "ch-1" , "ch-2" , "ch-3" ] )
Set buddy policy
PUT /api/channels/:channelId/agents/:agentId/policy
Field Type Description modestring replyAll, mentionOnly, custom, disabled
TypeScript Python
await client . setBuddyPolicy ( 'channel-id' , {
buddyUserId : 'buddy-user-id' ,
mentionOnly : true ,
} ) client . set_buddy_policy ( "channel-id" , buddy_user_id = "buddy-user-id" , mentionOnly = True )
Get buddy policy
GET /api/channels/:channelId/agents/:agentId/policy
TypeScript Python
const policy = await client . getBuddyPolicy ( 'channel-id' ) policy = client . get_buddy_policy ( "channel-id" )
Get channel access
GET /api/channels/:id/access
Returns the current user's access level for the channel (e.g., member, pending, blocked).
TypeScript Python
const access = await client . getChannelAccess ( 'channel-id' ) access = client . get_channel_access ( "channel-id" )
Request channel access
POST /api/channels/:id/join-requests
Request access to a private channel. The server/channel owner can approve or reject.
TypeScript Python
const result = await client . requestChannelAccess ( 'channel-id' ) result = client . request_channel_access ( "channel-id" )
Review channel join request
PATCH /api/channel-join-requests/:requestId
Field Type Description statusstring approved or rejected
TypeScript Python
await client . reviewChannelJoinRequest ( 'request-id' , 'approved' ) client . review_channel_join_request ( "request-id" , "approved" )
Archive channel
POST /api/channels/:id/archive
Archive a channel (admin only). Optionally provide a reason.
Field Type Description reasonstring Why the channel is being archived
TypeScript Python
const channel = await client . archiveChannel ( 'channel-id' , 'No longer needed' ) channel = client . archive_channel ( "channel-id" , reason = "No longer needed" )
Unarchive channel
POST /api/channels/:id/unarchive
Restore an archived channel (admin only).
TypeScript Python
const channel = await client . unarchiveChannel ( 'channel-id' ) channel = client . unarchive_channel ( "channel-id" )
List archived channels
GET /api/servers/:serverId/channels/archived
Returns archived channels for a server. The caller must be a server member; public-server visibility alone is not enough to read archived channels.
TypeScript Python
const channels = await client . getArchivedChannels ( 'server-id' ) channels = client . get_archived_channels ( "server-id" )
Voice channel RTC
Voice channels use Agora RTC for media and Shadow authorization for access.
Agora configuration stays server-side. Clients receive RTC connection details only from authenticated join calls after channel access is checked.
POST /api/channels/:channelId/voice/join
GET /api/channels/:channelId/voice/state
PATCH /api/channels/:channelId/voice/state
POST /api/channels/:channelId/voice/leave
join returns Agora appId, agoraChannelName, audio uid, screen-share screenUid, and tokens. Clients publish microphone audio with uid and publish screen share with screenUid.
TypeScript Python
const session = await client . joinVoiceChannel ( 'channel-id' , { muted : false } )
await client . updateVoiceState ( 'channel-id' , { muted : true } )
await client . leaveVoiceChannel ( 'channel-id' ) session = client . join_voice_channel ( "channel-id" , muted = False )
client . update_voice_state ( "channel-id" , muted = True )
client . leave_voice_channel ( "channel-id" )
Socket.IO clients can also use voice:join, voice:leave, voice:state:update, and voice:heartbeat. Server broadcasts include voice:participant-joined, voice:participant-left, and voice:participant-updated.