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

# Agent Media WebSocket

> One permanent WebSocket URL per agent. A client with a shared secret dials it for every call

Use this when your client can dial only one WebSocket URL and give it a shared secret. Examples are a media application or a stream-to-WebSocket setting in a telephony platform, or your own app. Each connection places one call with the agent. You do not start a call first, and you do not get a one-time URL.

The client dials the same URL for every call. The agent answers in its voice, uses its tools and knowledge base, and can press keys, transfer to a person or hang up, as in an [Agent Stream WebSocket](https://docs.60db.ai/websocket-api/agent-stream.md) call. If your code can start calls, the two-step flow is still available. See [Start a call from your code](#start-a-call-from-your-code).

## Quick start

1. Get the agent's `media_websocket_url` from [Get agent](https://docs.60db.ai/api-reference/agents/get-agent.md). It looks like `wss://api.60db.ai/ws/agents/{agentId}/media`.
2. Get the agent's secret, the `whsec_…` value from [Create agent](https://docs.60db.ai/api-reference/agents/create-agent.md) (`webhook_secret`) or from [Rotate webhook secret](https://docs.60db.ai/api-reference/agents/rotate-webhook-signing.md).
3. Connect with one of the [authentication](#authentication) options, then stream audio in the [audio format](#audio) you chose.

## The URL

`wss://api.60db.ai/ws/agents/{agentId}/media`

`agentId` is the agent's `id`. The URL takes two optional query parameters:

| Query | Values | Default | Meaning |
| - | - | - | - |
| `format` | `pcm16` or `mulaw` | `pcm16` | The audio format of your raw audio, and of the start message that is made for it. See [Audio](#audio) |
| `sample_rate` | `8000` | `8000` | The only rate supported. Any other value is refused with `400` |

## The shared secret

The secret is the agent's signing secret. It starts with `whsec_`, and it is shown only in the Create agent response and in the Rotate webhook secret response. [Get agent](https://docs.60db.ai/api-reference/agents/get-agent.md) does not return it.

* Rotating the secret refuses the old one at once.
* The same secret signs the requests that 60db sends to your server for this agent, so rotating it changes both at once. Update both places.

Each call runs with the permissions of the agent's creator. If that person is no longer an active member of the workspace, or is no longer allowed to place calls, the connection is refused with `403`. Removing the creator from the workspace therefore stops the URL from placing calls.

## Authentication

Send one of these with the connection request. Any one of them is enough.

| Option | How to send it | Notes |
| - | - | - |
| Bearer header (recommended) | `Authorization: Bearer <secret>` | The secret is in a header, not in the URL |
| Signed headers | `X-60db-Timestamp: <unix seconds>` and `X-60db-Signature: v1=<hex>` | The signature is the hex HMAC-SHA256 of `{timestamp}.{agentId}`, keyed with the secret. The timestamp must be within 300 seconds of the server clock. The signed text is the agent `id` only, so the `format` query is not signed |
| Query secret | `?secret=<secret>` in the URL | Use only if your platform cannot send headers. Proxies, load balancers and access logs often record the full URL, so the secret can leak there. The server removes it from the request, but the client already sent it in the URL |

Use the Bearer header when you can. If your platform signs requests, use the signed headers. Use the query option only as a last resort.

### Node.js: Bearer

This needs the `ws` package (`npm install ws`). The global `WebSocket` cannot set headers.

```javascript theme={null}
import WebSocket from 'ws';

const url = process.env.AGENT_MEDIA_URL;   // media_websocket_url, plus ?format=mulaw for μ-law
const secret = process.env.AGENT_SECRET;   // whsec_... from create or rotate

const ws = new WebSocket(url, {
  headers: { Authorization: `Bearer ${secret}` },
});

ws.on('unexpected-response', (req, res) => {
  console.error('Refused with HTTP status', res.statusCode);
});
ws.on('message', (data, isBinary) => {
  if (isBinary) return playAudio(data);          // the agent's audio, in the format you chose
  const m = JSON.parse(data.toString('utf8'));
  if (m.event === 'ended') console.log(m.summary);
});
```

### Node.js: signed headers

Compute the signature just before you connect, because the timestamp must be within 300 seconds.

```javascript theme={null}
import crypto from 'node:crypto';
import WebSocket from 'ws';

const agentId = process.env.AGENT_ID;
const secret = process.env.AGENT_SECRET;   // whsec_... from create or rotate

const timestamp = String(Math.floor(Date.now() / 1000));
const signature = 'v1=' + crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${agentId}`)
  .digest('hex');

const ws = new WebSocket(`wss://api.60db.ai/ws/agents/${agentId}/media`, {
  headers: { 'X-60db-Timestamp': timestamp, 'X-60db-Signature': signature },
});
```

## Audio

| `format` | Encoding | Bytes per 20 ms frame |
| - | - | - |
| `pcm16` (default) | Signed 16-bit little endian, 8 kHz, mono | 320 |
| `mulaw` | G.711 μ-law, 8 kHz, mono | 160 |

Send caller audio as binary messages in real time. The agent's audio comes back as binary messages in the same format.

### Raw audio without a start message

A client may send binary audio first and no start message. When the first frame is binary, 60db first creates the native start message `{"event":"start","format":"<format from the query>"}` and then passes the frame on. So the `format` query must match the audio you send.

If the first frame is text, it is passed on unchanged. A native-style start message from the [Agent Stream WebSocket](https://docs.60db.ai/websocket-api/agent-stream.md) therefore also works here.

## Refusals

A refused connection gets a plain HTTP response instead of a WebSocket. A WebSocket client reports it as an unexpected server response with that status. The body is JSON: `{"success": false, "message": "…"}`. A `401` also sends `WWW-Authenticate: Bearer`. The message never contains the secret.

The checks run in this order. The first failing check decides the status.

| Status | When |
| - | - |
| `429` | More than 60 connections per minute from one address, by default. Retry shortly |
| `400` | `format` is not `pcm16` or `mulaw`, `sample_rate` is not `8000`, or the request is not a WebSocket upgrade |
| `404` | The path is not `/ws/agents/{agentId}/media`, or no agent has that `id` |
| `401` | None of the three authentication options passes |
| `429` | More than 30 connections per minute to this agent, by default. Retry shortly |
| `403` | The agent's creator is no longer a member of the workspace, or the creator may not create calls |
| `503` | The authorization service is unavailable. Retry later |
| `400` | The agent has tools but no `tool_webhook_url`. Set one on the agent |
| `402` | Not enough AI credits to place a call |
| `429` | The workspace already has the maximum number of stream calls running |
| `500` | The socket could not be opened. Retry later |

## Messages and limits

After the connection opens, the messages are the same as on the [Agent Stream WebSocket](https://docs.60db.ai/websocket-api/agent-stream.md):

* From the agent: `started`, `clear`, `dtmf`, `transfer`, `hangup`, `ended` and, when your `start` message carried a stream id, the platform's end-call `command` (see [End-call command](https://docs.60db.ai/websocket-api/agent-stream.md#end-call-command)).
* From your client: `stop`, `dtmf` and `transfer_result`.

Limits:

* A message over 4 KB closes the connection with `1009`.
* The server pings every 15 seconds. A connection silent for 45 seconds is closed with `1001`.
* A call lasts at most 1,020 seconds. The socket then closes with `1000`.
* If more than 64 KB arrives before the call service is ready, the socket closes with `1008`.
* If the call service fails, the socket closes with `1011`.

Each accepted connection creates one call with the source `websocket`. It is billed like a stream call. You can end a running call from your server with [End stream call](https://docs.60db.ai/api-reference/agents/end-stream-call.md). The call keeps the sentiment and judge scores from the runtime's `ended` message. If the client leaves before the connection is accepted, the placed call is closed without connecting.

## Settings for a media application form

Use these values in the form of a "media application" or "stream to WebSocket" setting:

| Form field | Value |
| - | - |
| Name | Any label, for example the agent's name |
| WebSocket URL | The agent's `media_websocket_url`. For μ-law, add `?format=mulaw` |
| Shared secret | The agent's `whsec_…` secret |
| Direction | Bidirectional |
| Audio format | 16-bit PCM, 8 kHz (`pcm16`, the default), or μ-law, 8 kHz (`mulaw`). It must match the `format` in the URL |

Check how your platform sends the shared secret. If it sends an `Authorization: Bearer` header, the Bearer option applies. If it signs requests with a timestamp, the signed headers option applies. If it only puts the secret in the URL, the query option applies, with the risk described in [Authentication](#authentication).

## Start a call from your code

To start a call from your server and get a one-time URL instead, use [Start stream call](https://docs.60db.ai/api-reference/agents/start-stream-call.md) and follow the two steps in the [Agent Stream WebSocket](https://docs.60db.ai/websocket-api/agent-stream.md) guide. That flow needs an API key on your server, and the URL it returns is valid for 55 seconds.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.