SDK reference — every call, the configuration, the errors, the limits
The lookup page: every public call of the page SDK and the server SDK, each linked to the page that explains it, then the configuration, the error codes and the limits. What changed between versions: Changes.
Part of the SDK · every call: the reference.
The surface
Three calls get you started; everything else hangs off the Space they return. Types for the whole surface:
witbitz-sdk.d.ts, and for your server
witbitz-admin.d.ts (save them beside your code, or use them for editor
hints). A test holds this page to the SDK's code both ways: every name here exists, and every public name the types declare is here.
new Witbitz({ endpoint?, viewer?, app?, tenant?, session?, userToken?, … }) |
A client bound to the Space endpoint. Make one, reuse it. Defaults to the public production endpoint. Creating rooms from your own page needs tenant — see Do I need a tenant?; every option is under Configuration. |
await wb.createSpace({ agent, tools?, immediateTools?, model?, data?, admission?, tier?, key?, platformTools?, region?, framing?, dataWrites?, direct?, temporary?, safeMedia? }) |
Mint a Space and return a live Space. agent = { name, instructions, tools?, model? } (the name up to 80 characters, the instructions up to 4,000). data declares the store; admission gates it on email; tier: 'enclave' runs it in the confidential tier; key: 'split' makes a split-key room. tools, immediateTools, tier and key are fixed when the room is made; admission changes only through changeMembers(). platformTools, framing ('builder': your instructions and tools only) and dataWrites start the room's in-place settings; region picks where the room lives; safeMedia: true turns on Safe media. Any other key is refused. |
await wb.openSpace(link, { resume?, tier? }) |
Join a Space from a share link — link is required; to open the page's own URL, pass location.href (see Invite links to tell whether it is one). The link's fragment carries the key; it never touches the server. |
A Space:
space.shareLink |
The link to hand someone — the link is the admission capability. |
await wb.signIn({ email }) → { ticket, email } then await wb.signIn({ email, code }) → session · wb.signOut() |
Sign in with an emailed code (any address, any origin). wb.session is JSON you may persist and pass back as new Witbitz({ session }). See Sign in. |
wb.connections.attach({ label, host, secret }) · .list() · .detach(id, { spaces? }) |
The account's connections — a bearer token the standing Sentinel holds and spends; attach returns { id, label, host, callKey, account } (account: the signed-in address it belongs to) and the secret is gone from the page. A whsec_ secret with the webhook's whole https:// address as host makes a signed webhook: the Sentinel signs each approved request (Standard Webhooks) and only that one path can be called. |
wb.connections.startConnect({ tenant, providerId }) · .finishConnect({ state, code, label?, host? }) |
Connect to a builder's OAuth app: startConnect → { authUrl, state } (send the person there), then pass the callback's state and code to finishConnect → a connection like attach's. The tokens stay in the enclave. |
space.connections.grant(conn) · .revoke(id) |
What THIS Space's assistant may spend: it gets connection_call for a granted API-key or OAuth connection, always proposed; an MCP connection's tools are proposed too, except those in auto (connection calls). |
space.siri.enable({ alias, makeDefault? }) → { key, endpoint, alias } · .collect({ deliver? }) · .disable() |
The Siri inbox for this Space. deliver(text, meta) takes each drop instead of posting it, for an app with its own inbox. wb.siri.state (JSON — it holds the inbox private keys) is yours to persist and hand back with wb.siri.restore(state); wb.siri.shortcut is { key, endpoint } for the Siri Shortcut; wb.siri.rotateKey() / .revoke() are account-wide. |
space.room |
The server-visible room id. |
space.safeMedia.copy(message, { at?, i? }) · .make(…) · .set(on) · .refresh() · .isOriginal(copy, b64) · .on · space.on('safeMedia' \| 'safeCopy', cb) |
Safe media: others' photos and PDFs shown only from copies an attested enclave made safe, verified on the device. createSpace({ safeMedia: true }) / configure({ safeMedia }) turn it on; send() makes the copies of what it sends — see Safe media. |
space.temporary |
true for a temporary room (createSpace({ temporary })): wiped when idle, never in the account backup. |
await space.run(input, { instructions?, schema?, images? }) → { ok, text, output?, usage?, error? } |
The assistant as a function: one answer, optionally structured, nothing written to the room — see The assistant as a function. |
await space.admission() · await space.admits(email) |
Read this room's real gate from its sealed config — see Reading the gate. |
await space.isAdmin(email) · await space.isOwner(email) |
Is this address one of the room's admins, or one of its owners — see Admins and Owners. |
await wb.createSpace({ direct: email }) · await space.directPeer() |
A direct chat with one person, as in the Spaces app — see Direct chats. |
await wb.createSpace({ key: 'split' }) |
A split-key room: its link is half a key; your own function holds the other half and serves it only to your origin — see Split-key rooms. |
await space.changeMembers(admission) · await space.rotateKey() · space.on('rekeyed' \| 'removed' \| 'pending', cb) · wb.invite |
Change members in place (3.0.0): the same room and history, a new key only the members on the list receive — see Changing members in place. |
space.on('moved' \| 'moveRefused', cb) · space.fork() |
A move an older client made is followed (a stamped notice sealed to this device); fork() is retired and throws use_change_members — members change in place. See A Space no longer moves. |
await space.attachment(att) → { b64, mime, name? } |
The bytes of message.images[i] / message.files[i]. Attachments are stored out of line, sealed under the room key; this fetches and opens them on the device. |
lane.a2a.enable({ name, description? }) · .addPeer(name, { reply?, job? }) · .setReply(id, mode) · .removePeer(id) · .collect() · .disable() · .cardUrl · .mcpUrl · .peers · on('a2a', …) |
Let outside agents reach this lane over A2A, with the member approving each answer — see Outside agents, over A2A. |
aguiAgent({ AbstractAgent, Observable }, space) · aguiRun(space, input) · aguiHistory(space) |
The Space as an AG-UI 1.0 agent in your page — behind CopilotKit or any AG-UI front end; a proposal is an interrupt — see AG-UI |
lane.link(otherLane, { to?, as?, cap?, resume? }) → a link: .a · .b (its two lanes) · .state · .unlink() |
Two of the member's lanes ask each other: each assistant gets tell_lane; persist state and pass it back as resume after a reload, unlink() ends it. See Assistants that ask each other. |
lane.setLinks([space, { space, label }]) · lane.send(text, { links }) |
Let a lane's assistant read other rooms the member holds — one lane for several teams. See Private lanes. |
space.on('message', cb, { replay? }) · space.messages |
cb(message) for every entry in the room — your messages, other members', the agent's replies, store writes and system lines; messages is the array so far. On an opened Space, the existing history replays here first; pass { replay: false } to hear only what arrives after it (notifications, analytics, side effects). Every on() returns a function that removes the listener (and still chains: .on(…).on(…)). self: true marks the agent's output, never yours — the full shape and how to tell senders apart: Messages. |
await space.send(text, { from?, mode?, instructions?, images?, files?, stopKey? }) |
Post a message. stopKey is the turn's cancel key (8–64 letters and digits): space.turn() sets one so t.abort() can stop the reply — use that rather than passing your own. instructions (up to 8,000 characters) reaches the assistant for this reply only — today's date, "answer in Hebrew" — and is never stored. images (up to 4) and files (up to 2) are { b64 \| bytes, mime, name? }; the room stores them sealed, and anything over the limits is refused at the call. The agent decides whether to answer and does so on its own turn; the reply arrives via on('message'), not as a return value. mode overrides the reply mode for this turn ('auto' / 'always' / 'ondemand'). |
const t = space.turn(text, { timeoutMs?, …send's options }) · t.on('partial', cb) · t.text · t.result · await t.finished · t.abort() |
A turn you can hold. The same message as send(), plus a handle on its answer: finished resolves { status, reply, text, proposed, usage?, error? } with status one of 'done', 'silent' (the assistant chose not to answer), 'failed', 'aborted', 'timeout' — it never rejects. partial streams this reply; abort() stops it (what streamed so far is kept as the reply). timeoutMs defaults to 180 s and stops the turn when it passes. See A turn you can hold. |
await space.send(text, { replyTo: message }) · await space.react(message, '👍') |
A reply quotes the message it answers, as in the Spaces app. A reaction is written on the message itself — no new entry, no assistant turn — and reaches every device as an 'update'; { off: true } takes yours off. In a verified room a reaction belongs to the signed-in key that made it. |
await space.setReplyMode(mode) · await space.replyMode() |
The room's standing reply mode ('auto' / 'always' / 'ondemand', default 'auto') — mode on a send overrides it for one turn. It is the same setting as the Spaces app's reply chip, re-read before a turn once it is 20 s old. Every turn (and run) also carries the room's model pick from the app's model chip, as the app's own turns do. |
await space.approve(id, { from? }) · await space.deny(id, { from? }) · await space.pending() |
Decide a pending card by id, the same as a proposal's own approve({ from? }) / deny({ from? }); pending() polls now and returns the waiting cards. approve(id, { recordResult: false }) is for a connection call whose answer you hand the assistant yourself: the room records the call (who approved, what, how it went) but not the answer. A refused decision throws (not_admin, not_authorized, …) and keeps the card. The full round trip: Proposals, end to end. |
space.snapshot() / resume |
snapshot() returns a JSON-safe { count, etag, messages, rows }; hand it back as resume to openSpace(link, { resume }) (or the Space / Lane constructor) and the next open continues from there instead of replaying the whole ledger. |
space.toJSON() · wb.toJSON() |
What logs, JSON.stringify and error reporters see: { room, shareLink: '[REDACTED]', messages, … } and { viewer, tenant?, signedIn, tenantKey?: '[REDACTED]' } — never a key or a link. Read space.shareLink itself when you mean to share it. |
await space.sealedProof() |
What the server actually stores for this room — the opaque, sealed ledger, plus its size, hash, and who can decrypt it (the operator never can). Lets your app prove the operator can't read the store, not just claim it. null before the first message. |
await space.privateLane({ agent, actions? }) |
Attach a private assistant lane for the current member — see Private lanes. → a Lane. |
await wb.openLane(laneLink, { space, actions?, resume?, mirror? }) |
Reopen a member's existing lane on a later page load, from the link privateLane returned (lane.shareLink). space is the shared Space it belongs to (its crossings go there). A lane of a confidential Space opens as confidential. → a Lane. |
space.on('partial', cb) |
The agent's reply as it is written: cb({ text, step }) — text is the reply so far ('' before the first words, with step saying what the agent is doing: { phase: 'thinking' \| 'writing' \| 'deciding' } or { tool }; 'deciding': an Auto-mode assistant is deciding whether to reply at all — it may end with no reply), and text: null once no reply is in progress. The finished reply still arrives as a message. See Live updates. |
space.on('error', cb) |
Poll errors (non-fatal; the loop keeps going). After a refusal no retry cures (not_authorized with reason not_on_list, or gate_unreadable) it reads once a minute — see Errors. |
await wb.usernames.claim(name, { publish?, name? }) · .release() · .mine() · .resolve(name) |
The signed-in member's @username. One name means one person across all of Witbitz, not per app: verified messages and rows carry it (_user), and the Spaces app can admit by it (the SDK's admission takes emails and domains). claim refuses a taken name (taken) or one released in the last 30 days (held); publish: true also lists the person in the public directory (off by default), under the display name when you pass one. resolve needs no sign-in and answers { username, exists }. A released name doesn't change what the member already wrote. |
await space.certificates() → { verified, failed, unchecked, before, builds, calls, tools } |
Turn certificates in a confidential room: every assistant reply is signed inside the enclave with the build's measurement, each model call's verified receipt and each tool call. This verifies the loaded history on the device: the attestation, the published audit verdict, each signature, each reply's text. failed lists { id, reason }. unchecked counts replies whose attestation rides an earlier reply this instance has not loaded — not a failure. A regular room has none. |
const mic = wb.dictation() · await mic.start({ onPartial?, onLevel? }) · await mic.stop() → text · mic.cancel() · mic.listening |
Press-to-talk: the microphone streams straight from the page to a confidential speech model; Witbitz never hears it. Needs a signed-in user or a user token — see Voice input. |
await wb.speak(text) · wb.speech.play(text) · wb.speech.stop() · wb.speech.playing |
Read text aloud with the Spaces app's Play aloud player; the text goes straight to a confidential speech model. Needs a signed-in user or a user token — see Reading aloud. |
wb.account.rooms · .devices · .links · .archive(room) · .rename(room, label) · .forget(room) · .signOut(id) · await .sync() · .enabled · .deviceId · .onChange · .onSignedOut · .lastError |
The account's live index: the rooms, archived and forgotten marks, and devices, kept in step on every device that holds the backup — see One account on every device. |
wb.protect.available() · await .enable({ label?, withoutBackup? }) · await .unlock() · .lock() · await .disable() · await .save(name, value) · await .load(name) · .remove(name) · await .seal(v) · await .open(x) · .autoLock(minutes) · .on(event, cb) · .state |
Device storage protection: what the app keeps on the device, sealed under a key a platform passkey derives — see Protect what the app keeps on the device. |
await wb.models({ tier? }) |
The models this deployment offers on a tier, each with the reasoning efforts it takes — see The model and its reasoning effort. |
await space.configure({ agent: { name?, instructions?, model? }, data?, platformTools?, framing?, dataWrites?, safeMedia?, from? }) |
Change the assistant in place, for every member — see Changing the assistant. data declares or changes the store's collections; platformTools, framing, dataWrites and safeMedia as at createSpace; from is the name the change line shows. |
await space.plan(change) → { how, history, call, steps, reason?, code? } |
What a change would require, before you make it — nothing is changed. change takes configure's, changeMembers' (admission) and createSpace's fields. how: 'in_place' (the assistant's name, instructions, model; the store; platformTools, framing, dataWrites — admins only where the room has them; a new member list in an email-gated room too, call: 'space.changeMembers(admission)'), 'new_room' (the tier, a split key, the tools — the history does not come along), or 'refused' with the reason and the error code that changeMembers() or configure() would give (use_change_members for a history: a Space no longer moves). |
await wb.capabilities() |
Feature detection: { sdk, endpoint, features, limits, tiers: { standard, enclave }, models } — this SDK's version ('dev' on the live build) and what the deployment serves. Mostly for on-prem. |
space.data · space.on('data', cb) |
The Space as a store: rows(collection), get(collection, id), put, delete, batch(ops) — see The room as a store. |
await space.ready |
Resolves once the room's whole history has been read (a long room loads in pages; every page): by then messages and data.rows() hold what the room had, so it is the moment to swap a cached copy for the live room. A refused read (the confidential tier waking up, a gated room before sign-in) does not count; those retry and raise error. Rejects only when the key can never open the room (cannot_open) or the handle was closed first (closed). Every Space and Lane has it. |
space.close() |
Stop streaming and release the handle. |
space.title · space.on('title', cb) |
The room's name, the same on every device and in the Spaces app; null while it has none — see The room's name. |
space.on('proposal' \| 'proposals' \| 'resolved', cb) |
A card waiting for approval, the whole waiting list, and a card decided — see Proposals, end to end. |
lane.setActions(actions) · await lane.mirrorNow() |
The lane's standing actions, offered on every turn until changed; copy the shared room's new messages into a lane opened with mirror now, without waiting — see Private lanes. |
wb.a2a.state · .restore(state) · .onState · .endpoint · await .status() · await .revoke() |
The account's door for outside agents: state holds its private keys — persist it and hand it back like the session — see Outside agents, over A2A. |
wb.connections.startMcp({ url, label?, redirectUri? }) · .attachMcp({ url, label?, token? }) · .finishMcp({ state, code, label?, host? }) · .mcpTools(id) |
Connect an MCP server: startMcp says whether it needs a sign-in (attachMcp when not; else send the person to authUrl and finish with finishMcp); mcpTools lists what the connection offers — see MCP servers. |
wb.computers.list() · .create({ name?, keys?, egress?, browsePrivate? }) · .open(id, { onStep?, images?, onNewImage? }) · .remove(id) · .usage() |
The person's cloud computers, kept sealed in this app's account: create takes model keys from the account unless keys brings the person's own; open checks the computer runs the published Witbitz image (a new published image: images: 'auto', or 'ask' → onNewImage(pcr0, role)) and returns a Computer; its hours charge the person's AWS key (usage). Not in vault mode yet (vault_mode) — see Cloud computers. |
computer.sessions.list() · .create({ dir?, title? }) · .messages(id, { limit? }) · .prompt(id, text, { model?, agent? }) · .abort(id) · .rename(id, title) · .remove(id) · .todo(id) · .status() |
A cloud computer's coding sessions. computer.id, computer.name, computer.status. |
computer.approvals.pending() · .answer(id, 'once' \| 'always' \| 'reject', { session?, message? }) · computer.questions.pending() · .answer(id, answers) · .dismiss(id) |
The agent's approvals and questions; a reject with a message tells the model to stop and ask. |
computer.files.list(path?) · .changes() · .diff() · computer.browser.status() · computer.egress.get() · computer.on(type, fn) · computer.close() |
Its files and changes, the agent's browser and what the computer may reach, its live events ('*' for all), and close — a last save, then its hours stop. |
wb.useSession(session) · await wb.refreshSession() · wb.useUserToken(token \| fn) |
Put a saved sign-in back, renew it before it expires (refresh_failed → sign in again), or let your server's customer token pay for this page's turns — see Sign in. |
wb.backup.enable({ email? }) → { code } · .restore({ code, email?, … }) · .save() · .add(space) · .remove(room) · .keyFor(room) · .rooms · .state · .onState · .enabled · .format · .upgrade() · .invite · .setInvite(state) |
The account's encrypted backup and its recovery code; state holds the master secret — see Account backup and the recovery code. |
wb.invite.state · .status · .restore(state) · await .enroll() |
The account's invite key, so others can add this person to a room by email — see Changing members in place. |
wb.erasure.available · await .file({ force? }) · await .status() |
File this account's room list for your tenant, so admin.customers.erase can delete the person's rooms later — see Erasing a user. |
await Witbitz.inVault({ vault, … }) · wb.vault → .rooms() · .openRoom(room) · .share(space) · .signIn() · .signOut() · .whoami() · .forget(space) · .showSiriKey() · .on(event, cb) · .origin · .info |
Vault mode: your page runs framed by a vault you host on another origin, which keeps every key; sign-in and sharing happen there — see Vault mode. |
wb.verifyTenantKey() · wb.origins · wb.hosts · wb.providers · wb.rooms · wb.users · wb.billing |
Builder calls on the page client — deprecated: use WitbitzAdmin on your server. |
What the module exports, beside Witbitz (also the default export), Space, Lane, VaultSession and Protect (the class of wb.protect):
SDK_VERSION |
This SDK's version — 'dev' on the live build, the release number under /sdk/<version>/. |
LIMITS · EFFORTS |
The runtime's declaration limits, and the reasoning efforts a model may take. |
checkActions(actions) · checkDataSchema(data) · checkModelEffort(model, catalog?) |
Check before you send: they throw limit_exceeded (or invalid_effort) for what the server would cut, drop or refuse. catalog is wb.models(). |
isLaneCopy(message) |
Is this a lane's copy of its shared room's line — the ones the Spaces app draws as "from the room". |
aguiAgent · aguiRun · aguiHistory |
The Space as an AG-UI agent (above). |
verifyEat(token, opts) · fetchAndVerifyEat(url, opts) · EAT_PROFILE |
Check an enclave's standard attestation token — see EAT. |
WitbitzError (a type) |
What every SDK error is: an Error with a code from Error codes. |
The server SDK — WitbitzAdmin
For your server, never a page: it takes your tenant's secret key (or a restricted key).
new WitbitzAdmin({ tenant, tenantKey, signingKey?, endpoint? }), from
witbitz-admin.mjs; types in
witbitz-admin.d.ts. How each part works:
Your server.
await admin.verifyTenantKey() → { ok, tenant } |
Is this key good, and whose is it? |
WitbitzAdmin.generateSigningKey() · admin.signing.register(publicKey?) · .status() · .publishOrigins(origins?) |
Sign every request to the Sentinel with your own Ed25519 key — see Signing your tenant's requests. |
admin.keys.create({ type, label?, scopes?, expiresAt?, maxCustomerCapUsd? }) · .list() · .revoke(keyId) |
Your API keys, and restricted keys with only the capabilities you choose — see Restricted keys. |
admin.app.name() · .rename(name) |
Your app's name in the sign-in mail — see Your app's name. |
admin.origins.declare(origins) · .list() |
The page origins that act for your tenant; declaring replaces the list — see Your page origins. |
admin.hosts.declare(hosts) · .list() |
The hosts your users may attach a key for — see Hosts and OAuth apps. |
admin.providers.attach({ clientId, clientSecret, redirectUri, scopes, … }) · .attachMcp({ issuer, clientId, … }) · .list() · .revoke(id) |
Your own OAuth apps, the secret sealed in the Sentinel — see Hosts and OAuth apps. |
admin.rooms.delete(room, { retain?, graceDays?, after? }) · .restore(room) · .list({ cursor?, limit? }) · .idleDeletion(days) |
Delete, restore and list your rooms, and the idle rule — see Deleting a room and Your rooms. |
admin.customers.create({ userRef, capMonthlyUsd? }) · .list() · .update(userId, { capMonthlyUsd }) · .revoke(userId) · .token(userId, { ttlSec? }) · .erase({ email }) |
Your app's billed users, each with a monthly cap, the token their page uses, and erasing a person who has left — see Billing, per user and Erasing a user. |
admin.billing.get() · .csv({ byUser? }) · .requireUserToken(on?) · .visitorLimit(limit) |
What you are billed, whether every turn must carry a customer token, and how many questions a day people who are not signed in may ask (visitor limits). |
admin.toJSON() |
What logs see: never the key. |
Configuration
endpoint— the Space API. Defaults to the public production endpoint (https://api.witbitz.chat/v1/space), a self-authenticating endpoint: a Space proves itself by its key-commitment, not a tenant account.viewer— the origin ashareLinkopens in. Defaults to the reference Spaces app, so a link Just Works. Point it at your own origin to open Spaces in a viewer you build (the SDK also exposesspace.keyso you can construct links yourself).app— an optional tag recorded with the Space.tenant— your tenant's public id (t_…, never a key). Spaces this page creates are attributed to your tenant — its inventory, its take-down, its storage — when the page's origin is one your tenant declared (PUT /v1/tenant/webhookswithorigins). A page on an undeclared origin gains nothing by naming a tenant; a declared origin needs no credential on the page.live—falseturns off live updates (polling only). On by default in a browser.session,userToken— a persisted sign-in (see Sign in) and a tenant-vouched user's token (see Billing, per user);userTokenis a string or a function that returns one.tenantKey,tenantEndpoint— the builder side, server only; prefernew WitbitzAdmin(…)(see Your server).backup,deviceName,vaultApi— the account backup and index;vaultApidefaults tohttps://api.witbitz.chat/v1/vault.siri,a2a,a2aEndpoint— persisted Siri and A2A state; the A2A door defaults tohttps://agents.witbitz.chat.appShare— your split-key function's URL (see Split-key rooms).region— the home region new rooms are made in (a code like'us'); default: the default region. A room is used from wherever its link is opened; a private lane always takes its shared room's region.enclaveClaim— the URL of the enclave's published measurement (/enclave-claim.json), only when yourvieweris an origin of your own that does not serve it; https only.loginBase,sentinel— a tenant's own sign-in origin and standing Sentinel pins (see the end of Sign in).fetchImpl,WebSocketImpl— for tests and non-browser runtimes.- On-prem / your own model, keys, and IdP: point
endpointat your deployment. See Deployment options and On-prem.
Error codes
Every error the SDK throws, or emits on 'error', carries a code. A refused server call passes the server's own
code through (e.g. not_admin, not_authorized, tenant_required).
code |
Means |
|---|---|
limit_exceeded |
Over a declaration limit (media, actions, schema); refused before anything is sent |
invalid_effort |
A reasoning_effort the model does not take (wb.models()) |
sign_in_required · no_session · refresh_failed |
Needs wb.signIn; refreshSession had nothing to renew, or could not |
tenant_required |
Creating a room with no tenant behind it — see Do I need a tenant? |
tenant_key_rejected · bad_response · http_<status> |
A builder call (server side) was refused or answered badly |
origin_not_declared · split_unopenable · split_mismatch |
A split-key room's app half was refused, or the halves do not match |
not_in_vault · vault_refused · vault_timeout · vault_unsupported · vault_opens_links · vault_signs_in · keyless · cancelled · rate_limited |
Vault mode: the page is not framed by that vault; the vault refused the call or did not answer; not available in a vault yet; a link is opened by the vault; sign in with wb.vault.signIn(); a call that needs the key in the page; the person said no in the vault; too many new Spaces |
vault_mode |
Cloud computers are not in a vault yet |
no_screen |
Vault mode: the vault has no sign-in or share screen (your screens lack it). The backup and Siri-key screens fall back to the kit's own |
cannot_open · closed |
space.ready rejected: this key never opens the room, or you closed the handle first |
reserved_field · row_changed |
A row set id or a _ field; an ifVersion write lost the race (e.conflicts) |
not_admin · not_owner |
A decision or fork the room reserves for admins / owners |
no_invite_key · changed_meanwhile · stale_key |
A member change in place: this account has no invite key for your app on this device (restore wb.invite.state; in a vault, the vault's own); someone else changed the members at the same moment (read the room and try again); a 2.2.x page met a re-keyed room (move to 3.0.0) |
direct_chat |
rename() in a direct chat — it is called by the other person's name |
gate_required |
A connection granted to a room that is not email-gated |
admins_required |
A collection written by admins only, in a room with no admins or owners (create, configure, fork) |
config_too_large |
The room's settings (instructions, tools, data schema…) seal to more than 200 KB — on create, configure or fork; nothing is saved |
tier_mismatch |
A lane's linked room (setLinks) is on the other tier, or a tier asked for is not the room's |
config_unavailable · attachment_unavailable · enclave_claim_unavailable · enclave_unavailable · unavailable |
Something the call needed could not be read (enclave_unavailable: the confidential tier's enclave could not be reached or verified — being replaced or relaunched); retry |
enclave_seal_failed · partial |
A confidential fork could not seal the new room; a room was only partly deleted |
use_change_members |
space.fork(), or plan() given a history: a Space no longer moves — change its members in place (space.changeMembers()) |
not_a_lane · self_link · already_linked |
lane.link() was given something that is not a Lane, the lane itself, or a lane it is already linked to |
removed |
An option that no longer exists: admission.removeUsers, admission.approve: 'admins' |
invalid_argument · invalid_link |
A call was given something it cannot use (an attachment without mime, an unknown tier, key, framing or reply mode, an empty Siri alias…); openSpace / openLane was given a link that is not a Space link or lacks its key |
invalid_model · bad_platform_tools · bad_request |
model not offered on this tier, a platformTools that is not true, false or a list of platform tools, or a request the server refused as malformed (e.g. a rename() name not 1–80 characters) |
governed_room · config_immutable |
configure() in a room whose owner signed its policy (it changes only by a new room), or a change the server takes only by a move or a new room |
enclave_not_accepted |
A confidential room's enclave runs a build Witbitz has not published; the room key is not sealed to it |
no_answer |
the Sentinel did not answer (a connection's sign-in, a Siri record) — try again |
failed · refused |
a refusal, or no answer at all, that came without a server code of its own — from any call that refuses through the SDK's shared path (row writes, setReplyMode, fork, configure, approve / deny, connections.grant / revoke, the mirror, the Siri record); status and reason, when present, say more |
unknown_field · create_time · use_fork |
configure() refused a field before sending: not a setting it changes; one chosen when a room is made (tools, immediateTools, tier, key, agent.tools); or admission, which changeMembers() changes (the code keeps its old name) |
no_client |
a confidential Space opened without its Witbitz client cannot make a private lane — open it through wb.openSpace |
history_incomplete |
A fork's history could not be read in full (this device carried it), so no notice was posted and no member moved; the new room it made is empty — fork again |
keyless |
The feature is not available when the room key is held by a worker |
mic_denied · already_listening · stt_unavailable · voice_unattested |
Voice input |
no_hebrew · empty · tts_rate_limited · tts_unavailable · audio · play |
Reading aloud |
locked · wrong_passkey · backup_required · prf_unsupported · already_enabled · not_enabled |
Device storage protection |
'error' events from the poll loop are transient (the loop keeps going): no_key, bad_mkseal (a confidential room's enclave was replaced: the SDK attests the new one and the call — any keyed call — is sent once more), enclave_warming,
not_found, cannot_open (this key opens nothing in the room's history — the wrong link or key), unavailable (no answer just now), http_<status>. Some refusals a retry cannot cure: not_authorized with reason not_on_list (the
signed-in email is not on the room's list), and gate_unreadable (the room's
settings cannot be read). After one of those the loop slows to one read a minute, so a re-admission is still noticed;
a send() or a stream signal reads at once, as always. send() does not throw: its result always carries a boolean ok. It is true when
the message was posted (also when the assistant stays silent, and { ok: true, pending: true } when the request was cut
after the server accepted it — the reply still arrives on 'message'; do not resend). It is false for every refusal,
with status, error and message — e.g. { ok: false, status: 401, error: 'user_token_required' } — and for a busy
room, { ok: false, error: 'busy' }. When the request never got an answer (the network failed, the enclave could not be
reached) it is { ok: false, status: 0, error, detail } — error is unavailable, enclave_unavailable or the
failure's code, detail says what happened. A status-0 send MAY have been posted (the request can be cut after the
server took it): read the room (or wait for 'message') before you send it again. space.turn() settles such a send as failed with that error — the same caution applies. Check if (!r.ok). A refused send's error and reason are the ones in the table above:
most often not_authorized with reason not_on_list or grant_required (an invite room this device has no
invitation to), gate_unreadable, policy_invalid, user_token_required, visitor_limit (429, with scope and reason: your visitor limit), visitor_count_unavailable (503: retry shortly), reply_not_stored (retry: the reply came
back but could not be kept) and bad_mkseal / enclave_warming (a confidential room — see above). Row writes (data.put/delete/batch) answer
{ ok: true, id, ops } and throw on a refusal.
Limits
What an app declares — its actions and its data collections — is bounded by the runtime. Past a limit the runtime
does not fail; it cuts or drops the excess, which is why the SDK refuses such a declaration up front with an error
whose code is limit_exceeded and whose message names the limit. The numbers are also exported as LIMITS.
The same checks are exported, to run on your own (a build step, a test): checkActions(actions) and
checkDataSchema(data) throw limit_exceeded; checkModelEffort(model, await wb.models()) throws invalid_effort for an
effort the model does not take; EFFORTS lists every effort name.
| What | Limit | Past it, the runtime would… |
|---|---|---|
| actions per turn | 32 | drop the rest |
an action's name |
letters, digits, _; up to 64 |
drop the action |
an action's description |
2,000 characters | cut it |
an action's input schema |
8,000 characters of JSON | drop the whole action |
| collections | 16 | drop the rest |
| fields per collection | 32 | drop the rest |
| a collection or field name | letters, digits, _ . -; up to 40 |
rename it |
a collection's description |
300 characters | cut it |
| a field's description | 200 characters | cut it |
images on a send |
4, each under 1.1 MB | refused at the call |
files on a send |
2, each under 3.7 MB | refused at the call |
| linked rooms on a lane | 16 | refused at the call |
Put what the assistant must not miss at the start of a description. A list at the end of a long description is the first thing a limit takes — and a model that sees two of four calendars does not know there were four.
Machine-readable source: sdk-reference.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)