> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rumik.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# web call

> start a call and let the browser join it over webrtc.

`POST /v1/webcall` starts a call and returns the credentials a browser needs to
join it. the media flows browser ↔ agent directly; it never touches your
servers.

<Steps>
  <Step title="start the call on your server">
    your backend calls `/v1/webcall` with your api key and the agent to run.
  </Step>

  <Step title="pass the credentials to the page">
    send the `host`, `token` and `roomName` down to the browser. the token is
    short-lived and scoped to this one room.
  </Step>

  <Step title="join">
    the page connects with a livekit client and enables the microphone. the
    agent greets the caller as soon as it sees them join.
  </Step>
</Steps>

if a [before-call tool](/variables-and-tools) of the agent needs to know the
call's id, start the call in two steps instead — see
[knowing the call id first](#knowing-the-call-id-first).

## request

exactly one of:

<ParamField body="agentId" type="string">
  the agent to run — its UUID or its `ua_…` handle. `agent_id` is accepted too.
  needs your api key in `Authorization`. the call's id is minted inside this
  request: the agent's before-call tools run with it before you get it back.
</ParamField>

<ParamField body="accessToken" type="string">
  the single-use `wct_` token a [register-call](/register-call) returned. it is
  the credential on its own — send no api key — and the call starts under the
  `call_id` that response already gave you. `access_token` is accepted too.
</ParamField>

<CodeGroup>
  ```bash one hop theme={null}
  curl -X POST https://silk-api.rumik.ai/v1/webcall \
    -H "Authorization: Bearer rk_live_•••••••••" \
    -H "Content-Type: application/json" \
    -d '{ "agentId": "ua_fe3277d8" }'
  ```

  ```bash registered call theme={null}
  curl -X POST https://silk-api.rumik.ai/v1/webcall \
    -H "Content-Type: application/json" \
    -d '{ "accessToken": "wct_9f8c2b1e4d5a4e6fa7b8c9d0e1f2a3b4" }'
  ```
</CodeGroup>

## response

<ResponseField name="token" type="string">
  short-lived livekit access token. give it to the browser client; do not reuse
  it for a second call.
</ResponseField>

<ResponseField name="host" type="string">
  the livekit websocket URL to connect to.
</ResponseField>

<ResponseField name="roomName" type="string">
  the room that was created for this call.
</ResponseField>

<ResponseField name="callId" type="string">
  the call's id. it is what shows up in **conversations** and in your usage, so
  store it if you want to correlate a call with your own records later. your
  tools receive the same value as `{call_id}`. for a registered call it is the
  `call_id` you were already given.
</ResponseField>

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
  "host": "wss://livekit.rumik.ai",
  "roomName": "call-891e23fe503944c28dbf5e5a2a105190",
  "callId": "01a05b4d-b1ad-7be0-8d29-04b2fd7e7dca"
}
```

## knowing the call id first

a one-hop start does everything inside the request: it mints the call's id,
runs the agent's before-call tools (which receive it as `{call_id}`), starts
the agent, and only then answers. so if a before-call tool of yours is meant to
look the call up by its id, that request reaches your endpoint before `callId`
has reached you, and there is nothing yet to match it against.

start such a call in two steps. [register it](/register-call) first: that
mints the id and starts nothing. record the id against your own session, then
start the call with the token — the before-call tools run now, under an id
you already hold.

<Steps>
  <Step title="register">
    `POST /v1/register-call` with your api key. store the `call_id` it
    returns next to whatever your tool will need when it is asked about it.
  </Step>

  <Step title="start">
    `POST /v1/webcall` with `{ "accessToken": "wct_…" }` and no api key. this is
    where billing and the before-call tools happen; `callId` in the response
    equals the registered `call_id`.
  </Step>

  <Step title="join">
    as before: hand `host`, `token` and `roomName` to the page.
  </Step>
</Steps>

the token is single-use and expires in minutes, and it does not carry your
key, so the page may run step 2 itself: register on your server, give the
page the token, and let it call `/v1/webcall` directly.

<Warning>
  any non-200 from step 2 spends the token — `429`, `409` and `402` included.
  do not retry with the same token; it answers `401`. recovery is a fresh
  `/v1/register-call`, which gives you a **new** `call_id` — discard the one
  you stored for the failed start.
</Warning>

<CodeGroup>
  ```python your server theme={null}
  import requests

  API_KEY = "rk_live_•••••••••"
  BASE = "https://silk-api.rumik.ai"

  def start_call(agent: str, customer_ref: str) -> dict:
      reg = requests.post(
          f"{BASE}/v1/register-call",
          headers={"Authorization": f"Bearer {API_KEY}"},
          json={"agent_id": agent},
          timeout=30,
      )
      reg.raise_for_status()
      call_id = reg.json()["call_id"]
      sessions[call_id] = customer_ref      # your before-call tool looks this up
      try:
          r = requests.post(
              f"{BASE}/v1/webcall",         # no api key: the token is the credential
              json={"accessToken": reg.json()["access_token"]},
              timeout=30,
          )
          r.raise_for_status()
      except Exception:
          sessions.pop(call_id, None)       # the token is spent; register again for a new id
          raise
      return r.json()                       # callId == call_id -> forward to the page
  ```

  ```javascript node theme={null}
  const reg = await fetch("https://silk-api.rumik.ai/v1/register-call", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RUMIK_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ agent_id: "ua_fe3277d8" }),
  }).then((r) => r.json());

  await sessions.set(reg.call_id, customerRef);   // before the call starts

  const res = await fetch("https://silk-api.rumik.ai/v1/webcall", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ accessToken: reg.access_token }),
  });
  if (!res.ok) {
    await sessions.delete(reg.call_id);   // the token is spent; register again for a new id
    const { code, error } = await res.json();
    throw new Error(`${res.status} ${code}: ${error}`);
  }
  const call = await res.json();   // {token, host, roomName, callId: reg.call_id}
  ```
</CodeGroup>

## joining from the browser

install a livekit client (`npm i livekit-client`, or the CDN build below) and
connect with the `host` and `token` you were given.

<CodeGroup>
  ```html browser theme={null}
  <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
  <audio id="agent" autoplay></audio>
  <script>
  const { Room, RoomEvent, Track } = LivekitClient;

  async function join() {
    // your own endpoint, which calls /v1/webcall server-side and forwards the
    // result — the rk_live_ key never reaches the browser.
    const call = await fetch("/api/start-call", { method: "POST" }).then((r) => r.json());

    const room = new Room();
    room.on(RoomEvent.TrackSubscribed, (track) => {
      if (track.kind === Track.Kind.Audio) track.attach(document.getElementById("agent"));
    });

    await room.connect(call.host, call.token);
    await room.localParticipant.setMicrophoneEnabled(true);   // caller can now talk
    return room;                                              // room.disconnect() to hang up
  }
  </script>
  ```

  ```python your server theme={null}
  import requests

  API_KEY = "rk_live_•••••••••"
  BASE = "https://silk-api.rumik.ai"

  def start_call(agent: str) -> dict:
      r = requests.post(
          f"{BASE}/v1/webcall",
          headers={"Authorization": f"Bearer {API_KEY}"},
          json={"agentId": agent},
          timeout=30,
      )
      r.raise_for_status()
      return r.json()          # {token, host, roomName, callId} -> forward to the page
  ```

  ```javascript node theme={null}
  const res = await fetch("https://silk-api.rumik.ai/v1/webcall", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RUMIK_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ agentId: "ua_fe3277d8" }),
  });

  if (!res.ok) {
    const { code, error } = await res.json();
    throw new Error(`${res.status} ${code}: ${error}`);
  }
  const call = await res.json();   // {token, host, roomName, callId}
  ```
</CodeGroup>

## ending a call

the call ends when the caller leaves the room — `room.disconnect()`, closing the
tab, or losing the network. it also ends on its own when it reaches the agent's
maximum session length, or the length your balance can fund, whichever is
shorter.

you are billed for the seconds the call actually ran. the transcript and
recording land in **conversations** shortly after it ends.

## errors

see the [error table](/voice-agents#errors). the ones you will meet in normal
operation:

<AccordionGroup>
  <Accordion title="401 unauthorized (registered call)">
    the `accessToken` is unknown, expired or already spent — the three are
    deliberately indistinguishable. register again; tokens are cheap.
  </Accordion>

  <Accordion title="409 agent_not_deployed">
    the agent was saved but never deployed, so no version is live to answer.
    press **deploy** in the dashboard; nothing is started or billed until you
    do. a registered call's token is spent by this answer: register again
    once the agent is deployed.
  </Accordion>

  <Accordion title="402 insufficient_balance">
    the account cannot fund a usable call. top up, or enable auto top-up in the
    dashboard so this does not interrupt live traffic. a registered call's
    token is spent by this answer: register again after topping up.
  </Accordion>

  <Accordion title="429 concurrency_limit_exceeded">
    every concurrent slot is in use. the body carries `active_requests` and
    `limit`. for a one-hop start, queue the caller and retry the same request
    when a slot frees, or raise your plan capacity. for a registered call the
    token is already spent: retry with a fresh `/v1/register-call`, under the
    new `call_id` it returns.
  </Accordion>
</AccordionGroup>
