Skip to content

Commit aa0875e

Browse files
authored
Add vapi integration (#248)
## Description Describe your changes in detail
1 parent 65665a7 commit aa0875e

7 files changed

Lines changed: 415 additions & 22 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
sidebar_position: 2
3+
title: VAPI Integration
4+
description: Add a VAPI voice AI agent to a Fishjam room with a single API call.
5+
---
6+
7+
import Tabs from "@theme/Tabs";
8+
import TabItem from "@theme/TabItem";
9+
10+
# VAPI Integration
11+
12+
:::info
13+
This tutorial requires a working Fishjam backend. If you haven't set one up yet, please check the [Backend Quick Start](../tutorials/backend-quick-start).
14+
:::
15+
16+
This guide shows how to add a [VAPI](https://vapi.ai/) voice AI agent to a Fishjam room.
17+
Unlike custom agent integrations (such as [Gemini](./gemini-live-integration)), there is no need to build a WebSocket bridge yourself —
18+
Fishjam connects to VAPI internally. You only need to create a VAPI call and pass its ID to Fishjam.
19+
20+
## Overview
21+
22+
The workflow has two steps:
23+
24+
1. **Create a VAPI call** using the VAPI SDK. This gives you a `callId`.
25+
2. **Pass the call ID to Fishjam** via `createVapiAgent()` / `create_vapi_agent()`. Fishjam joins the call and streams audio to and from the room.
26+
27+
:::note
28+
The VAPI peer receives audio from only one peer at a time, so this integration works best in 1-on-1 calls. All peers in the room hear VAPI's responses.
29+
:::
30+
31+
## Prerequisites
32+
33+
You will need:
34+
35+
- **Fishjam Server Credentials:** `fishjamId` and `managementToken`. You can get them at [fishjam.io/app](https://fishjam.io/app).
36+
- **VAPI Private API Key:** Obtainable from the [VAPI Dashboard](https://dashboard.vapi.ai/).
37+
- **VAPI Assistant ID** _(optional)_**:** Create an assistant in the [VAPI Dashboard](https://dashboard.vapi.ai/), or provide a transient assistant configuration inline.
38+
39+
### Installation
40+
41+
<Tabs groupId="language">
42+
<TabItem value="ts" label="TypeScript">
43+
44+
```bash
45+
npm install @fishjam-cloud/js-server-sdk @vapi-ai/server-sdk
46+
```
47+
48+
</TabItem>
49+
50+
<TabItem value="python" label="Python">
51+
52+
```bash
53+
pip install fishjam-server-sdk vapi
54+
```
55+
56+
</TabItem>
57+
</Tabs>
58+
59+
## Implementation
60+
61+
### Step 1: Create a VAPI Call
62+
63+
Use the VAPI SDK to create a call with `vapi.websocket` transport and `pcm_s16le` audio at `16000` Hz.
64+
You can reference an existing assistant by its ID, or create a transient assistant inline by providing the full configuration via the `assistant` field instead of `assistantId`.
65+
66+
<Tabs groupId="language">
67+
<TabItem value="ts" label="TypeScript">
68+
69+
```ts
70+
import { VapiClient, Vapi } from '@vapi-ai/server-sdk';
71+
72+
const vapiClient = new VapiClient({
73+
token: process.env.VAPI_API_KEY!,
74+
});
75+
76+
// [!code highlight:11]
77+
const call = await vapiClient.calls.create({
78+
assistantId: process.env.VAPI_ASSISTANT_ID!,
79+
transport: {
80+
provider: 'vapi.websocket',
81+
audioFormat: {
82+
format: 'pcm_s16le',
83+
container: 'raw',
84+
sampleRate: 16000,
85+
},
86+
},
87+
}) as Vapi.Call;
88+
```
89+
90+
</TabItem>
91+
92+
<TabItem value="python" label="Python">
93+
94+
```python
95+
import os
96+
from vapi import Vapi
97+
98+
vapi_client = Vapi(token=os.environ["VAPI_API_KEY"])
99+
100+
# [!code highlight:11]
101+
call = vapi_client.calls.create(
102+
assistant_id=os.environ["VAPI_ASSISTANT_ID"],
103+
transport={
104+
"provider": "vapi.websocket",
105+
"audioFormat": {
106+
"format": "pcm_s16le",
107+
"container": "raw",
108+
"sampleRate": 16000,
109+
},
110+
},
111+
)
112+
```
113+
114+
</TabItem>
115+
</Tabs>
116+
117+
### Step 2: Add the VAPI Peer to Fishjam
118+
119+
Pass the call ID and your VAPI API key to Fishjam. Fishjam handles the rest.
120+
121+
<Tabs groupId="language">
122+
<TabItem value="ts" label="TypeScript">
123+
124+
```ts
125+
import { VapiClient, Vapi } from '@vapi-ai/server-sdk';
126+
127+
const vapiClient = new VapiClient({
128+
token: process.env.VAPI_API_KEY!,
129+
});
130+
131+
const call = await vapiClient.calls.create({
132+
assistantId: process.env.VAPI_ASSISTANT_ID!,
133+
transport: {
134+
provider: 'vapi.websocket',
135+
audioFormat: {
136+
format: 'pcm_s16le',
137+
container: 'raw',
138+
sampleRate: 16000,
139+
},
140+
},
141+
}) as Vapi.Call;
142+
143+
// ---cut---
144+
import { FishjamClient } from '@fishjam-cloud/js-server-sdk';
145+
146+
const fishjamClient = new FishjamClient({
147+
fishjamId: process.env.FISHJAM_ID!,
148+
managementToken: process.env.FISHJAM_TOKEN!,
149+
});
150+
151+
const room = await fishjamClient.createRoom();
152+
153+
// [!code highlight:4]
154+
await fishjamClient.createVapiAgent(room.id, {
155+
callId: call.id,
156+
apiKey: process.env.VAPI_API_KEY!,
157+
});
158+
```
159+
160+
</TabItem>
161+
162+
<TabItem value="python" label="Python">
163+
164+
```python
165+
import os
166+
from fishjam import FishjamClient, PeerOptionsVapi
167+
168+
fishjam_client = FishjamClient(
169+
fishjam_id=os.environ["FISHJAM_ID"],
170+
management_token=os.environ["FISHJAM_TOKEN"],
171+
)
172+
173+
room = fishjam_client.create_room()
174+
175+
# [!code highlight:2]
176+
options = PeerOptionsVapi(api_key=os.environ["VAPI_API_KEY"], call_id=call.id)
177+
peer = fishjam_client.create_vapi_agent(room.id, options)
178+
```
179+
180+
</TabItem>
181+
</Tabs>
182+
183+
## That's it
184+
185+
Once both steps are complete, the VAPI assistant is live in the room — peers can speak to it and hear its responses.
186+
187+
The VAPI peer's lifetime is tied to the underlying WebSocket connection. If there is a prolonged period of silence or the call ends for any other reason, the peer will disconnect and be removed from the room. You can detect this by listening for `PeerDisconnected` and `PeerDeleted` server notifications. If the peer crashes (e.g. due to an invalid transport configuration or call ID), a `PeerCrashed` notification is sent instead. See [Listening to events](../how-to/backend/server-setup#listening-to-events) for how to subscribe to these notifications.
188+
189+
## Billing
190+
191+
The VAPI peer is billed as a single Fishjam peer. VAPI's own usage pricing applies separately — see [VAPI pricing](https://vapi.ai/pricing) for details.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@docusaurus/types": "^3.10.0",
5959
"@google/genai": "^1.35.0",
6060
"@shikijs/twoslash": "^3.6.0",
61+
"@vapi-ai/server-sdk": "^1.1.0",
6162
"cspell": "^9.1.2",
6263
"docusaurus-plugin-llms": "^0.3.0",
6364
"docusaurus-plugin-typedoc": "1.4.0",

spelling.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,5 @@ Memberof
107107
unmutes
108108
websocat
109109
RTCPIP
110+
VAPI
111+
vapi

versioned_docs/version-0.26.0/api/server-python/fishjam/index.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -355,14 +355,14 @@ Options specific to the VAPI peer
355355
Attributes:
356356
- api_key (str): VAPI API key
357357
- call_id (str): VAPI call ID
358-
- subscribe_mode (Union[Unset, SubscribeMode]): Configuration of peer's subscribing policy
358+
- subscribe_mode (SubscribeMode | Unset): Configuration of peer's subscribing policy
359359

360360
### __init__
361361
```python
362362
def __init__(
363363
api_key: str,
364364
call_id: str,
365-
subscribe_mode: Union[Unset, SubscribeMode] = <Unset object>
365+
subscribe_mode: SubscribeMode | Unset = <Unset object>
366366
)
367367
```
368368
Method generated by attrs for class PeerOptionsVapi.
@@ -381,7 +381,7 @@ call_id: str
381381

382382
### subscribe_mode
383383
```python
384-
subscribe_mode: Union[Unset, SubscribeMode]
384+
subscribe_mode: SubscribeMode | Unset
385385
```
386386

387387

@@ -575,18 +575,18 @@ Describes peer status
575575

576576
Attributes:
577577
- id (str): Assigned peer id Example: 4a1c1164-5fb7-425d-89d7-24cdb8fff1cf.
578-
- metadata (Union['PeerMetadata', None]): Custom metadata set by the peer Example: \{'name': 'FishjamUser'\}.
578+
- metadata (None | PeerMetadata): Custom metadata set by the peer Example: \{'name': 'FishjamUser'\}.
579579
- status (PeerStatus): Informs about the peer status Example: disconnected.
580580
- subscribe_mode (SubscribeMode): Configuration of peer's subscribing policy
581581
- subscriptions (Subscriptions): Describes peer's subscriptions in manual mode
582-
- tracks (list['Track']): List of all peer's tracks
582+
- tracks (list[Track]): List of all peer's tracks
583583
- type_ (PeerType): Peer type Example: webrtc.
584584

585585
### __init__
586586
```python
587587
def __init__(
588588
id: str,
589-
metadata: Optional[PeerMetadata],
589+
metadata: None | PeerMetadata,
590590
status: PeerStatus,
591591
subscribe_mode: SubscribeMode,
592592
subscriptions: Subscriptions,
@@ -604,7 +604,7 @@ id: str
604604

605605
### metadata
606606
```python
607-
metadata: Optional[PeerMetadata]
607+
metadata: None | PeerMetadata
608608
```
609609

610610

versioned_docs/version-0.26.0/api/server-python/fishjam/room/index.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,52 +15,52 @@ class RoomConfig:
1515
Room configuration
1616

1717
Attributes:
18-
- max_peers (Union[None, Unset, int]): Maximum amount of peers allowed into the room Example: 10.
19-
- public (Union[Unset, bool]): True if livestream viewers can omit specifying a token. Default: False.
20-
- room_type (Union[Unset, RoomType]): The use-case of the room. If not provided, this defaults to conference.
21-
- video_codec (Union[Unset, VideoCodec]): Enforces video codec for each peer in the room
22-
- webhook_url (Union[None, Unset, str]): URL where Fishjam notifications will be sent Example:
18+
- max_peers (int | None | Unset): Maximum amount of peers allowed into the room Example: 10.
19+
- public (bool | Unset): True if livestream viewers can omit specifying a token. Default: False.
20+
- room_type (RoomType | Unset): The use-case of the room. If not provided, this defaults to conference.
21+
- video_codec (VideoCodec | Unset): Enforces video codec for each peer in the room
22+
- webhook_url (None | str | Unset): URL where Fishjam notifications will be sent Example:
2323
https://backend.address.com/fishjam-notifications-endpoint.
2424

2525
### __init__
2626
```python
2727
def __init__(
28-
max_peers: Union[NoneType, Unset, int] = <Unset object>,
29-
public: Union[Unset, bool] = False,
30-
room_type: Union[Unset, RoomType] = <Unset object>,
31-
video_codec: Union[Unset, VideoCodec] = <Unset object>,
32-
webhook_url: Union[NoneType, Unset, str] = <Unset object>
28+
max_peers: int | None | Unset = <Unset object>,
29+
public: bool | Unset = False,
30+
room_type: RoomType | Unset = <Unset object>,
31+
video_codec: VideoCodec | Unset = <Unset object>,
32+
webhook_url: None | str | Unset = <Unset object>
3333
)
3434
```
3535
Method generated by attrs for class RoomConfig.
3636

3737
### max_peers
3838
```python
39-
max_peers: Union[NoneType, Unset, int]
39+
max_peers: int | None | Unset
4040
```
4141

4242

4343
### public
4444
```python
45-
public: Union[Unset, bool]
45+
public: bool | Unset
4646
```
4747

4848

4949
### room_type
5050
```python
51-
room_type: Union[Unset, RoomType]
51+
room_type: RoomType | Unset
5252
```
5353

5454

5555
### video_codec
5656
```python
57-
video_codec: Union[Unset, VideoCodec]
57+
video_codec: VideoCodec | Unset
5858
```
5959

6060

6161
### webhook_url
6262
```python
63-
webhook_url: Union[NoneType, Unset, str]
63+
webhook_url: None | str | Unset
6464
```
6565

6666

0 commit comments

Comments
 (0)