# Open a call (WebRTC)

From the Lero API docs: https://lero.studio/docs/en#conversa. Everything in one file: https://lero.studio/docs/en/llms-full.txt.

`POST /v1/agents/conversation`

Live call signaling: you send the SDP offer of your WebRTC client and get the SDP answer. After that, audio and the `chat` data channel flow peer-to-peer between your client and the agent (streaming STT, LLM and TTS, with barge-in and turn detection). Without `versao`, `llm` and `voz`, the recommended setup applies. With `instrucoes`, the agent becomes yours: the system prompt replaces the Lero persona. Step by step in [Conversational Agent: step by step](#agente-guia).

Authentication: key.

Request body (JSON):

- `type` (string, required): Always `offer`.
- `sdp` (string, required): The full SDP offer, with complete ICE (non-trickle), up to 20,000 characters.
- `versao` (string, optional): The agent version, an id from `GET /v1/agents`. Recommended: `v0.4`.
- `llm` (string, optional): The LLM, an id from `GET /v1/agents`. Recommended: `oss120b`.
- `voz` (string, optional): `rio`, `sp` or `niteroi`. Ignored on single-voice versions (`vozes: false`).
- `instrucoes` (string, optional): System prompt, up to 30,000 characters. Replaces the Lero persona; voice rules (no markdown, code or URLs in speech) are appended.
- `conhecimento` (string | array, optional): Knowledge base (RAG): text or `[{titulo, texto}]`, up to 50 items and 60,000 characters. The agent consults it and never reads it aloud.
- `abertura` (string, optional): First message: how the agent opens the call. An instruction, not the literal sentence.
- `palavras` (string[], optional): STT keywords: up to 40 names and terms to recognize.
- `silencio_s` (false | number[], optional): Silence prompts. Off with `instrucoes`; `[20, 40, 60]` turns them on (2 nudges, then goodbye).
- `pesquisa` (boolean, optional): Web search tool. Off with `instrucoes`.
- `max_tokens` (integer, optional): Max length of each reply, 60 to 1000 (default 260).

Responses:

- `200`: The SDP answer: apply it with `setRemoteDescription`.
- `400`: `voz_invalida`
- `400`: `pedido_invalido`
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `400`: `oferta_invalida`
- `400`: `versao_invalida`
- `400`: `llm_invalido`
- `400`: `roteiro_invalido`
- `502`: `sem_bot`
- `503`: `desligado`

cURL:

```bash
curl -X POST "https://app.lero.studio/api/v1/agents/conversation" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "offer",
    "sdp": "v=0\r\no=- ...",
    "versao": "v0.4",
    "llm": "oss120b",
    "voz": "rio",
    "instrucoes": "Você é a recepção da Clínica Exemplo. Marca consultas e tira dúvidas de preço.",
    "conhecimento": [{ "titulo": "Preços", "texto": "Consulta: R$ 250..." }]
  }'
```

JavaScript:

```javascript
// browser: mic and speaker. In production, send the offer to YOUR backend, which calls Lero with the key.
const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
const chat = pc.createDataChannel("chat");                 // 1. data channel BEFORE the offer
chat.onmessage = (e) => console.log(JSON.parse(e.data));   // events: estado, fala, latencia

const mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } });
mic.getTracks().forEach((t) => pc.addTrack(t, mic));       // 2. audio up
const audio = new Audio(); audio.autoplay = true;
pc.ontrack = (e) => { audio.srcObject = e.streams[0]; };   //    agent voice down

await pc.setLocalDescription(await pc.createOffer());      // 3. offer with complete ICE
await new Promise((ok) => {
  if (pc.iceGatheringState === "complete") return ok();
  pc.onicegatheringstatechange = () => pc.iceGatheringState === "complete" && ok();
  setTimeout(ok, 2500);
});

const res = await fetch("https://app.lero.studio/api/v1/agents/conversation", {      // 4. signaling: offer → answer
  method: "POST",
  headers: { "Authorization": `Bearer ${LERO_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ type: "offer", sdp: pc.localDescription.sdp, versao: "v0.4", llm: "oss120b", voz: "rio" }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.erro}`);
await pc.setRemoteDescription(data);                       // done: the call starts

// hang up: pc.close(); mic.getTracks().forEach((t) => t.stop());
```

Python:

```python
# headless (QA bots, tests, AI agents): pip install aiortc requests
import asyncio, json, os

import requests
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer, MediaRecorder


async def main():
    pc = RTCPeerConnection()
    chat = pc.createDataChannel("chat")
    chat.on("message", lambda m: print(json.loads(m)))       # transcript and state
    pc.addTrack(MediaPlayer("pergunta.wav").audio)           # what the "person" says
    rec = MediaRecorder("resposta.wav")                      # what the agent says
    pc.on("track", lambda t: rec.addTrack(t))
    await pc.setLocalDescription(await pc.createOffer())     # returns with complete ICE
    r = requests.post(
        "https://app.lero.studio/api/v1/agents/conversation",
        headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
        json={"type": "offer", "sdp": pc.localDescription.sdp, "versao": "v0.4", "llm": "oss120b"},
        timeout=20,
    )
    r.raise_for_status()
    await pc.setRemoteDescription(RTCSessionDescription(**r.json()))
    await rec.start()
    await asyncio.sleep(30)
    await rec.stop()
    await pc.close()


asyncio.run(main())
```

Response (200):

```json
{
  "type": "answer",
  "sdp": "v=0\r\no=- ..."
}
```
