# Sessions API

> Bash in a browser: its live screen, a voice call, or the whole of Bash inside your app.

Send `Authorization: Bearer vk_test_...` on every call.

## `POST /v1/sessions`

Its own page: [POST /v1/sessions](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions.md)

Open a session for one of your users (user_id), or for the company's own Bash: chat, a screen to watch or drive, a voice call, and what Bash remembers (memory, when your company turned the panel on). Answers a 15 minute token for the user's browser, a ready page url and the sites allowed to frame it. The token reaches only that user, and only the session's conversation.

Your API key, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample.

### Request

Body:

- `user_id` (`string`): Your id for the user this session is for. Absent: your company's own Bash. Example: `traveller-42`.
- `conversation_id` (`string`): A conversation of that user's to open it in (conv_...). Absent, with chat or voice: a new conversation on the user's default deployment, then your company's. Limits: at least 1 character.
- `chat` (`boolean`): Show the conversation, so your user can talk to Bash and answer its approvals. Default: false. Default: `false`.
- `screen` (`false | "view" | "control"`): Show Bash's screen: view to watch it, control to also take the wheel. Default: false, no screen. Default: `false`.
- `voice` (`boolean | object`): Let your user talk to Bash by voice: true, or a voice by name. Default: false. Default: `false`.
- `memory` (`boolean`): Show what Bash remembers about your user. Your company must turn it on first (PATCH /v1/org, memory_panel: true). Default: false. Default: `false`.
- `driver` (`"bash" | "user"`): With screen: control, who drives when the screen opens. bash (the default): your user watches and taps Take control. user: your user drives from the first frame the page shows, with no Take control and nothing to give back, for a page that is theirs to use (a sign-in Bash opened for them).

### Answers 201

- `id` (`string`): The session (ses_...). Example: `ses_8h2`.
- `object` (`"session"`): Always session.
- `user_id` (`string | null`): Your id for its user; null on your company's own Bash.
- `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone.
- `chat` (`boolean`): Whether it shows the conversation.
- `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none.
- `voice` (`boolean`): Whether your user can talk by voice.
- `memory` (`boolean`): Whether it shows what Bash remembers.
- `token` (`string`): The session's own token, shown this once and good for 15 minutes. It reaches this user's session and conversation only: messages, events, approvals, files and memory. Never your account, keys or usage.
- `url` (`string`): The page to show your user, in a tab or an iframe; it carries the token.
- `frame_ancestors` (`string[]`): The sites that may frame the page: your company's allowed_embed_origins. Empty: it opens only as a page of its own.
- `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time.

### Code

**TypeScript**

```ts
const session = await vestra.user('traveller-42').embed();
```

**Python**

```python
session = vestra.user("traveller-42").embed()
```

**CLI**

```bash
vestra embed --user traveller-42
```

**HTTP**

```bash
# Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals,
# the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an
# iframe with allow="microphone".
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }'

# Check the session. See whether it is still open.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"

# End the session. Its token stops working at once.
curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "embed",
    "arguments": { "user": "traveller-42", "memory": true }
  }
}
```

## `GET /v1/sessions/{session_id}`

Its own page: [GET /v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id.md)

Whether a session is still open, and what it was opened with. Your key, or the session's own token.

Your API key, or a session's token, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

### Answers 200

- `id` (`string`): The session (ses_...). Example: `ses_8h2`.
- `object` (`"session"`): Always session.
- `user_id` (`string | null`): Your id for its user; null on your company's own Bash.
- `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone.
- `chat` (`boolean`): Whether it shows the conversation.
- `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none.
- `voice` (`boolean`): Whether your user can talk by voice.
- `memory` (`boolean`): Whether it shows what Bash remembers.
- `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time.
- `status` (`"live" | "expired" | "revoked"`): live: its token works. expired: its 15 minutes are up. revoked: it was ended early.
- `screen_paused` (`true`, may be absent): A view-only screen while it is private: a person signs in on that computer (a browser fetch with front, a hand-off), or a private fetch runs or keeps its tab. Nothing shows, and GET /v1/sessions/{session_id}/screen answers 409 browser_in_use, until it is done. Absent otherwise.

### Code

**TypeScript**

```ts
const session = await vestra.sessions.get('ses_8h2');
```

**Python**

```python
session = vestra.sessions.get("ses_8h2")
```

**HTTP**

```bash
# Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals,
# the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an
# iframe with allow="microphone".
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }'

# Check the session. See whether it is still open.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"

# End the session. Its token stops working at once.
curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "embed",
    "arguments": { "user": "traveller-42", "memory": true }
  }
}
```

**CLI**: No sample in your language for this route, so here it is over HTTP.

## `DELETE /v1/sessions/{session_id}`

Its own page: [DELETE /v1/sessions/{session_id}](https://vestra.ai/developers/reference/api/sessions/delete-v1-sessions-session-id.md)

End a session now: its token stops working, its streams close and its call hangs up. Your key, or the session's own token.

Your API key, or a session's token, scope `runtime` or `all`. On the card: [`embed`](https://vestra.ai/developers/embed.md). Every call here is a checked sample.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

### Answers 200

- `id` (`string`): The session (ses_...). Example: `ses_8h2`.
- `object` (`"session"`): Always session.
- `user_id` (`string | null`): Your id for its user; null on your company's own Bash.
- `conversation_id` (`string | null`): Its conversation (conv_...); null for a screen opened alone.
- `chat` (`boolean`): Whether it shows the conversation.
- `screen` (`false | "view" | "control"`): Its screen: view, control, or false for none.
- `voice` (`boolean`): Whether your user can talk by voice.
- `memory` (`boolean`): Whether it shows what Bash remembers.
- `expires_at` (`string`): When the token stops working, 15 minutes after it was made, as an ISO 8601 time.
- `status` (`"live" | "expired" | "revoked"`): live: its token works. expired: its 15 minutes are up. revoked: it was ended early.
- `screen_paused` (`true`, may be absent): A view-only screen while it is private: a person signs in on that computer (a browser fetch with front, a hand-off), or a private fetch runs or keeps its tab. Nothing shows, and GET /v1/sessions/{session_id}/screen answers 409 browser_in_use, until it is done. Absent otherwise.

### Code

**TypeScript**

```ts
await vestra.sessions.end('ses_8h2');
```

**Python**

```python
vestra.sessions.end("ses_8h2")
```

**HTTP**

```bash
# Open a session on your server. Do this for the user signed in to your app. It gives them chat, approvals,
# the live screen and voice. The response has a url and a token, valid for 15 minutes. Put the url in an
# iframe with allow="microphone".
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "chat": true, "screen": "view", "voice": true }'

# Check the session. See whether it is still open.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"

# End the session. Its token stops working at once.
curl -X DELETE "$VESTRA_BASE_URL/v1/sessions/ses_8h2" \
  -H "Authorization: Bearer $VESTRA_API_KEY"
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "embed",
    "arguments": { "user": "traveller-42", "memory": true }
  }
}
```

**CLI**: No sample in your language for this route, so here it is over HTTP.

## `GET /v1/sessions/{session_id}/frame-policy`

Its own page: [GET /v1/sessions/{session_id}/frame-policy](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-frame-policy.md)

Which sites may show a session's page. The page asks this itself; you never need to.

No key: anyone may call it. On the card: [`embed`](https://vestra.ai/developers/embed.md). Over HTTP it is made from the contract: fill in each {…} and every value.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

### Answers 200

- `object` (`"frame_policy"`): Always frame_policy.
- `frame_ancestors` (`string[]`): The sites that may frame the session's page; empty when it may open only as a page of its own.

### Code

**HTTP**

```bash
curl "$VESTRA_BASE_URL/v1/sessions/{session_id}/frame-policy"
```

**TypeScript, Python, CLI, MCP**: No sample in your language for this route, so here it is over HTTP.

## `GET /v1/sessions/{session_id}/screen`

Its own page: [GET /v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/get-v1-sessions-session-id-screen.md)

With the session's token: the stream credentials of the user's Bash browser, good for 5 minutes. For a session opened with a screen. 409 payment_in_progress while the person pays for something in their browser.

A session's token. On the card: [`watch`](https://vestra.ai/developers/watch.md). Over HTTP it is made from the contract: fill in each {…} and every value.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

### Answers 200

- `object` (`"browser_stream"`): Always browser_stream.
- `neko_url` (`string`): The address of the screen's stream.
- `preview_token` (`string | null`): A second token the stream address asks for, when it asks for one; null otherwise.
- `preview_token_param` (`string`, may be absent): The query parameter preview_token goes in, on the stream address and its picture. Absent: bl_preview_token. Example: `_token`.
- `neko_token` (`string`): The token for the screen's stream: good for 5 minutes, so read this again for a fresh one.
- `ice_servers` (`object[]`): Servers that help the WebRTC connection to the screen through firewalls; empty when none are set.
  - `urls` (`string[]`): The server's addresses (stun: or turn:).
  - `username` (`string`, may be absent): Its username, for a TURN server.
  - `credential` (`string`, may be absent): Its password, for a TURN server.
- `width` (`integer`): The screen's width in pixels. Example: `1440`.
- `height` (`integer`): The screen's height in pixels. Example: `900`.
- `crop` (`object`, may be absent): While a hand-off is open on the screen (a browser fetch with front, such as a sign-in): the part of the screen its page fills, at a phone's size, or only the element its front_crop named. Show only this part, and map taps into it. Absent: show the whole screen.
  - `x` (`integer`): Its left edge, in screen pixels.
  - `y` (`integer`): Its top edge, in screen pixels.
  - `width` (`integer`): Its width, in screen pixels.
  - `height` (`integer`): Its height, in screen pixels.
- `crop_pending` (`true`, may be absent): A hand-off's front_crop is not on its page yet (for up to 20 seconds from its start): show no picture yet, and read this again in a couple of seconds for its crop. Absent: crop says what to show.

### Code

**TypeScript**

```ts
const session = await vestra.user('traveller-42').watch();
const screen = await session.screen();
```

**Python**

```python
session = vestra.user("traveller-42").watch()
screen = session.screen()
```

**HTTP**

```bash
curl "$VESTRA_BASE_URL/v1/sessions/{session_id}/screen" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

**CLI, MCP**: No sample in your language for this route, so here it is over HTTP.

## `POST /v1/sessions/{session_id}/screen`

Its own page: [POST /v1/sessions/{session_id}/screen](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-screen.md)

With the session's token, in control mode: the user takes the wheel or gives it back (action), or says they are driving (input), so Bash waits.

A session's token. On the card: [`watch`](https://vestra.ai/developers/watch.md). Every call here is a checked sample.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

Body:

Either `{ action }`:

- `action` (`"take" | "give_back"`, required): take: the person takes the wheel and Bash waits. give_back: Bash drives again.

Or `{ input }`:

- `input` (`true`, required): The person is typing or clicking now: Bash holds off while these keep coming, each good for a few seconds.

### Answers 200

Either `{ object, by }`:

- `object` (`"screen_control"`): Always screen_control.
- `by` (`"user" | "bash"`): Who drives the screen now: user, the person, or bash.

Or `{ object, delivered }`:

- `object` (`"screen_input"`): Always screen_input.
- `delivered` (`boolean`): Whether Bash's computer heard it; false when it is not connected.

### Code

**TypeScript**

```ts
const session = await vestra.user('traveller-42').watch({ control: true });
await session.take();
```

**Python**

```python
session = vestra.user("traveller-42").watch(control=True)
session.take()
```

**HTTP**

```bash
# Watch the screen live. Create a session with a screen to watch, and open its url in a browser tab or an iframe.
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "screen": "view" }'

# Let a person take control. In a session opened with "screen": "control", the person can take over from Bash,
# using the session's own token.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/screen" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "action": "take" }'

# Take a screenshot. You get the latest frame of the user's browser as a JPEG.
curl "$VESTRA_BASE_URL/v1/users/traveller-42/screenshot" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -o screen.jpg
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "watch",
    "arguments": { "user": "traveller-42", "control": true }
  }
}
```

**CLI**: No sample in your language for this route, so here it is over HTTP.

## `GET /v1/users/{user_id}/screenshot`

Its own page: [GET /v1/users/{user_id}/screenshot](https://vestra.ai/developers/reference/api/sessions/get-v1-users-user-id-screenshot.md)

The latest frame of the user's browser, as a JPEG. 409 browser_in_use while a person finishes a sign-in on it (a browser fetch with front open): its page is for them alone. 409 payment_in_progress while the person pays for something in their browser, or just after.

Your API key, scope `runtime` or `all`. On the card: [`watch`](https://vestra.ai/developers/watch.md). Every call here is a checked sample.

### Request

Path:

- `user_id` (`string`, required): Your own id for the user, as you named them.

### Answers 200

The file itself (image/jpeg).

### Code

**TypeScript**

```ts
const frame = await vestra.user('traveller-42').screenshot();
```

**Python**

```python
frame = vestra.user("traveller-42").screenshot()
```

**HTTP**

```bash
# Watch the screen live. Create a session with a screen to watch, and open its url in a browser tab or an iframe.
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "screen": "view" }'

# Let a person take control. In a session opened with "screen": "control", the person can take over from Bash,
# using the session's own token.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/screen" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "action": "take" }'

# Take a screenshot. You get the latest frame of the user's browser as a JPEG.
curl "$VESTRA_BASE_URL/v1/users/traveller-42/screenshot" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -o screen.jpg
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "watch",
    "arguments": { "user": "traveller-42", "control": true }
  }
}
```

**CLI**: No sample in your language for this route, so here it is over HTTP.

## `POST /v1/sessions/{session_id}/voice`

Its own page: [POST /v1/sessions/{session_id}/voice](https://vestra.ai/developers/reference/api/sessions/post-v1-sessions-session-id-voice.md)

Called from the user's browser with the session's token and its WebRTC offer; answers the model's answer. Audio then flows straight to the voice model, and everything said lands in the session's conversation. For a session opened with voice.

A session's token. On the card: [`talk`](https://vestra.ai/developers/talk.md). Every call here is a checked sample.

### Request

Path:

- `session_id` (`string`, required): The session's id (`ses_...`).

Body:

- `sdp` (`string`, required): The browser's WebRTC offer (its SDP), in at most 65,536 characters. Limits: 1 to 65536 characters.

### Answers 201

- `object` (`"session_voice"`): Always session_voice.
- `sdp` (`string`): The answer to the browser's offer (its SDP): hand it to the browser's connection.
- `voice` (`string`): The voice Bash speaks in.

### Code

**HTTP**

```bash
# Call in an existing conversation. Open the session's url, and the page joins the call.
curl "$VESTRA_BASE_URL/v1/sessions" \
  -H "Authorization: Bearer $VESTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "traveller-42", "conversation_id": "conv_8f2k", "voice": true }'

# Connect your own voice client. The session's page does this for you: it sends the browser's WebRTC offer
# with the session's own token, and gets the answer back. Call it yourself only if you build your own client.
curl "$VESTRA_BASE_URL/v1/sessions/ses_8h2/voice" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "sdp": "the WebRTC offer from the browser" }'
```

**MCP**

```json
{
  "method": "tools/call",
  "params": {
    "name": "talk",
    "arguments": { "user": "traveller-42", "conversation": "conv_8f2k" }
  }
}
```

**TypeScript, Python, CLI**: No sample in your language for this route, so here it is over HTTP.
