Channels

Create channel

POST /api/servers/:serverId/channels
FieldTypeRequiredDescription
namestringYesChannel name
typestringNoChannel type (default: text)
descriptionstringNoChannel description
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.

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
const channels = await client.getServerChannels('server-id')
channels = client.get_server_channels("server-id")

Get channel

GET /api/channels/:id
const channel = await client.getChannel('channel-id')
channel = client.get_channel("channel-id")

Update channel

PATCH /api/channels/:id
FieldTypeDescription
namestringChannel name
descriptionstring | nullDescription
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

DELETE /api/channels/:id
await client.deleteChannel('channel-id')
client.delete_channel("channel-id")

Get channel members

GET /api/channels/:id/members
FieldTypeDescription
uidstringUser UID(映射到 user.id
nicknamestringNickname (displayName 优先,否则 username)
avatarstring?Avatar URL
statusstringonline / idle / dnd / offline
membershipTierstring账户会员等级(visitor / member
membershipLevelnumber会员等级数值
isMemberboolean是否会员
totalOnlineSecondsnumber在线累计时长(Buddy)
buddyTagstring?Buddy Tag,来自 Buddy 配置
creatorobject?Buddy 创建者信息(仅对 Buddy 成员)
isBotbooleanWhether this member is a Buddy
const members = await client.getChannelMembers('channel-id')
members = client.get_channel_members("channel-id")

Add member to channel

POST /api/channels/:id/members
FieldTypeDescription
userIdstringUser ID to add
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
await client.removeChannelMember('channel-id', 'user-id')
client.remove_channel_member("channel-id", "user-id")

Reorder channels

PATCH /api/servers/:serverId/channels/positions
FieldTypeDescription
channelIdsstring[]Ordered array of channel IDs
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
FieldTypeDescription
modestringreplyAll, mentionOnly, custom, disabled
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
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).

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.

const result = await client.requestChannelAccess('channel-id')
result = client.request_channel_access("channel-id")

Review channel join request

PATCH /api/channel-join-requests/:requestId
FieldTypeDescription
statusstringapproved or rejected
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.

FieldTypeDescription
reasonstringWhy the channel is being archived
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).

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.

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.

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.