> This is a page from the ElevenLabs documentation. For a complete page index, fetch https://elevenlabs.io/docs/llms.txt. For the full documentation in a single file, fetch https://elevenlabs.io/docs/llms-full.txt.

# Integração com LLM personalizado

## Visão geral

A [integração nativa com a Twilio](/docs/pt/eleven-agents/phone-numbers/twilio-integration/native-integration) do ElevenAgents abrange o caso em que a ElevenLabs hospeda o LLM. Use este guia quando precisar de controle total sobre o cérebro do LLM no seu próprio servidor — seu próprio modelo, pipeline de RAG, roteamento de chamadas de função ou outro raciocínio no servidor — e o agente ainda estiver em um número de telefone da Twilio.

A parte do LLM personalizado é fornecida pelo [SDK Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine), que abre um WebSocket entre a ElevenLabs e seu servidor para que seu LLM possa transmitir respostas conforme a chamada acontece. A parte da Twilio usa o [Media Streams](https://www.twilio.com/docs/voice/media-streams) para encaminhar o áudio da chamada para o agente.

## Arquitetura

O SDK Speech Engine expõe dois endpoints WebSocket no sistema de conversação do agente:

* O **WebSocket do cérebro** é executado no seu servidor. A ElevenLabs se conecta a ele para enviar transcrições e receber texto gerado pelo LLM.
* O **WebSocket de conversação** é executado na ElevenLabs. Os clientes se conectam a ele para enviar áudio e receber áudio sintetizado de volta. A ponte da Twilio se conecta por uma URL assinada e encaminha áudio μ-law em ambas as direções.

Como o Twilio Media Streams e o Speech Engine usam `ulaw_8000`, a ponte encaminha áudio codificado em base64 sem transcodificação.

```mermaid
sequenceDiagram
    participant Caller
    participant Twilio
    participant Bridge as Bridge Server
    participant EL as ElevenLabs (conversation WS)
    participant Brain as Brain Server

    Caller->>Twilio: Dial number
    Twilio->>Bridge: POST /incoming-call
    Bridge-->>Twilio: TwiML <Connect><Stream>
    Twilio->>Bridge: WebSocket /media-stream
    Bridge->>EL: Open conversation WebSocket (signed URL)

    loop Conversation
        Caller->>Twilio: Speak
        Twilio->>Bridge: media event (μ-law base64)
        Bridge->>EL: user_audio_chunk
        EL->>Brain: user_transcript
        Brain-->>EL: agent_response (streamed)
        EL->>Bridge: audio event (μ-law base64)
        Bridge->>Twilio: media event
        Twilio->>Caller: Play audio
    end
```

A ponte e o servidor do cérebro podem ser executados no mesmo processo, se for conveniente — o exemplo abaixo os combina.

## Quando usar este padrão

Tanto este guia quanto a [integração nativa com a Twilio](/docs/pt/eleven-agents/phone-numbers/twilio-integration/native-integration) colocam um agente em um número de telefone da Twilio. A diferença é quem hospeda o LLM:

* **Integração nativa**: a ElevenLabs hospeda o LLM, e você o configura pelo agente. Mais simples.
* **LLM personalizado via SDK Speech Engine** (este guia): você hospeda o LLM no seu próprio servidor. Controle total sobre o modelo, RAG, chamadas de função e lógica de negócios. Mais componentes envolvidos.

Se a lógica do seu LLM couber na configuração padrão do agente, prefira a integração nativa. Use este guia quando seu cérebro precisar executar código na sua própria infraestrutura.

Este padrão usa o SDK Speech Engine, que usa uma conexão WebSocket para se comunicar entre seu servidor e a API da ElevenLabs. Você também pode usar o guia de [LLM personalizado](/docs/pt/eleven-agents/customization/llm/custom-llm), que usa um endpoint HTTP compatível com OpenAI em vez do SDK Speech Engine.

A principal diferença entre os dois são WebSockets em vez de solicitações HTTP. Usar WebSockets significa manter uma única conexão, em vez de estabelecer uma nova conexão HTTP a cada turno, o que pode melhorar a latência.

## Pré-requisitos

* Uma conta [Twilio](https://www.twilio.com/) e um número de telefone com suporte a voz.
* Um recurso do Speech Engine. Siga o [início rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine) para criar um e conhecer o padrão de servidor do cérebro.
* Um túnel HTTPS público (por exemplo, [ngrok](https://ngrok.com)). A Twilio disca para sua ponte pela internet pública.
* Python 3.9+ ou Node.js 18+.

## Configure o agente para áudio μ-law

O Twilio Media Streams usa áudio μ-law de 8 kHz. Configure o Speech Engine para aceitar e emitir o mesmo formato, para que a ponte não precise transcodificar.

**`configure_engine.py`**

```python title="configure_engine.py"
import asyncio
import os
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])


async def update_engine():
    await elevenlabs.speech_engine.update(
        speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
        asr={"user_input_audio_format": "ulaw_8000"},
        tts={
            "model_id": "eleven_flash_v2",
            "agent_output_audio_format": "ulaw_8000",
        },
        speech_engine={
            "request_headers": {"x-api-key": os.environ["SHARED_SECRET"]},
        },
    )


asyncio.run(update_engine())
```

**`configure-engine.mts`**

```typescript title="configure-engine.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  asr: { userInputAudioFormat: "ulaw_8000" },
  tts: {
    modelId: "eleven_flash_v2",
    agentOutputAudioFormat: "ulaw_8000",
  },
  speechEngine: {
    requestHeaders: { "x-api-key": process.env.SHARED_SECRET! },
  },
});
```

`eleven_flash_v2` mantém baixa a latência de conversão de texto em voz, algo importante em uma chamada telefônica. O bloco `request_headers` instrui a ElevenLabs a incluir `x-api-key: <shared-secret>` em toda conexão WebSocket do cérebro — o servidor do cérebro verifica o cabeçalho para garantir que somente seu Speech Engine possa acessá-lo.

## Crie o servidor de ponte

A ponte disponibiliza três rotas:

* `POST /incoming-call` — webhook da Twilio. Retorna TwiML instruindo a Twilio a abrir um Media Stream para `/media-stream`.
* `GET /media-stream` — WebSocket do Twilio Media Streams. Encaminha áudio de e para o WebSocket de conversação do Speech Engine.
* `GET /ws` — WebSocket do cérebro. A ElevenLabs se conecta aqui quando uma conversa começa. Executa o servidor padrão `engine.serve()` / `engine.attach()`.

#### Instale as dependências

**`Python`**

```bash title="Python"
pip install "elevenlabs" "aiohttp" "twilio" "python-dotenv"
```

**`Node`**

```bash title="Node"
npm install @elevenlabs/elevenlabs-js express ws twilio dotenv openai
```

#### Gere uma URL assinada para o Speech Engine

A ponte solicita uma URL assinada sempre que uma nova chamada chega. A URL incorpora o ID do Speech Engine e uma assinatura de uso único, para que a ponte nunca precise da chave de API bruta.

**`bridge.py`**

```python title="bridge.py"
from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])

async def signed_url() -> str:
    response = await elevenlabs.conversational_ai.conversations.get_signed_url(
        agent_id=os.environ["SPEECH_ENGINE_ID"],
    )
    return response.signed_url
```

**`bridge.mts`**

```typescript title="bridge.mts"
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";

const elevenlabs = new ElevenLabsClient({
  apiKey: process.env.ELEVENLABS_API_KEY,
});

async function signedUrl(): Promise<string> {
  const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
    agentId: process.env.SPEECH_ENGINE_ID!,
  });
  return response.signedUrl;
}
```

#### Disponibilize a resposta TwiML

Quando uma chamada chega, a Twilio envia um POST para `/incoming-call`. A resposta é um TwiML que abre um Media Stream para o próprio WebSocket `/media-stream` da ponte.

**`bridge.py`**

```python title="bridge.py"
from aiohttp import web
from twilio.request_validator import RequestValidator

validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])


async def incoming_call(request: web.Request) -> web.Response:
    form = await request.post()
    signature = request.headers.get("X-Twilio-Signature", "")
    url = str(request.url)
    if not validator.validate(url, dict(form), signature):
        return web.Response(status=403, text="forbidden")

    host = request.headers.get("X-Forwarded-Host") or request.host
    twiml = (
        '<?xml version="1.0" encoding="UTF-8"?>'
        "<Response><Connect>"
        f'<Stream url="wss://{host}/media-stream"/>'
        "</Connect></Response>"
    )
    return web.Response(text=twiml, content_type="text/xml")
```

**`bridge.mts`**

```typescript title="bridge.mts"
import express from "express";
import twilio from "twilio";

const app = express();
app.use(express.urlencoded({ extended: false }));

app.post(
  "/incoming-call",
  twilio.webhook({ validate: true }),
  (req, res) => {
    const host = req.headers["x-forwarded-host"] ?? req.get("host");
    const twiml = `<?xml version="1.0" encoding="UTF-8"?>
      <Response>
        <Connect>
          <Stream url="wss://${host}/media-stream"/>
        </Connect>
      </Response>`;
    res.type("text/xml").send(twiml);
  },
);
```

`RequestValidator` (Python) e `twilio.webhook({ validate: true })` (Node) verificam o cabeçalho `X-Twilio-Signature` em relação a `TWILIO_AUTH_TOKEN`. Sem validação, qualquer pessoa na internet pública poderia enviar um POST para `/incoming-call` e cobrar chamadas da sua conta.

#### Faça a ponte do Media Stream

O Media Stream é um WebSocket que envia uma sequência de eventos JSON: `connected`, `start`, `media` (a carga de áudio) e `stop`. A ponte abre um WebSocket de conversação do Speech Engine em `start` e encaminha o áudio em ambas as direções até o stream ser fechado.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import asyncio
import json

import aiohttp
from aiohttp import web


async def media_stream(request: web.Request) -> web.WebSocketResponse:
    twilio_ws = web.WebSocketResponse()
    await twilio_ws.prepare(request)

    stream_sid: str | None = None
    el_session: aiohttp.ClientSession | None = None
    el_ws: aiohttp.ClientWebSocketResponse | None = None
    pump_task: asyncio.Task | None = None

    async def pump_el_to_twilio(el: aiohttp.ClientWebSocketResponse):
        async for msg in el:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)
            etype = event.get("type")
            if etype == "audio":
                await twilio_ws.send_str(json.dumps({
                    "event": "media",
                    "streamSid": stream_sid,
                    "media": {"payload": event["audio_event"]["audio_base_64"]},
                }))
            elif etype == "interruption":
                await twilio_ws.send_str(json.dumps({
                    "event": "clear",
                    "streamSid": stream_sid,
                }))
            elif etype == "ping":
                event_id = event.get("ping_event", {}).get("event_id")
                await el.send_str(json.dumps({
                    "type": "pong", "event_id": event_id,
                }))

    try:
        async for msg in twilio_ws:
            if msg.type != aiohttp.WSMsgType.TEXT:
                continue
            event = json.loads(msg.data)

            if event["event"] == "start":
                stream_sid = event["start"]["streamSid"]
                el_session = aiohttp.ClientSession()
                el_ws = await el_session.ws_connect(await signed_url())
                await el_ws.send_str(json.dumps({
                    "type": "conversation_initiation_client_data",
                }))
                pump_task = asyncio.create_task(pump_el_to_twilio(el_ws))

            elif event["event"] == "media" and el_ws is not None:
                await el_ws.send_str(json.dumps({
                    "user_audio_chunk": event["media"]["payload"],
                }))

            elif event["event"] == "stop":
                break
    finally:
        if pump_task:
            pump_task.cancel()
        if el_ws and not el_ws.closed:
            await el_ws.close()
        if el_session and not el_session.closed:
            await el_session.close()

    return twilio_ws
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
import { WebSocket, WebSocketServer } from "ws";
import { createServer } from "node:http";

const httpServer = createServer(app);
const wss = new WebSocketServer({ noServer: true });

httpServer.on("upgrade", (req, socket, head) => {
  if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

async function handleMediaStream(twilioWs: WebSocket) {
  let streamSid: string | null = null;
  let elReady: Promise<WebSocket | null> | null = null;

  twilioWs.on("message", async (raw) => {
    const event = JSON.parse(raw.toString());

    if (event.event === "start") {
      streamSid = event.start.streamSid;
      // Convert rejection into a clean null + close so a failed signed-URL
      // fetch doesn't become an unhandled rejection on the next media event.
      elReady = openElevenLabsWebSocket(twilioWs, () => streamSid).catch((err) => {
        console.error("Failed to open Speech Engine conversation:", err);
        twilioWs.close();
        return null;
      });
    } else if (event.event === "media" && elReady) {
      const elWs = await elReady;
      if (!elWs) return;
      elWs.send(JSON.stringify({
        user_audio_chunk: event.media.payload,
      }));
    } else if (event.event === "stop") {
      twilioWs.close();
    }
  });

  twilioWs.on("close", async () => {
    (await elReady)?.close();
  });
}

async function openElevenLabsWebSocket(
  twilioWs: WebSocket,
  getStreamSid: () => string | null,
): Promise<WebSocket> {
  const elWs = new WebSocket(await signedUrl());
  await new Promise<void>((resolve, reject) => {
    elWs.once("open", () => resolve());
    elWs.once("error", reject);
  });
  elWs.send(JSON.stringify({
    type: "conversation_initiation_client_data",
  }));

  elWs.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    const streamSid = getStreamSid();
    if (event.type === "audio") {
      twilioWs.send(JSON.stringify({
        event: "media",
        streamSid,
        media: { payload: event.audio_event.audio_base_64 },
      }));
    } else if (event.type === "interruption") {
      twilioWs.send(JSON.stringify({ event: "clear", streamSid }));
    } else if (event.type === "ping") {
      elWs.send(JSON.stringify({
        type: "pong", event_id: event.ping_event?.event_id,
      }));
    }
  });

  return elWs;
}
```

O evento `interruption` do Speech Engine dispara um evento `clear` no stream da Twilio, que descarta qualquer áudio em buffer para que a interrupção funcione corretamente. O evento `ping` é respondido com `pong` para manter o WebSocket de conversação ativo.

#### Execute o servidor do cérebro junto

O servidor do cérebro é o servidor padrão do Speech Engine exibido no [início rápido](/docs/pt/eleven-api/guides/cookbooks/speech-engine). A única adição é a verificação do segredo compartilhado no upgrade do WebSocket — aceite a conexão apenas se `x-api-key` corresponder ao valor definido no Speech Engine.

**`bridge.py`**

```python title="bridge.py" maxLines=0
import os

from elevenlabs import AsyncElevenLabs

elevenlabs = AsyncElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
SHARED_SECRET = os.environ["SHARED_SECRET"]


async def brain_ws(request: web.Request) -> web.WebSocketResponse:
    if request.headers.get("x-api-key") != SHARED_SECRET:
        return web.Response(status=401, text="unauthorized")

    ws = web.WebSocketResponse()
    await ws.prepare(request)

    engine = await elevenlabs.speech_engine.get(os.environ["SPEECH_ENGINE_ID"])
    session = engine.create_session(ws)

    async def on_transcript(transcript):
        # Replace this with your own LLM call; see the quickstart.
        await session.send_response("Hello, you've reached the demo.")

    session.on("user_transcript", on_transcript)
    await session.run()
    return ws


def make_app() -> web.Application:
    app = web.Application()
    app.router.add_post("/incoming-call", incoming_call)
    app.router.add_get("/media-stream", media_stream)
    app.router.add_get("/ws", brain_ws)
    return app


if __name__ == "__main__":
    web.run_app(make_app(), port=3001)
```

**`bridge.mts`**

```typescript title="bridge.mts" maxLines=0
httpServer.on("upgrade", async (req, socket, head) => {
  if (req.url === "/ws") {
    if (req.headers["x-api-key"] !== process.env.SHARED_SECRET) {
      socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
      socket.destroy();
      return;
    }
    // Hand off to engine.attach() — see the Speech Engine quickstart.
  } else if (req.url === "/media-stream") {
    wss.handleUpgrade(req, socket, head, (ws) => handleMediaStream(ws));
  } else {
    socket.destroy();
  }
});

httpServer.listen(3001);
```

Consulte o [início rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine#server-setup) para ver a implementação completa de `on_transcript`, incluindo uma chamada ao LLM e resposta transmitida.

## Direcione a Twilio para a ponte

#### Inicie a ponte e um túnel público

```bash
ngrok http 3001
python bridge.py
```

Anote a URL `https://` exibida pelo ngrok — a Twilio enviará POSTs para ela.

#### Atualize o ws_url do Speech Engine

Defina `speech_engine.ws_url` como a URL pública do WebSocket do endpoint do seu cérebro para que a ElevenLabs saiba onde se conectar.

```python
await elevenlabs.speech_engine.update(
    speech_engine_id="seng_8k3m9xr4hjnfg983brhmhkd98n6",
    speech_engine={"ws_url": "wss://abc123.ngrok.io/ws"},
)
```

```typescript
await elevenlabs.speechEngine.update("seng_8k3m9xr4hjnfg983brhmhkd98n6", {
  speechEngine: { wsUrl: "wss://abc123.ngrok.io/ws" },
});
```

#### Configure o número da Twilio

No console da Twilio, abra a **Configuração de voz** do seu número de telefone:

* **Uma chamada é recebida**: Webhook
* **URL**: `https://abc123.ngrok.io/incoming-call`
* **Método HTTP**: POST

Se o número estiver vinculado a um Elastic SIP Trunk, desvincule-o primeiro — um número da Twilio é direcionado para um tronco ou para um webhook, não para ambos.

#### Ligue para o número

Ligue para o número de qualquer telefone. O agente atende; fale durante a chamada e você deverá ouvir a resposta do agente. Com o registro de depuração ativado, a ponte registra o SID da chamada, o ID da conversa e o formato de áudio a cada turno.

## Considerações para produção

* **Validação de webhook**: sempre valide o `X-Twilio-Signature` em `/incoming-call`. O exemplo acima usa a biblioteca auxiliar da Twilio; não pule esta etapa.
* **Segredo compartilhado**: exija o segredo compartilhado no WebSocket do cérebro. Sem ele, qualquer pessoa que adivinhar sua URL do ngrok poderá se conectar e se passar pela ElevenLabs.
* **Host estável**: as URLs do plano gratuito do ngrok mudam a cada reinicialização. Use um domínio ngrok reservado ou um hostname real para não precisar atualizar o `ws_url` do Speech Engine e o webhook da Twilio após cada reinicialização.
* **Latência**: cada chamada adiciona dois saltos de rede além do tempo até o primeiro token do LLM. Use um modelo de baixa latência e transmita as respostas por streaming para manter baixa a latência percebida.
* **Um processo ou dois**: o exemplo coloca a ponte e o cérebro na mesma porta, para que um único túnel do ngrok cubra tudo. Em produção, você pode separá-los em dois serviços, desde que cada um tenha uma URL pública.
* **Injeção de prompt**: a entrada falada de uma chamada telefônica é uma entrada de usuário não confiável. Valide as transcrições antes que influenciem chamadas de ferramentas ou gravações no banco de dados.

## Próximas etapas

#### [Integração nativa com Twilio](/docs/pt/eleven-agents/phone-numbers/twilio-integration/native-integration)

Use o LLM hospedado em vez de um personalizado.

#### [Guia rápido do Speech Engine](/docs/pt/eleven-api/guides/cookbooks/speech-engine)

Crie o servidor do cérebro completo com um LLM de streaming.

#### [LLM personalizado (compatível com OpenAI)](/docs/pt/eleven-agents/customization/llm/custom-llm)

Um mecanismo alternativo de LLM personalizado que usa um endpoint HTTP compatível com OpenAI.

#### [Referência do SDK Python](/docs/pt/eleven-api/resources/libraries/speech-engine/python-sdk-reference)

Classes, métodos e eventos do SDK Python do Speech Engine.

#### [Referência do SDK JavaScript](/docs/pt/eleven-api/resources/libraries/speech-engine/javascript-sdk-reference)

Classes, métodos e eventos do SDK JavaScript do Speech Engine.