Skip to content

Reference

VoltKit RTC API Reference

Every VoltKit RTC endpoint with its request, response, errors and the SDK call that wraps it: rooms, tokens, chat, broadcast, usage and realtime.

The API has two parts. Your backend calls the REST API with an API secret. Your clients connect to the realtime API with a token your backend minted.

Every REST call below is shown three ways: the HTTP request, the reply, and the SDK call that makes it for you. The SDK lines use the Node.js server SDK; nothing requires it, and any HTTP client in any language works.

Making a request

All REST requests go to your API base URL (on your project's Credentials tab) and are authenticated with your API secret. Bodies and replies are JSON.

curl -X POST "$VOLTKIT_RTC_URL/v1/rooms" \
  -H "X-API-Key: $VOLTKIT_RTC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"roomId":"show-42","hostIdentity":"user-7"}'
const { StreamClient } = require("@voltkit-rtc/server");
const rtc = new StreamClient({ baseUrl: process.env.VOLTKIT_RTC_URL, apiKey: process.env.VOLTKIT_RTC_SECRET });

Authorization: Bearer sk_... is accepted as well as X-API-Key.

Times named createdAt, startedAt and ts are milliseconds since the epoch. expiresAt is seconds.

When a request fails

An error is JSON with a machine-readable error and a readable message. Decide on error, show or log message.

{ "error": "room-exists", "message": "a room with that id already exists" }

Any authenticated call can also fail with 401 unauthorized, 403 tenant-suspended, 429 rate-limited or 500 internal. Those are not repeated under each endpoint. Every code, what it means and what to do about it is on the Errors page.

With the SDK, a failure is a StreamError carrying the same code:

try {
  await rtc.createRoom({ roomId: "show-42", hostIdentity: "user-7" });
} catch (err) {
  console.log(err.code, err.status, err.retryable);   // "room-exists" 409 false
}

REST: rooms

Create a room

HTTPPOST /v1/rooms
SDKrtc.createRoom({ roomId, hostIdentity, metadata })

Creates a room and returns a token for its host. roomId is yours to choose (letters, digits, ., -, _, up to 128) and is generated if you leave it out. metadata is any JSON you want every participant to receive when they join.

Request:

{
  "roomId": "show-42",
  "hostIdentity": "user-7",
  "metadata": {
    "layout": "grid"
  }
}

Response, 201 Created:

{
  "roomId": "show-42",
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "originNode": "node-1",
  "state": "live",
  "hostIdentity": "user-7",
  "createdAt": 1791048388384,
  "nodes": [
    "node-1"
  ],
  "participants": 0,
  "metadata": {
    "layout": "grid"
  },
  "hostToken": "eyJhbGciOi...",
  "wsUrl": "wss://rtc.example.com/ws",
  "expiresAt": 1791048988
}

Errors:

Status and errorWhen
400 bad-requesthostIdentity is missing, or roomId has characters that are not allowed
402 quota-exhaustedThe project has nothing left. Also daily-limit-reached, monthly-limit-reached. Anything live is warned, then ended.
409 room-existsYou already have a live room with that id. Use PUT (ensureRoom) if that is fine.
429 room-quotaThe project is at its limit of rooms running at once
503 capacity-exhausted, no-nodesNo capacity for a new room right now. Safe to retry shortly.

Create or get a room

HTTPPUT /v1/rooms/{roomId}
SDKrtc.ensureRoom({ roomId, hostIdentity, metadata })

Creates the room, or returns the live one with a fresh host token if it already exists. This is the call to use when a host may be reconnecting. metadata is only applied when the room is created.

Request:

{
  "hostIdentity": "user-7"
}

Response, 201 Created, or 200 OK if it already existed:

{
  "roomId": "show-42",
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "originNode": "node-1",
  "state": "live",
  "hostIdentity": "user-7",
  "createdAt": 1791048388384,
  "nodes": [
    "node-1"
  ],
  "participants": 0,
  "metadata": {
    "layout": "grid"
  },
  "hostToken": "eyJhbGciOi...",
  "wsUrl": "wss://rtc.example.com/ws",
  "expiresAt": 1791048988
}

Errors:

Status and errorWhen
400 bad-requesthostIdentity is missing
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)
429 room-quotaAt the limit of rooms running at once
503 capacity-exhausted, no-nodesNo capacity right now

Mint a token

HTTPPOST /v1/rooms/{roomId}/tokens
SDKrtc.createToken({ roomId, identity, role })

Lets one user into the room. role is host, cohost, moderator or viewer, and defaults to viewer. Your backend decides the role; the service enforces it. Hand token and wsUrl to the client SDK.

Request:

{
  "identity": "user-99",
  "role": "viewer"
}

Response, 200 OK:

{
  "token": "eyJhbGciOi...",
  "wsUrl": "wss://rtc.example.com/ws",
  "role": "viewer",
  "identity": "user-99",
  "roomId": "show-42",
  "expiresAt": 1791048988
}

Errors:

Status and errorWhen
400 bad-requestidentity is missing
400 bad-rolerole is not one of the four
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)
404 no-roomThe room does not exist or has ended
429 viewer-quotaThe room is at its viewer limit

Get a room

HTTPGET /v1/rooms/{roomId}
SDKrtc.getRoom(roomId)

One room with who is in it and what they are publishing. Poll this for a live viewer count rather than counting webhook events.

Response, 200 OK:

{
  "room": {
    "roomId": "show-42",
    "tenantId": "vk_3f9c1a7b2d4e6f80",
    "originNode": "node-1",
    "state": "live",
    "hostIdentity": "user-7",
    "createdAt": 1791048388384,
    "nodes": [
      "node-1"
    ],
    "participants": 0,
    "metadata": {
      "layout": "grid"
    }
  },
  "participants": [
    {
      "identity": "user-7",
      "role": "host",
      "handRaised": false,
      "muted": false
    }
  ],
  "producers": [
    {
      "producerId": "5d0c...",
      "identity": "user-7",
      "kind": "audio",
      "source": "mic",
      "paused": false
    }
  ]
}

Errors:

Status and errorWhen
404 no-roomThe room does not exist, has ended, or is not yours

List rooms

HTTPGET /v1/rooms
SDKrtc.listRooms()

Every room your project has live right now.

Response, 200 OK:

{
  "rooms": [
    {
      "roomId": "show-42",
      "tenantId": "vk_3f9c1a7b2d4e6f80",
      "originNode": "node-1",
      "state": "live",
      "hostIdentity": "user-7",
      "createdAt": 1791048388384,
      "nodes": [
        "node-1"
      ],
      "participants": 0,
      "metadata": {
        "layout": "grid"
      }
    }
  ]
}

List participants

HTTPGET /v1/rooms/{roomId}/participants
SDKrtc.listParticipants(roomId)

Who is connected, at this moment.

Response, 200 OK:

{
  "participants": [
    {
      "identity": "user-7",
      "role": "host",
      "handRaised": false,
      "muted": false,
      "metadata": {
        "name": "Asha"
      }
    }
  ]
}

Errors:

Status and errorWhen
404 no-roomThe room does not exist or has ended

End a room

HTTPDELETE /v1/rooms/{roomId}
SDKrtc.deleteRoom(roomId)

Ends the room for everyone and clears its chat. Clients receive roomClosed.

Response, 200 OK:

{
  "closed": true
}

Errors:

Status and errorWhen
404 no-roomAlready ended, or never existed

REST: chat

Mint a chat token

HTTPPOST /v1/chat/{channel}/tokens
SDKrtc.createChatToken({ channel, identity, role })

Lets one user into a channel's chat with no media rights. A channel has the same name as the room it belongs to; a channel with no room is chat on its own. Someone already in the room does not need this: chat is on their room connection.

Request:

{
  "identity": "user-99",
  "role": "viewer"
}

Response, 200 OK:

{
  "token": "eyJhbGciOi...",
  "wsUrl": "wss://rtc.example.com/ws/chat",
  "role": "viewer",
  "identity": "user-99",
  "channel": "show-42",
  "expiresAt": 1791048988
}

Errors:

Status and errorWhen
400 bad-requestidentity is missing, or the channel name has characters that are not allowed
400 bad-roleUnknown role
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)
429 viewer-quotaThe channel is at its member limit

Send a message

HTTPPOST /v1/chat/{channel}/messages
SDKrtc.sendMessage({ channel, text, type, from, to, metadata, ephemeral })

Posts a message from your backend: a system notice, a gift, a moderation warning. from defaults to system; you may also speak as one of your own users. to makes it a direct message. ephemeral: true delivers without storing. A message needs text or metadata.

Request:

{
  "text": "The show starts in 5 minutes",
  "type": "system"
}

Response, 201 Created:

{
  "id": "1791048388390-0",
  "channel": "show-42",
  "from": "system",
  "type": "system",
  "text": "The show starts in 5 minutes",
  "ts": 1791048388390
}

Errors:

Status and errorWhen
400 bad-requestNo text or metadata, text too long, or a direct message addressed to its own sender
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)

Read history

HTTPGET /v1/chat/{channel}/messages?limit=50&before={id}
SDKrtc.listMessages(channel, { limit, before })

Messages oldest first. Pass the oldest id you hold as before to page backwards. Your backend sees direct messages too; clients only ever see their own.

Response, 200 OK:

{
  "messages": [
    {
      "id": "1791048388390-0",
      "channel": "show-42",
      "from": "system",
      "type": "system",
      "text": "The show starts in 5 minutes",
      "ts": 1791048388390
    }
  ]
}

Errors:

Status and errorWhen
400 bad-requestbefore is not a message id

Delete a message

HTTPDELETE /v1/chat/{channel}/messages/{messageId}
SDKrtc.deleteMessage(channel, messageId)

Removes it from history and tells every connected client.

Response, 200 OK:

{
  "deleted": true
}

Errors:

Status and errorWhen
404 no-messageNo such message in this channel

Mute or unmute someone

HTTPPUT or DELETE /v1/chat/{channel}/mutes/{identity}
SDKrtc.muteChat(channel, identity) / rtc.unmuteChat(channel, identity)

A muted identity cannot send. The mute survives them reconnecting, and lasts until you remove it or the room ends.

Response, 200 OK:

{
  "identity": "user-13",
  "muted": true
}

List mutes and members

HTTPGET /v1/chat/{channel}/mutes · GET /v1/chat/{channel}
SDKrtc.listChatMutes(channel) / rtc.getChatInfo(channel)

Who is muted, and how many distinct identities are connected to the channel.

Response, 200 OK:

{
  "muted": [
    "user-13"
  ]
}

REST: broadcast

Create a stream

HTTPPOST /v1/streams
SDKrtc.createStream({ streamId, playback, metadata })

Creates a broadcast stream. playback is public (anyone with the URL can watch) or token (each viewer needs a URL you mint). The publish block is returned only here: whipUrl carries the publish token, so treat it like a password.

Request:

{
  "streamId": "show-42",
  "playback": "public"
}

Response, 201 Created:

{
  "streamId": "show-42",
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "streamKey": "9f2c41d07ab35e886c01d4f2",
  "state": "idle",
  "playback": "public",
  "createdAt": 1791048388384,
  "viewers": 0,
  "peakViewers": 0,
  "playbackUrls": {
    "llhls": "https://cdn.example.com/app/9f2c41d07ab35e886c01d4f2/llhls.m3u8",
    "webrtc": "wss://live.example.com/app/9f2c41d07ab35e886c01d4f2"
  },
  "publish": {
    "whipUrl": "https://live.example.com/app/9f2c41d07ab35e886c01d4f2?direction=whip&token=eyJ...",
    "token": "eyJhbGciOi...",
    "expiresAt": 1791052000
  }
}

Errors:

Status and errorWhen
400 bad-requeststreamId is missing or has characters that are not allowed
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)
404 broadcast-disabledBroadcast is not switched on for this service
409 stream-existsYou already have a stream with that id
429 stream-quotaAt the limit of streams

Mint a publish or playback URL

HTTPPOST /v1/streams/{streamId}/tokens
SDKrtc.createStreamToken({ streamId, action, identity })

action is publish (a fresh whipUrl for the broadcaster) or play (playback URLs for one viewer of a token stream).

Request:

{
  "action": "play",
  "identity": "user-99"
}

Response, 200 OK:

{
  "streamId": "show-42",
  "action": "play",
  "token": "eyJhbGciOi...",
  "expiresAt": 1791052000,
  "llhls": "https://cdn.example.com/app/9f2c41d07ab35e886c01d4f2/llhls.m3u8?token=eyJ...",
  "webrtc": "wss://live.example.com/app/9f2c41d07ab35e886c01d4f2?token=eyJ..."
}

Errors:

Status and errorWhen
400 bad-requestaction is not publish or play
402 quota-exhaustedOut of minutes (or daily-limit-reached, monthly-limit-reached)
404 no-streamNo such stream

Get, list, update, delete

HTTPGET /v1/streams/{streamId} · GET /v1/streams · PATCH /v1/streams/{streamId} · DELETE /v1/streams/{streamId}
SDKrtc.getStream(id) / rtc.listStreams() / rtc.updateStream(id, { playback, metadata }) / rtc.deleteStream(id)

state is idle or live. viewers counts playback sessions the media server serves itself; an audience behind a CDN is not in that number. Deleting a stream takes it off air and stops its publish URL working.

Response, 200 OK:

{
  "streamId": "show-42",
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "streamKey": "9f2c41d07ab35e886c01d4f2",
  "state": "live",
  "playback": "public",
  "createdAt": 1791048388384,
  "viewers": 37,
  "peakViewers": 52,
  "playbackUrls": {
    "llhls": "https://cdn.example.com/app/9f2c41d07ab35e886c01d4f2/llhls.m3u8",
    "webrtc": "wss://live.example.com/app/9f2c41d07ab35e886c01d4f2"
  },
  "startedAt": 1791048400000
}

Errors:

Status and errorWhen
404 no-streamNo such stream, or not yours

REST: usage

Usage so far

HTTPGET /v1/usage
SDKrtc.getUsage()

Today, this month and all time, with the balance. minutesRemaining is -1 on an unlimited plan; on a metered project that uses the account balance, it includes what the project can still use from it. allowance breaks the figure down; see Reading your usage.

Response, 200 OK:

{
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "plan": "metered",
  "status": "active",
  "minutesLimit": 6000,
  "usedMinutes": 90,
  "minutesRemaining": 5910,
  "today": {
    "participantSeconds": 5400,
    "publisherSeconds": 3600,
    "roomSeconds": 3600,
    "roomsCreated": 2,
    "peakParticipants": 14,
    "chatMessages": 212,
    "chatConnectionSeconds": 9000,
    "broadcastSeconds": 0,
    "broadcastViewerSeconds": 0,
    "streamsStarted": 0
  },
  "month": {
    "participantSeconds": 5400,
    "publisherSeconds": 3600,
    "roomSeconds": 3600,
    "roomsCreated": 2,
    "peakParticipants": 14,
    "chatMessages": 212,
    "chatConnectionSeconds": 9000,
    "broadcastSeconds": 0,
    "broadcastViewerSeconds": 0,
    "streamsStarted": 0
  },
  "lifetime": {
    "participantSeconds": 5400,
    "publisherSeconds": 3600,
    "roomSeconds": 3600,
    "roomsCreated": 2,
    "peakParticipants": 14,
    "chatMessages": 212,
    "chatConnectionSeconds": 9000,
    "broadcastSeconds": 0,
    "broadcastViewerSeconds": 0,
    "streamsStarted": 0
  }
}

Usage by day

HTTPGET /v1/usage/series?days=30
SDKrtc.getUsageSeries(days)

One entry per day, oldest first, ending today. Days with no usage are present and zero. days is capped at 90.

Response, 200 OK:

{
  "tenantId": "vk_3f9c1a7b2d4e6f80",
  "days": 2,
  "series": [
    {
      "day": "2026-10-02",
      "participantSeconds": 5400,
      "publisherSeconds": 3600,
      "roomSeconds": 3600,
      "roomsCreated": 2,
      "peakParticipants": 14,
      "chatMessages": 212,
      "chatConnectionSeconds": 9000,
      "broadcastSeconds": 0,
      "broadcastViewerSeconds": 0,
      "streamsStarted": 0
    },
    {
      "day": "2026-10-03",
      "participantSeconds": 5400,
      "publisherSeconds": 3600,
      "roomSeconds": 3600,
      "roomsCreated": 2,
      "peakParticipants": 14,
      "chatMessages": 212,
      "chatConnectionSeconds": 9000,
      "broadcastSeconds": 0,
      "broadcastViewerSeconds": 0,
      "streamsStarted": 0
    }
  ]
}

Realtime: connecting

Clients connect to the wsUrl returned with their token:

ConnectionURLToken from
Room: media and chatwss://.../ws?token=...POST /v1/rooms/{roomId}/tokens
Chat onlywss://.../ws/chat?token=...POST /v1/chat/{channel}/tokens

Messages are JSON. A request carries an id, which the reply repeats:

{ "id": 1, "method": "join", "params": { "token": "eyJ..." } }
{ "id": 1, "ok": true, "result": { "roomId": "show-42", "role": "viewer" } }
{ "id": 1, "ok": false, "error": { "code": "forbidden", "message": "role viewer may not publish" } }

The server also sends events, which have no id:

{ "event": "participantJoined", "data": { "identity": "user-99", "role": "viewer" } }

join must be the first request, within 15 seconds of connecting.

Realtime: room methods

MethodParamsWho
jointoken, metadataEveryone, first
leaveEveryone
getRoomStateEveryone. Unlike join, its producers include your own; skip them when subscribing
raiseHandmetadataEveryone
lowerHandidentityYourself; host/moderator may handle another hand. Role requests use metadata such as { "requestRole": "cohost" }
setMetadatametadataYourself
promoteidentity, roleHost grants other roles. Non-hosts may step themselves down to viewer.
forceMuteidentity, kind, pausedHost may manage non-hosts; moderator may manage viewers only. Release permits the owner to turn their device back on.
kickidentity, reasonHost may remove non-hosts; moderator may remove viewers only. Removed identities cannot reuse the old room token.
setRoomMetadatametadata, replacehost, moderator
endRoomhost

Media setup uses createTransport, connectTransport, produce, consume, resumeConsumer, pauseProducer, closeProducer and setPreferredLayers. The SDKs call these for you; if you are writing your own client, ask support for the protocol guide.

A publisher may have one producer per source, allowing camera, screen, microphone, music and screen audio together. videoLayers contains each spatial index and its width/height in produce, consume, room state and producer events. Joined videoPolicy defaults to 1080p/60 fps for every plan. Role, pending-hand and mute state survive reconnects; explicit operator restrictions still apply. See Video quality and Screen sharing.

Realtime: chat methods

Available on both connections.

MethodParamsNotes
sendMessagetext, type, to, metadata, clientMsgId, ephemeralNeeds text or metadata
getMessageslimit, beforeOldest first
deleteMessagemessageIdYour own, or any as a moderator
muteChatidentity, mutedhost, moderator
setTypingtypingLimited to one a second
getChatStateMember count, and whether you are muted

Realtime: events

EventDataWhen
participantJoinedidentity, role, metadataSomeone joined
participantLeftidentity, reasonSomeone left or was removed
newProducerproducerId, identity, kind, source, videoLayersA track is available. Sources: mic, music, camera, screen, screen-audio. Video layers describe actual dimensions for receive presets
producerClosedproducerId, identityA track ended
producerPausedproducerId, identity, kind, pausedSomeone paused or resumed a track: muted their mic, switched their music off
roleChangedidentity, role, byA promotion or demotion
handRaisedidentity, handRaised, metadata, reasonRaised, withdrawn, declined or granted. The roster includes handMetadata while pending
forceMutedby, kind, pausedYour device was blocked or released
participantMutedidentity, kind, muted, byRoster moderation changed. SDK 0.5.0 normalizes wire muted to public paused
participantUpdatedidentity, metadataSomeone's metadata changed
roomMetadataChangedmetadata, byThe room's metadata changed
messageThe messageA chat message, including your own
messageDeletedid, byA message was removed
chatMutedidentity, muted, bySomeone was muted in chat
typingidentity, typingSomeone else is typing
tokenRefreshedtoken, roleA fresh token; use it if you reconnect
mustReconnectreason, token, retryWith retry: true, close the socket yourself and reconnect, with the new token if one is given (promoted, relay-lost, origin-lost, origin-moved). replaced-by-new-session has retry: false and is followed by close code 4001
kickedby, reasonYou were removed
roomClosedreasonThe room ended. reason is closed, or why the project stopped: quota-exhausted, daily-limit-reached, monthly-limit-reached, suspended. The server then closes the connection (code 4003)
usageLimitreason, graceSeconds, endsAtThe project ran out; the session ends at endsAt unless it is topped up. Also sent on chat-only connections

Realtime errors

A refused request is answered with ok: false and an error object holding code and message. The SDKs raise it as RpcError (rooms) or ChatRpcError (chat-only connections). The full list, with what to do about each, is on the Errors page.

codeMeaning
no-token, bad-tokenThe token is missing, expired or not valid for this connection
no-room, room-endedThe room does not exist or is over
not-joinedA request was sent before join
forbiddenYour role does not allow that
no-capacityNo capacity to place you right now. Retry shortly.
chat-mutedA moderator has muted you in this chat
rate-limitedToo many chat messages. Slow down.
no-messageThat message does not exist, or is not yours to see
bad-paramsThe request was malformed. The message says how.
unknown-methodNo such method on this connection

Webhooks

Event types, payloads and signature verification are on the Webhooks page.

Last updated . Something unclear or wrong? Tell us.