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

# Implement Browser-Based Voice Calls with WebRTC

> Build browser-based voice calls with AI agents using WebRTC and WebSocket signaling.

Build browser-based voice calls with WebRTC. Handle WebSocket signaling, SDP exchange, and audio streams — with complete code for the full call lifecycle.

**What you'll learn:** WebRTC setup, SDP offer/answer exchange, audio handling, and call termination.

<Note>
  **Testing requires a valid API key.** Generate a web integration token from your agent's Web Integrations settings in the Dasha BlackBox dashboard before starting.
</Note>

## Prerequisites

Before you start, ensure you have:

* A Dasha BlackBox agent with voice enabled
* A web integration token (from Dashboard → Agent → Web Integrations)
* The `AllowWebCall` feature enabled on your web integration
* HTTPS environment (WebRTC requires secure context)
* User permission for microphone access

<Accordion title="Voice call architecture">
  Voice calls use WebRTC for real-time audio and WebSocket for signaling:

  ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
  sequenceDiagram
      participant Browser
      participant WebSocket as Dasha BlackBox Server
      participant Agent as AI Agent

      Browser->>WebSocket: WebSocket connect
      WebSocket->>Browser: Connection established
      Browser->>WebSocket: initialize (callType: "webCall")
      WebSocket->>Browser: event (name: "connection")
      WebSocket->>Browser: sdpInvite (SDP offer)

      Browser->>Browser: Create RTCPeerConnection
      Browser->>Browser: Set remote description (offer)
      Browser->>Browser: Get user media (microphone)
      Browser->>Browser: Create SDP answer

      Browser->>WebSocket: sdpAnswer (SDP answer)
      WebSocket->>Agent: WebRTC connection established

      Note over Browser,Agent: Bidirectional audio stream

      Browser->>WebSocket: terminate
      WebSocket->>Browser: conversationResult
      WebSocket->>Browser: event (name: "closed")
  ```
</Accordion>

***

## Step 1: Establish WebSocket connection

Connect and initialize with `callType: "webCall"`:

<Accordion title="Example: WebSocket connection for voice call">
  ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const token = 'YOUR_WEB_INTEGRATION_TOKEN';
  const ws = new WebSocket(
    `wss://blackbox.dasha.ai/api/v1/ws/webCall?token=${token}`
  );

  ws.onopen = () => {
    ws.send(JSON.stringify({
      type: 'initialize',
      timestamp: new Date().toISOString(),
      request: {
        callType: 'webCall',
        additionalData: {
          userId: 'user-123'
        }
      }
    }));
  };
  ```
</Accordion>

***

## Step 2: Handle SDP offer

After initialization, the server sends an `sdpInvite` message with the WebRTC offer:

<Accordion title="Example: SDP invite message format">
  ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {
    "type": "sdpInvite",
    "timestamp": "2025-01-20T10:00:02Z",
    "data": {
      "sdpOffer": "v=0\r\no=- 1234567890 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n..."
    }
  }
  ```
</Accordion>

### SdpInviteMessage schema

| Field           | Type                | Description             |
| --------------- | ------------------- | ----------------------- |
| `type`          | `"sdpInvite"`       | Message type identifier |
| `timestamp`     | `string` (ISO 8601) | Message timestamp       |
| `data.sdpOffer` | `string`            | WebRTC SDP offer        |

***

## Step 3: Create peer connection and send answer

Create an `RTCPeerConnection`, set the remote offer, get microphone access, and send the answer:

<Accordion title="Example: Handle SDP offer and create peer connection">
  ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  let peerConnection = null;
  let localStream = null;

  ws.onmessage = async (event) => {
    const message = JSON.parse(event.data);

    switch (message.type) {
      case 'event':
        if (message.name === 'connection') {
          console.log('Agent session ready, waiting for SDP offer...');
        }
        break;

      case 'sdpInvite':
        await handleSdpInvite(message.data.sdpOffer);
        break;

      case 'text':
        if (message.content.source === 'assistant') {
          console.log('Agent:', message.content.text);
        }
        break;

      case 'conversationResult':
        console.log('Call ended:', message.result);
        break;

      case 'error':
        console.error('Error:', message.data?.message);
        break;
    }
  };

  async function handleSdpInvite(sdpOffer) {
    // Create peer connection with STUN servers for NAT traversal
    peerConnection = new RTCPeerConnection({
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' }
      ]
    });

    // Set remote description (server's offer)
    await peerConnection.setRemoteDescription({
      type: 'offer',
      sdp: sdpOffer
    });

    // Get microphone access
    localStream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
      },
      video: false
    });

    // Add audio track to peer connection
    localStream.getTracks().forEach(track => {
      peerConnection.addTrack(track, localStream);
    });

    // Create and set local description (answer)
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);

    // Send SDP answer to server
    ws.send(JSON.stringify({
      type: 'sdpAnswer',
      timestamp: new Date().toISOString(),
      data: {
        sdpAnswer: answer.sdp
      }
    }));
  }
  ```
</Accordion>

### SdpAnswerMessage schema

| Field            | Type                | Required | Description             |
| ---------------- | ------------------- | -------- | ----------------------- |
| `type`           | `"sdpAnswer"`       | Yes      | Message type identifier |
| `timestamp`      | `string` (ISO 8601) | Yes      | Message timestamp       |
| `data.sdpAnswer` | `string`            | Yes      | WebRTC SDP answer       |

***

## Step 4: Handle connection state changes

Monitor WebRTC connection state and handle audio:

<Accordion title="Example: WebRTC connection state handlers">
  ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  function setupPeerConnectionHandlers() {
    // Handle connection state changes
    peerConnection.onconnectionstatechange = () => {
      console.log('Connection state:', peerConnection.connectionState);

      switch (peerConnection.connectionState) {
        case 'connected':
          console.log('Voice call connected');
          updateUI('connected');
          break;
        case 'disconnected':
          console.log('Voice call disconnected');
          updateUI('disconnected');
          break;
        case 'failed':
          console.error('Voice call failed');
          handleConnectionFailure();
          break;
      }
    };

    // Handle ICE connection state
    peerConnection.oniceconnectionstatechange = () => {
      console.log('ICE state:', peerConnection.iceConnectionState);

      if (peerConnection.iceConnectionState === 'failed') {
        // Attempt ICE restart
        peerConnection.restartIce();
      }
    };

    // Handle incoming audio from agent
    peerConnection.ontrack = (event) => {
      const audioElement = document.getElementById('agent-audio');
      if (audioElement) {
        audioElement.srcObject = event.streams[0];
      }
    };

    // Handle ICE candidates
    peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        // ICE candidates are included in the SDP, no action needed
        console.log('ICE candidate gathered');
      }
    };
  }
  ```
</Accordion>

***

## Step 5: End the call

Clean up resources when ending the call:

<Accordion title="Example: End call and cleanup resources">
  ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  function endCall() {
    // Send terminate to server
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({
        type: 'terminate',
        timestamp: new Date().toISOString()
      }));
    }

    // Stop local audio tracks
    if (localStream) {
      localStream.getTracks().forEach(track => track.stop());
      localStream = null;
    }

    // Close peer connection
    if (peerConnection) {
      peerConnection.close();
      peerConnection = null;
    }
  }

  // Handle page unload
  window.addEventListener('beforeunload', endCall);
  ```
</Accordion>

***

## Next steps

<CardGroup cols={2}>
  <Card title="Message Reference" icon="book" href="/docs/websockets/message-reference">
    Complete schema documentation for all WebSocket messages
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/docs/websockets/error-handling">
    Handle connection errors and edge cases
  </Card>

  <Card title="Chat Implementation" icon="comments" href="/docs/websockets/chat-implementation">
    Build text chat interfaces
  </Card>

  <Card title="Tool Execution" icon="wrench" href="/docs/websockets/tool-execution">
    Handle agent tool calls via WebSocket
  </Card>
</CardGroup>
