# 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](./sdk-changes.md). Part of [the SDK](./sdk.md) · every call: [the reference](#the-surface). --- ## The surface Three calls get you started; everything else hangs off the `Space` they return. Types for the whole surface: [`witbitz-sdk.d.ts`](https://witbitz-spaces.pages.dev/witbitz-sdk.d.ts), and for your server [`witbitz-admin.d.ts`](https://witbitz-spaces.pages.dev/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?](./sdk.md#tenant); every option is under [Configuration](#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](./sdk-store.md#the-room-as-a-store); `admission` gates it on email; `tier: 'enclave'` runs it in the confidential tier; `key: 'split'` makes a [split-key room](./sdk-keys.md#split-key). `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](./sdk-assistant.md#changing-the-assistant); `region` picks where the room lives; `safeMedia: true` turns on [Safe media](./sdk-rooms.md#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](./sdk.md#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](./sdk-people.md#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](./sdk-approvals.md#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](./sdk-rooms.md#safe-media). | | `space.temporary` | `true` for a [temporary room](./sdk-rooms.md#temporary) (`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](./sdk-assistant.md#run). | | `await space.admission()` · `await space.admits(email)` | Read this room's **real gate** from its sealed config — see [Reading the gate](./sdk-people.md#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](./sdk-people.md#admins) and [Owners](./sdk-people.md#owners). | | `await wb.createSpace({ direct: email })` · `await space.directPeer()` | A **direct chat** with one person, as in the Spaces app — see [Direct chats](./sdk-people.md#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](./sdk-keys.md#split-key). | | `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](./sdk-people.md#members). | | `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](./sdk-people.md#members). See [A Space no longer moves](./sdk-people.md#fork). | | `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](./sdk-agents.md#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](./sdk-agents.md#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](./sdk-agents.md#lane-links). | | `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](./sdk-agents.md#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](./sdk-rooms.md#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()`](./sdk-assistant.md#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](./sdk-assistant.md#turn). | | `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](./sdk-approvals.md#proposal-round-trip). | | `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](./sdk-agents.md#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](./sdk-rooms.md#live). | | `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](#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](./sdk-assistant.md#dictation). | | `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](./sdk-assistant.md#speak). | | `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](./sdk-accounts.md#account-index). | | `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](./sdk-accounts.md#protect). | | `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](./sdk-assistant.md#model-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](./sdk-assistant.md#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](./sdk-store.md#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](./sdk-rooms.md#room-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](./sdk-approvals.md#proposal-round-trip). | | `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](./sdk-agents.md#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](./sdk-agents.md#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](./sdk-accounts.md#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](./sdk-computers.md). | | `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](./sdk-builder.md#user-billing) pay for this page's turns — see [Sign in](./sdk-people.md#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](./sdk-accounts.md#backup). | | `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](./sdk-people.md#members). | | `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](./sdk-builder.md#user-erasure). | | `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](./sdk-keys.md#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`](#the-server-sdk-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//`. | | `LIMITS` · `EFFORTS` | The runtime's [declaration limits](#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](./sdk-keys.md#eat). | | `WitbitzError` (a type) | What every SDK error is: an `Error` with a `code` from [Error codes](#errors). | ## The server SDK — WitbitzAdmin For your server, never a page: it takes your tenant's secret key (or a [restricted key](./sdk-builder.md#restricted-keys)). `new WitbitzAdmin({ tenant, tenantKey, signingKey?, endpoint? })`, from [`witbitz-admin.mjs`](https://witbitz-spaces.pages.dev/sdk/3.10.0/witbitz-admin.mjs); types in [`witbitz-admin.d.ts`](https://witbitz-spaces.pages.dev/witbitz-admin.d.ts). How each part works: [Your server](./sdk-builder.md). | | | |---|---| | `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](./sdk-builder.md#tenant-signing). | | `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](./sdk-builder.md#restricted-keys). | | `admin.app.name()` · `.rename(name)` | Your app's name in the sign-in mail — see [Your app's name](./sdk-builder.md#app-name). | | `admin.origins.declare(origins)` · `.list()` | The page origins that act for your tenant; declaring replaces the list — see [Your page origins](./sdk-builder.md#origins). | | `admin.hosts.declare(hosts)` · `.list()` | The hosts your users may attach a key for — see [Hosts and OAuth apps](./sdk-builder.md#integrations). | | `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](./sdk-builder.md#integrations). | | `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](./sdk-builder.md#delete) and [Your rooms](./sdk-builder.md#room-list). | | `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](./sdk-builder.md#user-billing) and [Erasing a user](./sdk-builder.md#user-erasure). | | `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](./sdk-builder.md#visitor-limits)). | | `admin.toJSON()` | What logs see: never the key. | ## Configuration {#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 a `shareLink` opens 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 exposes `space.key` so 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/webhooks` with `origins`). A page on an undeclared origin gains nothing by naming a tenant; a declared origin needs no credential on the page. - **`live`** — `false` turns off [live updates](./sdk-rooms.md#live) (polling only). On by default in a browser. - **`session`**, **`userToken`** — a persisted sign-in (see [Sign in](./sdk-people.md#sign-in)) and a tenant-vouched user's token (see [Billing, per user](./sdk-builder.md#user-billing)); `userToken` is a string or a function that returns one. - **`tenantKey`**, **`tenantEndpoint`** — the builder side, server only; prefer `new WitbitzAdmin(…)` (see [Your server](./sdk-builder.md#builder-calls)). - **`backup`**, **`deviceName`**, **`vaultApi`** — the [account backup](./sdk-accounts.md#backup) and [index](./sdk-accounts.md#account-index); `vaultApi` defaults to `https://api.witbitz.chat/v1/vault`. - **`siri`**, **`a2a`**, **`a2aEndpoint`** — persisted [Siri](./sdk-accounts.md#siri) and [A2A](./sdk-agents.md#a2a) state; the A2A door defaults to `https://agents.witbitz.chat`. - **`appShare`** — your split-key function's URL (see [Split-key rooms](./sdk-keys.md#split-key)). - **`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 your `viewer` is 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](./sdk-people.md#sign-in)). - **`fetchImpl`**, **`WebSocketImpl`** — for tests and non-browser runtimes. - **On-prem / your own model, keys, and IdP:** point `endpoint` at your deployment. See [Deployment options](./deployment-options.md) and [On-prem](./deployment-options.md#your-boundary). ## Error codes {#errors} 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](#limits) (media, actions, schema); refused before anything is sent | | `invalid_effort` | A `reasoning_effort` the model does not take ([wb.models()](./sdk-assistant.md#model-effort)) | | `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?](./sdk.md#tenant) | | `tenant_key_rejected` · `bad_response` · `http_` | 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](./sdk-keys.md#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](./sdk-computers.md) are not in a vault yet | | `no_screen` | [Vault mode](./sdk-keys.md#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](./sdk-people.md#members): 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()`](./sdk-people.md#members)) | | `not_a_lane` · `self_link` · `already_linked` | [`lane.link()`](./sdk-agents.md#lane-links) 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()`](./sdk-people.md#members) 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](./sdk-assistant.md#dictation) | | `no_hebrew` · `empty` · `tts_rate_limited` · `tts_unavailable` · `audio` · `play` | [Reading aloud](./sdk-assistant.md#speak) | | `locked` · `wrong_passkey` · `backup_required` · `prf_unsupported` · `already_enabled` · `not_enabled` | [Device storage protection](./sdk-accounts.md#protect) | `'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_`. 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](./sdk-builder.md#visitor-limits)), `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 {#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.