--- title: 'Voice' description: 'Save the LiveKit project your box opens voice rooms on, once per box, from Settings › Voice settings in Studio. The secret goes in and never comes back out.' position: 22 --- # Voice > Voice calls run on a **LiveKit** project: a LiveKit server, on LiveKit Cloud > or your own, that carries the audio between a caller and your agent. Your > **box** (the container running your workspace, on your laptop under > `stackbone dev` or deployed in your cloud) needs that project's URL, API key > and API secret to open a room. You save the three once per box, in > **Settings › Voice settings** in Studio. The key and the secret go in and > never come back out. ## Save the LiveKit credentials
1. **Open Settings › Voice settings** in Studio. A box with nothing saved says **Not configured**. 2. **Paste the URL, the API key and the API secret** from your LiveKit project's settings. The URL starts with `wss://`, or with `https://`, which is the same server under the name the LiveKit server SDKs use. Studio refuses `ws://` and `http://` before it sends anything: the room tokens your box signs travel to that address. 3. **Click Save.** The status turns to **Configured** and shows the URL. The API key and API secret fields empty themselves.
The box keeps the three values in its own secret store, encrypted at rest. The change applies to the next call, with no restart. ## What Studio shows afterwards | Value | What the screen shows | | ---------- | ---------------------------------------------------------- | | URL | The address, in clear. It is an address, not a credential. | | API key | `Saved`. Never the value, a mask of it or its length. | | API secret | `Saved`. Never the value, a mask of it or its length. | No screen, no route of the box's HTTP API and no CLI command returns the key or the secret. If you need them again, copy them from LiveKit. ### LiveKit status Once the credentials are saved, Studio shows whether your box can reach LiveKit with them. You see the status on the Voice settings screen and next to **Call** on an agent's **Voice** tab. | Status | What it means | | ----------------------- | ----------------------------------------------------------------------------------------------------------- | | **LiveKit reachable** | The box asked LiveKit for its rooms with the saved credentials, and LiveKit answered. | | **LiveKit unreachable** | The box could not reach LiveKit, or LiveKit refused the key or the secret. Check the URL, key and secret. | | **Not configured** | The box has no credentials it can read. The Voice tab links to Voice settings, where you save them (again). | The status only checks the credentials. It does not tell you whether a voice worker (the process that joins the room and speaks for your agent) runs on that LiveKit project: LiveKit has no API that lists them. **Call** stays available in every status. Under `stackbone dev`, the terminal tells you when the worker is ready: see [Calls under `stackbone dev`](/docs/home/features/voice#calls-under-stackbone-dev). The box keeps each answer for a few seconds, so **Check again** right after a fix can show the old result for a moment. The check needs the `runs:invoke` permission, the same one that places a call. Without it, Studio shows no status. ## Replace or remove them To replace them, type all three again and click **Save**. The box takes the URL, the key and the secret as one write, so a new URL never runs with a key that belongs to another project. To remove them, click **Remove** and confirm. The three values are deleted together. Voice agents on this box cannot open a call until you save new ones. ## Who can change them Reading the status, saving and removing all need the `secrets:rotate` permission, which the owner and admin roles hold. Any other role sees **Your role cannot open this** on the screen. Studio's **Activity** screen records each save with the URL and the person who saved, and each removal with the person who removed. Neither entry holds the key or the secret. [Governance](/docs/home/features/governance) covers that trail. ## They are not ordinary secrets The box stores the three values under the names `LIVEKIT_URL`, `LIVEKIT_API_KEY` and `LIVEKIT_API_SECRET`, and it reserves those names. They do not appear on the **Secrets** screen, and you cannot create, replace or delete a secret with one of those names there. A secret of your own with a similar name (`MY_LIVEKIT_API_KEY`, `LIVEKIT_URL_BACKUP`) is an ordinary secret and stays yours. The screen calls `GET`, `PUT` and `DELETE /api/voice-provider` on your box. The [API reference](/docs/home/features/api-reference) your box serves documents the three. ## Calls under `stackbone dev` A call needs a **voice worker**: the process that joins the LiveKit room, hears the caller and speaks your agent's replies. Under `stackbone dev` you do not start one. The CLI starts it on your machine when both of these are true: - At least one deep agent in the workspace declares `voice: true`. - The box has LiveKit credentials saved in **Settings › Voice settings**. When LiveKit accepts the worker, the terminal prints: ```text ◆ Voice worker ready (stackbone-voice-dev-1a2b3c4d) ``` From then on, **Call** on the agent's **Voice** tab reaches that worker. While the session runs, the CLI keeps the worker in step with your workspace and your box. You do not restart `stackbone dev` for any of these changes: | You | The CLI | | ------------------------------------------------------ | -------------------------------------- | | Add the first `voice: true` agent, or save credentials | Starts the worker. | | Remove the last voice agent, or remove the credentials | Stops the worker. | | Save different credentials | Restarts the worker with the new ones. | ### The first run installs the worker The worker ships inside the CLI, at the same version as the box it talks to. Its dependencies do not. The first time a session needs the worker, the CLI installs them with `npm` into `~/.cache/stackbone/voice-worker//`, and the terminal shows **Installing the voice worker** while it runs. The download is about 270 MB, most of it the ONNX runtime. On a slow network it can take a few minutes. Later sessions on the same CLI version reuse that folder. A new CLI version installs into a folder of its own. The folders of older versions stay until you delete them. ### Each session has its own worker name Each `stackbone dev` process picks a random worker name, `stackbone-voice-dev-` followed by 8 hex characters, and its box sends every call to that name. No other worker takes those calls, even on the same LiveKit project: - a worker you deployed to LiveKit Cloud, which registers as `stackbone-voice`; - the `stackbone dev` of another developer; - a second `stackbone dev` on your own machine. So a worker on LiveKit Cloud is of no use under `stackbone dev`, and you need none to test calls on your machine. Your dev worker does not take the calls of a deployed box either. The worker reaches the box at its local address, `http://127.0.0.1:4242` by default, also when the tunnel is open. Its requests to the box stay on your machine. ### When the worker fails If the worker cannot install, cannot start or exits, the CLI starts it once more. After a second failure, the CLI prints one warning with the cause and `stackbone dev` keeps running with no worker. Calls stay unanswered until you change the voice agents or the LiveKit credentials: either change starts a new worker. If you have nothing to change, restart `stackbone dev`. In Studio, a call that no worker joins within 15 seconds shows **No voice worker joined the call**. A call under `stackbone dev` records nothing, even with recording on: LiveKit cannot reach the bucket on your machine. See [When the box records nothing](/docs/home/features/voice#when-the-box-records-nothing). ## Remember callers An agent can opt into reading and saving customer memory independently. Telephone calls supply the original customer's number under `phoneNumber`; the agent must have that fixed identity key to select it. Browser calls without an explicit test identity continue without memory. Settings stay fixed for the call. Reading recalls scoped memory before the first output. Saving runs once after closure and uses only the committed conversation the caller heard. Uncertain interrupted output is omitted. See [Memory during voice calls](/docs/sdk/data/memory#memory-during-voice-calls) for the four modes and identity contract. ## Recording calls Recording is off by default: the box keeps no audio of a call unless the agent turns recording on, since a caller may need to consent first. With it on, your box records each call of that agent to its own bucket as an audio-only OGG file, and Studio plays it back from the call's session. A recording that fails never ends the call. ### Turn recording on for one agent Set `recording: true` in the agent's `voiceSettings`: ```ts import { defineDeepAgent } from '@stackbone/sdk/deep'; export default defineDeepAgent({ name: 'support-line', model: 'openai/gpt-5-mini', voice: true, voiceSettings: { recording: true }, }); ``` The agent's **Voice** tab in Studio has the same switch, **Record the call**. A value saved in Studio wins over the one in code, and **Reset** puts the code value back. The change applies to the next call. ### Give LiveKit a key for the recordings only LiveKit uploads the recording to your bucket itself, so the box hands LiveKit the S3 key it uploads with. By default that is the box's own key, which opens the whole bucket. To hand LiveKit a narrower one, set these two variables on the box: | Variable | What it is | | ----------------------------------------- | --------------------------------------------------------------- | | `STACKBONE_VOICE_RECORDING_S3_ACCESS_KEY` | Access key that can write under `/voice/recordings/`. | | `STACKBONE_VOICE_RECORDING_S3_SECRET_KEY` | Its secret key. | `` is your box's `STACKBONE_AGENT_ID`, the prefix of every key the box writes to the bucket. It is not the name of the voice agent. The recordings still go to the same bucket and endpoint. Set both or neither: with only one, the box refuses to start and names the two variables. The box never logs either value. ### Play a recording Open the call's session in Studio. The player reads a signed link that stays valid for one hour, so you can seek through a long call. Playing it needs the `runs:invoke` permission, the same one that places a call. ### When the box records nothing LiveKit cannot reach a bucket on your own machine or private network. When the bucket address is `localhost`, a loopback or private IP, a name with no dot (such as `minio` in a Compose network), or a name that only a private resolver answers (`.local`, `*.svc` and `*.svc.cluster.local` in Kubernetes, `.internal`, `.home.arpa`), the box opens the call as usual, records nothing, and logs one line that names the address. This is the case under `stackbone dev`.