Reference
Errors: Every Code and What To Do
Every error VoltKit RTC can return, from the REST API and the realtime connection: what each code means, whether to retry, and how the SDKs surface it.
Every error has a short, stable code. Branch on the code; show or log the message. Codes do not change between releases, messages may be reworded.
A blurry or frozen picture, echo, or a camera the browser will not open is not on this page: those come from the device or the network rather than from an error code. Troubleshooting shows how to tell which, and what to do, including the browser's own camera errors such as NotAllowedError.
How errors arrive
REST API. The HTTP status and a JSON body:
{ "error": "quota-exhausted", "message": "this account has used all of its minutes" }
Realtime connection. The reply to the request that failed:
{ "id": 7, "ok": false, "error": { "code": "forbidden", "message": "role viewer may not publish" } }
SDKs. The same code, on a typed error:
| SDK | Error type | Fields |
|---|---|---|
Node.js (@voltkit-rtc/server) | StreamError | code, status, retryable, plus isNotFound, isOutOfMinutes, isQuota, isCapacityExhausted, isBroadcastDisabled |
Browser (@voltkit-rtc/client) | RpcError, ChatRpcError | code, message |
| Browser, broadcast publishing | WhipError | status, isRefused |
Flutter (voltkit_rtc) | RpcError | code, message |
| Flutter, broadcast publishing | WhipError | status, isRefused |
From the browser and Flutter SDKs 0.7.0 and the Node.js SDK 0.4.0, every SDK reads one registry of codes shared by all VoltKit SDKs. It says whether a code is terminal (a join refused with it is not retried) and whether retrying can help:
| SDK | Registry | Also new |
|---|---|---|
| Browser 0.7.0 | ERROR_CODES, errorInfo(code), isTerminalError(code) | every error the SDK raises is an RpcError with a code; RpcError.data carries what the server attached ({ slots } for slot-occupied) |
| Flutter 0.7.0 | errorCodes, errorInfo(), isTerminalError() | RpcError.info, and RpcError.isTerminal reads the registry |
| Node.js 0.4.0 | ERROR_CODES, REST_ERROR_CODES | StreamError.isBillingUnavailable |
A newer service may send a code an older release does not know. It is passed through unchanged and is never treated as terminal.
// Node.js
try {
await rtc.ensureRoom({ roomId, hostIdentity });
} catch (err) {
if (err.isOutOfMinutes) return res.status(402).send("Top up to go live");
if (err.retryable) return retryLater();
throw err;
}
// Browser
import { RpcError } from "@voltkit-rtc/client";
try {
await client.sendMessage(text);
} catch (err) {
if (err instanceof RpcError && err.code === "chat-muted") showMutedNotice();
else if (err instanceof RpcError && err.code === "rate-limited") showSlowDown();
}
// Flutter
try {
await client.sendMessage(text);
} on RpcError catch (e) {
if (e.code == 'chat-muted') showMutedNotice();
}
REST errors
| Status | error | What happened | What to do |
|---|---|---|---|
400 | bad-request | A field is missing, the wrong type, or not allowed. The message says which. | Fix the request. Do not retry as it is. |
400 | bad-role | role is not host, cohost, moderator or viewer | Send one of the four |
401 | unauthorized | The API secret is missing, wrong or has been revoked | Check the secret on the Credentials tab; issue a new one if it leaked |
402 | quota-exhausted | The project has used every minute it may use (free, its own and the account balance), and every wallet share it may spend is empty | Recharge the VoltKit Wallet, or move credits into the project's shares. Turn on Use account balance for the project if the balance still has minutes, or wait for free minutes to renew. Anything live is warned and then ended. See which minutes are used first |
503 | billing-unavailable | The project is billed from the VoltKit Wallet and no price list is active on the service yet | Contact support. A retry does not help, so the SDKs do not retry it |
402 | daily-limit-reached | The project reached its usage limit for today | Wait for midnight UTC, or ask for a higher limit |
402 | monthly-limit-reached | The project reached its usage limit for this month | Wait for the next month, or ask for a higher limit |
403 | tenant-suspended | The project has been suspended, by support or because the account was disabled | Contact support |
404 | no-room | The room does not exist, has ended, or belongs to another project | Create it, or treat the session as over |
404 | no-stream | No such broadcast stream | Create it |
404 | no-message | No such chat message in that channel | Nothing: it is already gone |
404 | broadcast-disabled | Broadcast is not switched on for this service | Not something a retry fixes |
404 | chat-disabled | Chat is not switched on for this service | Not something a retry fixes |
409 | room-exists | You already have a live room with that id | Use PUT /v1/rooms/{id} (ensureRoom) to get it back instead |
409 | stream-exists | You already have a stream with that id | Use the existing stream, or delete it first |
429 | rate-limited | Too many API calls this minute | Back off and retry. The SDK does this for you. |
429 | room-quota | The project is at its limit of rooms running at once | End rooms that are no longer needed |
429 | viewer-quota | The room or channel is at its member limit | Wait for space, or ask support to raise the limit |
429 | stream-quota | The project is at its limit of streams | Delete streams that are no longer needed |
503 | capacity-exhausted, no-nodes | There is no capacity for a new room right now | Retry shortly. Worth alerting on if it persists. |
503 | unavailable | Something the request depends on could not be checked | Retry shortly |
401 | bad-signature | A signed request or webhook whose signature does not verify | Check the signing secret. The Node.js SDK's parseWebhook raises it for a payload not signed with your webhook secret |
403 | operator-only | That route describes the service's own infrastructure | Not available to projects |
404 | not-found | No such resource | Check the path and the id |
500 | internal | Something failed on our side | Retry once; tell us if it repeats |
401, 402, 403 and 429 rate-limited can come back from any call.
Realtime errors
code | What happened | What to do |
|---|---|---|
no-token, bad-token | The token is missing, expired, or for a different kind of connection (a chat token on a room connection, or the reverse) | Ask your backend for a new token. Do not retry with the same one. |
token-mismatch | refreshToken (updateToken in the SDKs, 0.7.0) sent a valid token for another project, room or identity than this session | Mint one for this room and identity. The session keeps the token it had |
wrong-tenant | Reserved: a token for another project's room (current servers answer bad-token or no-room) | Ask your backend for a new token |
kicked | This identity was removed from the room; tokens minted before cannot rejoin it | Leave the screen. The SDKs stop reconnecting by themselves |
billing-unavailable | The project is billed from the VoltKit Wallet and no price list is active yet | Contact support. The SDKs stop reconnecting by themselves |
not-joined | A request was sent before join finished | Wait for connect() to resolve |
too-many-requests | More than 32 requests are waiting for a reply on one connection | Wait for replies before sending more |
not-in-room | Your participant record is gone: the room is ending or you were removed | Treat the session as over |
no-node | The media server assigned to you went away | Retry; the SDKs reconnect by themselves |
source-already-published | That source (camera, microphone, screen, music) is already published | Stop it before publishing it again |
no-transport, no-producer, no-consumer | The media server no longer knows that transport, track or subscription: it closed | Refresh your state; the SDKs handle it |
consumer-cap, producer-cap, room-cap | The media server refused rather than degrade everyone | Retry shortly |
draining | The media server is being taken out of service | Retry; you are placed elsewhere |
chat-unavailable | Chat is not switched on for this service | Not something a retry fixes |
already-joined | join was sent twice on one connection | Nothing: you are in |
no-room, room-ended | The room does not exist or is over | Leave the screen. The SDK stops reconnecting by itself. |
participant-quota | A free testing trial room has 25 participants, or an explicit operator limit has been reached | Wait for a participant to leave. The first minute top-up for the trial project, or for the account balance it uses, removes the trial restriction; paid bundles do not set room caps. See trial conditions. |
quota-exhausted, daily-limit-reached, monthly-limit-reached, tenant-suspended | The project cannot use the service right now. join refuses even a token minted earlier, so an ended session cannot be rejoined | Top up or wait for the limit to reset. The SDKs stop reconnecting by themselves. |
forbidden | Your role does not allow that: a viewer publishing, a viewer muting someone | Hide the control for that role |
force-muted | A moderator muted you, and you tried to unmute | Wait until a moderator releases it |
no-participant | The person you named is not in the room | Refresh the roster |
no-capacity | No media server can take you right now | Retry shortly |
node-error | The media server refused or failed the request | Retry once; reconnect if it repeats |
cannot-consume | This viewer's browser cannot play the codec a host sends, usually H.264 from a phone on a browser built without it | Not something a retry fixes. Suggest Chrome, Edge, Safari or Firefox. See a viewer's browser cannot play H.264 |
bad-transport | A media request named a transport that is not yours | A client bug if you are writing your own; the SDKs never do this |
chat-muted | A moderator muted you in this chat | Tell the user. Do not retry. |
rate-limited | Too many chat messages too quickly | Slow down; the limit refills within seconds |
no-message | That message does not exist, or is not yours to see | Remove it from the list |
slot-occupied, slot-taken, slot-blocked | A seat is held by someone else, or locked | Offer another seat |
bad-params, bad-json | The request was malformed. The message says how. | Fix the request |
unknown-method | No such method on this connection. Media methods do not exist on a chat-only connection. | Use a room connection for media |
closed, disconnected | The connection closed before the reply arrived. The JavaScript SDK rejects the call with a plain Error instead of a code | The SDK reconnects; retry once it has |
internal | Something failed on our side | Retry once |
Video profile and screen-share errors (SDK 0.5.0)
| Code | Meaning | What to do |
|---|---|---|
stage-full | All three demo co-host places are occupied | The host can demote a co-host before accepting another. Becoming a co-host does not free an audience place. |
quality-unavailable | An older producer does not provide usable layer metadata | Choose Auto or update the publisher. Explicit presets map real layers; they cannot manufacture a resolution. |
frame-rate-fallback | The camera, device or upload cannot provide the requested 60 fps. Before 0.7.0 it also covered a project that does not allow 60 fps, which is now frame-rate-not-allowed | Continue at 30 fps and update the control. Browser emits an error warning; Flutter uses onPublishWarning (and onError from 0.7.0). It is not a reconnect signal. |
screen-share-unsupported | Browser/platform does not support capture, or the prepared iOS preview is not opted into / requires iOS 14+ | Offer supported capture; iOS preview requires separate setup and remains unverified. |
screen-share-active | A share is already starting or live | Stop it before starting another. |
screen-share-no-video | Browser capture supplied no live video track | Choose a screen/window/tab again. |
screen-share-service-required | Android foreground-service callbacks are missing | Follow the Android setup. |
screen-share-source-required | Flutter desktop capture lacks a selected source | Choose a screen/window and pass its id. |
screen-share-denied | Android capture permission was declined | Let the user retry; do not restart capture in the background. |
screen-share-size-unavailable | Flutter cannot read a captured screen frame | Check permission, selected source and its actual dimensions. |
screen-share-failed | Flutter capture/setup/restore failed | Keep the camera running, show feedback and retry screen capture if requested. |
screen-share-setup-required | Prepared 0.6.0 iOS preview lacks its native plugin, embedded extension or matching signed App Group | Follow the iOS extension guide and rebuild the app. |
screen-share-start-timeout | iOS broadcast did not start before the configured deadline | Let the person retry from the app and select Start Broadcast in the system sheet. |
screen-share-ended | iOS extension stopped while capture was starting | Show that the share ended; camera/mic can continue. |
Browser chooser cancellation can also reject with native NotAllowedError or AbortError; no screen was shared. Camera, microphone and music do not need to be stopped. Host/co-host permissions and server moderation apply to screen capture too; a viewer receives forbidden.
Media settings and wallet billing errors
A project's media settings say what it may publish. The browser and Flutter SDKs 0.7.0 read them when they join and never send what would be refused: they either refuse the call or send the most the project allows and warn, with the same code the server would use. The media server enforces the settings whatever a client sends; the codes it raises itself (marked server) apply to sessions billed from the VoltKit Wallet: every account, unless VoltKit keeps it on minute billing by agreement. Do not retry the same request: publish within the settings instead.
| Code | What happened | SDK behaviour |
|---|---|---|
media-disabled | The project switched that kind of media (audio, or video: camera and screen alike) off, or no enabled mode fits | Refused before any device opens |
frame-rate-not-allowed | The project does not allow 60 fps | Sent at 30 fps, with a warning; setVideoFrameRate(60) changes nothing and warns again |
video-quality-not-allowed | 60 fps is allowed, but not at the size asked for | The largest size allowed at 60 fps goes out, with a warning |
audio-quality-not-allowed | HD audio in a project limited to standard audio | setAudioQuality('hd') is refused; HD asked for in publish() goes out as standard, with a warning. In-app music (128 kbps stereo) is not affected: it is free and allowed in every project |
video-not-allowed, audio-not-allowed | server: video or audio is turned off for the project, or no mode it allows fits the stream | Not raised by a current SDK, which refuses first with media-disabled |
video-subscription-limit | This participant already receives the most videos allowed at once (25 by default) | The next video waits until one ends, with one warning each |
A 30 fps quality above the project's largest is not an error: the SDKs and the server cap it silently.
Codes the SDKs raise themselves
New in the browser and Flutter SDKs 0.7.0 and the Node.js SDK 0.4.0. Warnings arrive on error (browser) or onError and onPublishWarning (Flutter) and stop nothing; refusals reject the call.
| Code | SDK | What happened | What to do |
|---|---|---|---|
not-connected | browser | The call needs a joined connection and there is none right now | Retry once connectionState is connected |
timeout | browser, Flutter | The service did not answer within 15 seconds | Retry |
connect-failed | browser, Flutter | The connection could not be opened | Check the network; retry |
disconnected | Flutter | The connection is not open, or dropped while the call waited | Retry after the reconnect |
replaced | browser, Flutter | The same identity connected from somewhere else; this session is closed | Do not reconnect |
reconnect-gave-up | browser, Flutter | Every reconnect attempt failed; the session is closed | Start a new session |
devices-gone | browser, Flutter | A reconnect could not publish the broadcast again: its tracks had ended | Capture again and publish |
unpublished | browser, Flutter | unpublish() ran while that publish was under way | Nothing: unpublishing wins |
codec-fallback | browser, Flutter | A forced H.264 could not be used, so VP8 is sent | Nothing, or stop forcing H.264 |
produce-failed | Flutter | The media library did not deliver a track to the server | Retry; the platform log says why |
consume-failed | Flutter | One remote track could not be received; the rest of the room works | Retry the subscription |
unknown-producer | Flutter | setReceiveQuality named a video this client does not receive | Use the producerId from onTrack |
screen-audio-unavailable | Flutter | The platform granted no display audio; the screen is shared without sound | Nothing |
screen-stop-failed | Flutter | A capture could not be stopped cleanly; the share has ended anyway | Nothing |
degradation-preference-failed | Flutter | The encoder ignored the degradation preference | Nothing; a busy device lowers resolution first |
network | Node.js | The request never reached the service (DNS, refused, timeout) | Retried by the SDK where safe |
rates-unconfigured | Node.js | getRates() was called without ratesBaseUrl | Set ratesBaseUrl to the VoltKit website API |
redirected | Node.js | The service answered a request carrying your API secret with a redirect, which the SDK never follows | Set baseUrl to the exact scheme and host; see the server SDK |
When an account runs out mid-session
A live session is not left running when its project runs out. Clients first receive usageLimit with { reason, graceSeconds, endsAt }; about a minute later the session is ended: rooms with roomClosed (its reason is the same code), chat-only connections with a final usageLimit. Then the server closes the connection with WebSocket close code 4003.
| Close code | Meaning | Reconnect? |
|---|---|---|
4001 | The same identity joined from somewhere else | No |
4003 | Ended by the server, after it said why | No — it would be refused |
| any other | The connection dropped | Yes; the SDKs do it for you |
Rooms are warned while the minutes and credits a project may spend cover less than about a minute at the current rate: walletLow with remainingSeconds (and usageLimit, for older apps). A recharge in time sends walletLow with cleared: true, and nothing ends. If the credits reach zero, the rooms close at once with roomClosed (reason quota-exhausted) and close code 4003. See Running low and running out.
Why a session ended
When a client's session is over for good, the SDKs say why instead of leaving you to guess:
| Reason | Meaning |
|---|---|
left | You called close() |
replaced | The same identity joined the same room from somewhere else. Do not reconnect, or the two sessions will keep evicting each other. |
kicked | A moderator removed you |
room-closed | The host or your backend ended the room |
gave-up | The connection dropped and could not be restored after repeated attempts |
Broadcast publishing errors
Publishing a broadcast is one HTTP exchange with the media server. A failure is a WhipError:
status | Meaning | What to do |
|---|---|---|
401, 403 (isRefused) | The publish URL was not accepted: it expired, is for another stream, or the project is out of hours | Get a new URL from your backend. The same one will be refused again. |
0 | The media server could not be reached, or did not answer in time | Check the connection and try again |
Still stuck
Send the error code, the time, and your App ID (never your API secret) to contact@voltkit.in, or open a ticket from the Support page of your dashboard. For a quality or connection problem, Troubleshooting lists what to include so we can find your session.
Last updated . Something unclear or wrong? Tell us.