# API do Lero

Voz com sotaque brasileiro no seu produto: Text-to-Speech (TTS) em MP3, Speech-to-Text (STT) do português falado, com o tempo de cada frase e legenda pronta, e um Conversational Agent ao vivo por WebRTC. Voice Clone chega em breve. A API está nos planos Creator e Dev: o TTS gasta 1 crédito por caractere e o STT, 150 créditos por minuto de áudio.

- MCP: a voz do Lero dentro do Claude, do Codex e do Cursor, com um comando. Veja https://lero.studio/docs/ai#mcp
- Build with AI: cole um prompt no seu agente de código e ele integra a API no seu produto. Veja https://lero.studio/docs/ai#prompt

## Integração direta

Use o Lero no Claude, no Codex ou no Cursor sem escrever código. Crie uma chave em [app.lero.studio/desenvolvedores](https://app.lero.studio/desenvolvedores?aba=chaves) e rode o comando do seu agente, com a sua chave no lugar de `lero_sua_chave`:

Claude Code:

```bash
claude mcp add --env LERO_API_KEY=lero_sua_chave lero -- npx -y lero-mcp
```

Codex:

```bash
codex mcp add lero --env LERO_API_KEY=lero_sua_chave -- npx -y lero-mcp
```

Cursor:

```json
{
  "mcpServers": {
    "lero": {
      "command": "npx",
      "args": [
        "-y",
        "lero-mcp"
      ],
      "env": {
        "LERO_API_KEY": "lero_sua_chave"
      }
    }
  }
}
```

Claude Desktop:

```json
{
  "mcpServers": {
    "lero": {
      "command": "npx",
      "args": [
        "-y",
        "lero-mcp"
      ],
      "env": {
        "LERO_API_KEY": "lero_sua_chave"
      }
    }
  }
}
```

No Cursor, o JSON vai em `~/.cursor/mcp.json`. No Claude Desktop, em Settings > Developer > Edit Config. Precisa do Node 18 ou mais novo.

Pronto: peça um áudio na conversa, por exemplo *gera um áudio na voz do Rio dizendo "bom dia, tudo certo?"*, e o agente salva o MP3 em `~/Downloads/Lero`. Mais sobre o MCP em [Build with AI](https://lero.studio/docs/ai#mcp).

Para colocar o Lero no código do seu produto, cole o [prompt de integração](https://lero.studio/docs/ai#prompt) no seu agente de código, ou siga o início rápido abaixo.

## Início rápido

1. Crie uma chave em [app.lero.studio/desenvolvedores](https://app.lero.studio/desenvolvedores?aba=chaves). A API está nos planos Creator e Dev.
2. Guarde a chave no seu servidor, na variável `LERO_API_KEY`.
3. Peça a primeira fala:

cURL:

```bash
curl -X POST "https://app.lero.studio/api/v1/tts" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"texto": "Fala, tudo bem? Aqui é o Lero, direto do Rio.", "voz": "rio"}' \
  --output fala.mp3
```

Python:

```python
import os

import requests

r = requests.post(
    "https://app.lero.studio/api/v1/tts",
    headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
    json={"texto": "Fala, tudo bem? Aqui é o Lero, direto do Rio.", "voz": "rio"},
    timeout=300,
)
r.raise_for_status()
with open("fala.mp3", "wb") as f:
    f.write(r.content)
print("restam", r.headers["X-Creditos-Restantes"])
```

JavaScript:

```javascript
import { writeFile } from "node:fs/promises";

const res = await fetch("https://app.lero.studio/api/v1/tts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.LERO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ texto: "Fala, tudo bem? Aqui é o Lero, direto do Rio.", voz: "rio" }),
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).erro}`);
await writeFile("fala.mp3", Buffer.from(await res.arrayBuffer()));
```

Base URL de todas as rotas: `https://app.lero.studio/api/v1`. Pedidos e respostas em JSON (UTF-8), menos o áudio: a fala devolve MP3 e a [transcrição](https://lero.studio/docs#transcricao) recebe o arquivo em `multipart/form-data`.

As rotas têm nome em inglês: `/v1/tts`, `/v1/stt`, `/v1/voices`, `/v1/balance`, `/v1/agents` e `/v1/agents/conversation`. Os caminhos antigos, em português (`/v1/fala`, `/v1/transcricao`, `/v1/vozes`, `/v1/saldo`, `/v1/agentes` e `/v1/agentes/conversa`), seguem funcionando igual.

## Chave

Toda rota pede a chave no header `Authorization: Bearer <sua chave>` (ou no header `X-Api-Key`). A chave começa com `lero_` e aparece inteira uma vez só, na hora de criar. Depois, o app mostra só os 4 últimos caracteres.

- Use a chave só no servidor. Nunca no navegador, num app de celular ou num repositório: quem tiver a chave gasta os seus créditos.
- Cada chave pode ter um nome, um teto de créditos por mês e uma validade (1, 7, 30 ou 90 dias, ou nunca vencer).
- Vazou? Revogue a chave no app. Ela para na hora, e você cria outra.
- Até 10 chaves ativas por conta.

## Erros

Todo erro volta em JSON, com um código fixo no campo `erro` e uma mensagem em português. Trate pelo `erro`, nunca pelo texto da `mensagem`. Nos erros da fala e da transcrição também vêm `pedido` e, quando é um campo, `campo`.

```json
{"erro": "sem_credito", "mensagem": "Os créditos da conta acabaram.", "pedido": "3f9c1e0a7b2d4c6e8a1b3d5f7e9c0a2b"}
```

| status | erro | quando |
|---|---|---|
| 400 | `texto_invalido` | O texto está vazio, passou de 500 caracteres ou a voz não aceitou (emojis e símbolos). |
| 400 | `voz_invalida` | A voz não existe. Use `rio`, `sp` ou `niteroi`. |
| 400 | `audio_invalido` | Transcrição: o arquivo está vazio, não tem áudio que dê para ler ou o formato não é aceito. |
| 400 | `url_invalida` | Transcrição: a `url` não é https pública, não respondeu 200 ou não deu para baixar. |
| 400 | `formato_invalido` | Transcrição: o `formato` não é `json`, `texto`, `srt` nem `vtt`. |
| 400 | `idioma_invalido` | Transcrição: o `idioma` não é `pt-BR`. |
| 400 | `modelo_invalido` | O `model` não é `Lero v1`. |
| 400 | `pedido_invalido` | O corpo não é JSON. Na transcrição: o corpo não é multipart nem JSON, ou veio sem `audio` e sem `url`, ou com os dois. |
| 401 | `sem_chave` | O pedido veio sem chave. |
| 401 | `chave_invalida` | A chave está errada ou foi revogada. |
| 401 | `chave_vencida` | A chave passou da validade. |
| 402 | `sem_credito` | Os créditos da conta não cobrem o pedido. Na transcrição, o JSON traz `custo` (os créditos que o áudio precisa) e `saldo`. |
| 402 | `teto_da_chave` | A chave chegou ao teto de créditos do mês. |
| 403 | `plano` | A conta não está num plano com API (Creator ou Dev). |
| 403 | `escopo` | A chave não tem o escopo Fala (vale para a fala e a transcrição). |
| 403 | `bloqueado` | A conta está bloqueada. |
| 413 | `audio_grande` | Transcrição: o arquivo no corpo passou de 4 MB (mande por `url`) ou o da `url` passou de 500 MB. |
| 413 | `audio_longo` | Transcrição: o áudio passou de 2 h. |
| 429 | `muitos_pedidos` | Passou de 60 pedidos por minuto na conta. Espere o `Retry-After`. |
| 429 | `simultaneas` | O plano já está no limite de gerações simultâneas (Creator 5, Dev 10). Espere um terminar. |
| 429 | `ocupado` | A voz ou a transcrição está com fila. Espere o `Retry-After`. |
| 503 | `carregando` | A voz está subindo. Espere o `Retry-After`. |
| 503 | `motor_desligado` | A voz ou a transcrição está fora do ar. Espere o `Retry-After`. |
| 502 | `falhou` | A geração da fala ou a transcrição falhou. Tente de novo. |
| 504 | `demorou` | A geração da fala ou a transcrição passou do tempo. Tente de novo. |
| 500 | `interno` | Erro do nosso lado. Tente de novo; se continuar, mande o `X-Pedido-Id`. |
| 400 | `oferta_invalida` | Falta a SDP offer, ou ela não começa com `v=0`, ou passou de 20 mil caracteres. |
| 400 | `versao_invalida` | A `versao` não está na lista do `GET /v1/agents` para a sua chave. |
| 400 | `llm_invalido` | O `llm` não está na lista do `GET /v1/agents`. |
| 400 | `roteiro_invalido` | Um campo do custom agent passou do limite. O campo vem em `campo`. |
| 502 | `sem_bot` | O agente não respondeu à offer. Tente de novo. |
| 503 | `desligado` | Os agentes estão fora do ar agora. Espere o `Retry-After`. |

Quando a geração da fala falha depois da cobrança, o Lero devolve os créditos na hora: o campo `cobrado` vem `0` e a mensagem termina com "Nada foi cobrado.". A transcrição só cobra quando dá certo: erro na transcrição nunca gasta crédito.

## Créditos e limites

### Text-to-Speech (TTS)

- 1 crédito por caractere do texto, cobrado antes de gerar e devolvido se a geração falhar.
- Até 500 caracteres por pedido. Para um texto maior, divida em frases e junte os MP3.
- Cada pedido gera um áudio novo. Guarde o MP3 se for usar de novo.

### Speech-to-Text (STT)

- 150 créditos por minuto de áudio (2,5 por segundo), cobrado pelo segundo e arredondado para cima, só quando a transcrição dá certo. Conta o arquivo inteiro, silêncio incluído.
- Até 2 h de áudio por pedido. O arquivo no corpo vai até 4 MB; por `url`, até 500 MB.
- Cerca de 30 vezes o tempo real: 1 h de áudio sai em uns 2 min. Use timeout de 300 s.
- O Lero não guarda o áudio nem o texto da transcrição. Guarde a resposta se for usar de novo.

### A conta

- Até 60 pedidos por minuto por conta, somando todas as chaves e as rotas.
- Gerações simultâneas (fala e transcrição): 5 no Creator e 10 no Dev. Acima disso, `429 simultaneas`.
- A conversa ao vivo gasta 1.000 créditos por minuto de ligação, cobrados pelo segundo no fim; para ligar, a conta precisa de pelo menos 1 minuto de saldo.
- A franquia do plano entra no 1º uso de cada mês (fuso de Brasília). Os créditos do plano valem 30 dias e não passam para o mês seguinte.
- Créditos avulsos, só no Dev, na proporção do plano: valem 6 meses.

## Text-to-Speech (TTS)

Texto em fala, em MP3, com sotaque brasileiro.

### Gerar fala

`POST /v1/tts`

Transforma o texto em fala e devolve o MP3 (`audio/mpeg`). Custa 1 crédito por caractere do texto, cobrado antes de gerar e devolvido se a geração falhar.

Prefere JSON? Mande `Accept: application/json` (sem `*/*`) e o áudio vem em base64 no campo `audio`.

Autenticação: chave com o escopo Fala.

Corpo do pedido (JSON):

- `texto` (string, obrigatório): O que o Lero vai falar, de 1 a 500 caracteres. Os espaços das pontas não contam.
- `voz` (string, obrigatório): A voz: `rio` (Rio de Janeiro), `sp` (São Paulo) ou `niteroi` (Niterói).
- `model` (string, opcional): `Lero v1`, o único de fala por enquanto.

Headers da resposta:

- `X-Pedido-Id` (string): O id do pedido. Mande junto se precisar de suporte.
- `X-Creditos-Custo` (integer): Os créditos cobrados por este pedido.
- `X-Creditos-Restantes` (integer): O saldo da conta depois do pedido.
- `X-Duracao` (number): A duração do áudio, em segundos.

Responses:

- `200`: O MP3 da fala, com os headers acima. Com `Accept: application/json`, o JSON ao lado.
- `400`: `texto_invalido`
- `400`: `voz_invalida`
- `400`: `modelo_invalido`
- `400`: `pedido_invalido`
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `402`: `sem_credito`
- `402`: `teto_da_chave`
- `403`: `plano`
- `403`: `escopo`
- `403`: `bloqueado`
- `429`: `muitos_pedidos`
- `429`: `simultaneas`
- `429`: `ocupado`
- `503`: `carregando`
- `503`: `motor_desligado`
- `502`: `falhou`
- `504`: `demorou`
- `500`: `interno`

cURL:

```bash
curl -X POST "https://app.lero.studio/api/v1/tts" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"texto": "Fala, tudo bem? Aqui é o Lero, direto do Rio.", "voz": "rio"}' \
  --output fala.mp3
```

Python:

```python
import os

import requests

r = requests.post(
    "https://app.lero.studio/api/v1/tts",
    headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
    json={"texto": "Fala, tudo bem? Aqui é o Lero, direto do Rio.", "voz": "rio"},
    timeout=300,
)
r.raise_for_status()
with open("fala.mp3", "wb") as f:
    f.write(r.content)
print("restam", r.headers["X-Creditos-Restantes"])
```

JavaScript:

```javascript
import { writeFile } from "node:fs/promises";

const res = await fetch("https://app.lero.studio/api/v1/tts", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.LERO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ texto: "Fala, tudo bem? Aqui é o Lero, direto do Rio.", voz: "rio" }),
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).erro}`);
await writeFile("fala.mp3", Buffer.from(await res.arrayBuffer()));
```

Resposta · Accept: application/json (200):

```json
{
  "pedido": "3f9c1e0a7b2d4c6e8a1b3d5f7e9c0a2b",
  "custo": 45,
  "saldo": 1499955,
  "model": "Lero v1",
  "voz": "rio",
  "duracao": 3.12,
  "formato": "mp3",
  "audio": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYx..."
}
```

## Vozes

As vozes do Lero e os sotaques.

### Listar vozes

`GET /v1/voices`

A lista das vozes, com o id e o nome. Não gasta crédito.

Autenticação: chave.

Responses:

- `200`: As vozes.
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `403`: `plano`
- `403`: `bloqueado`

cURL:

```bash
curl "https://app.lero.studio/api/v1/voices" \
  -H "Authorization: Bearer $LERO_API_KEY"
```

Python:

```python
import os

import requests

r = requests.get(
    "https://app.lero.studio/api/v1/voices",
    headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

JavaScript:

```javascript
const res = await fetch("https://app.lero.studio/api/v1/voices", {
  headers: { "Authorization": `Bearer ${process.env.LERO_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.erro}`);
console.log(data);
```

Resposta (200):

```json
{
  "vozes": [
    {
      "id": "rio",
      "nome": "Rio de Janeiro"
    },
    {
      "id": "sp",
      "nome": "São Paulo"
    },
    {
      "id": "niteroi",
      "nome": "Niterói"
    }
  ]
}
```

## Speech-to-Text (STT)

Áudio em texto, no português falado do Brasil (gírias, siglas e números), com o tempo de cada frase e legenda pronta.

### Transcrever áudio

`POST /v1/stt`

Transforma o áudio em texto, no português falado do Brasil: gírias, siglas, números e nomes no lugar. Devolve o texto com parágrafos e as frases com o tempo de início e fim; se você pedir, o tempo de cada palavra ou a legenda pronta (SRT ou VTT).

Mande o arquivo no campo `audio`, em `multipart/form-data` (até 4 MB), ou um link https público em `url` (até 500 MB; link assinado de S3, GCS ou R2 serve). Arquivo maior que 4 MB vai por `url`, num JSON com as mesmas opções. Formatos: MP3, WAV, M4A, OGG, OPUS, WEBM, FLAC e vídeo MP4 (o Lero usa o áudio do vídeo).

Custa 150 créditos por minuto de áudio (2,5 por segundo), cobrado pelo segundo e arredondado para cima, só quando a transcrição dá certo: se falhar, nada é cobrado. Conta a duração do arquivo inteiro, silêncio incluído. Exemplo: 11 min de áudio custam 1.650 créditos.

Até 2 h de áudio por pedido. A transcrição sai em cerca de 30 vezes o tempo real (1 h de áudio em uns 2 min): use timeout de 300 s. O Lero não guarda o áudio nem o texto da transcrição: apaga tudo ao responder.

| `formato` | resposta |
|---|---|
| `json` (padrão) | JSON com o texto, as frases e o custo (ao lado) |
| `texto` | `text/plain`: só o texto, com parágrafos |
| `srt` | `application/x-subrip`: a legenda SRT |
| `vtt` | `text/vtt`: a legenda WebVTT |

No JSON, `texto` traz parágrafos (`\n\n`) nas pausas longas e `segmentos` traz as frases, com `inicio` e `fim` em segundos. Com `timestamps=palavra`, cada frase traz também `palavras`: `[{inicio, fim, palavra}]`. O `formatado` vem `true` quando a revisão de pontuação e maiúsculas cobriu o texto todo. Os formatos `texto`, `srt` e `vtt` vêm em UTF-8.

Autenticação: chave com o escopo Fala (o mesmo da fala).

Corpo do pedido (multipart/form-data ou application/json):

- `audio` (file, um dos dois): O arquivo de áudio, até 4 MB: MP3, WAV, M4A, OGG, OPUS, WEBM, FLAC ou vídeo MP4. Mande `audio` ou `url`, nunca os dois.
- `url` (string, um dos dois): Link https público do áudio, até 500 MB; o Lero baixa. Para arquivo maior que 4 MB, use `url` (link assinado de S3, GCS, R2 etc. serve).
- `formato` (string, opcional): `json` (padrão), `texto` (text/plain), `srt` ou `vtt` (legendas prontas).
- `timestamps` (string, opcional): `frase` (padrão) ou `palavra`: cada segmento ganha `palavras`, com o início e o fim de cada palavra. Só no `json`.
- `palavras_chave` (string[], opcional): Até 50 nomes, marcas, siglas e termos para reconhecer e manter a grafia, ex. `["Lero", "Unflat", "CNPJ"]`. No multipart, numa lista separada por vírgula: `Lero,Unflat,CNPJ`.
- `formatar` (boolean, opcional): A revisão de pontuação e maiúsculas, padrão `true`. Com `false`, volta mais rápido, com a pontuação do reconhecimento; o preço é o mesmo.
- `idioma` (string, opcional): `pt-BR`, o único por enquanto.
- `model` (string, opcional): `Lero v1`, o único de transcrição por enquanto.

Headers da resposta:

- `X-Pedido-Id` (string): O id do pedido. Mande junto se precisar de suporte.
- `X-Creditos-Custo` (integer): Os créditos cobrados por este pedido.
- `X-Creditos-Restantes` (integer): O saldo da conta depois do pedido.
- `X-Duracao` (number): A duração do áudio, em segundos.

Responses:

- `200`: A transcrição: o JSON ao lado (`formato=json`) ou o arquivo direto (`texto`, `srt`, `vtt`), com os headers acima.
- `400`: `audio_invalido`
- `400`: `url_invalida`
- `400`: `formato_invalido`
- `400`: `idioma_invalido`
- `400`: `modelo_invalido`
- `400`: `pedido_invalido`
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `402`: `sem_credito`
- `402`: `teto_da_chave`
- `403`: `plano`
- `403`: `escopo`
- `403`: `bloqueado`
- `413`: `audio_grande`
- `413`: `audio_longo`
- `429`: `muitos_pedidos`
- `429`: `simultaneas`
- `429`: `ocupado`
- `503`: `motor_desligado`
- `502`: `falhou`
- `504`: `demorou`
- `500`: `interno`

cURL:

```bash
# arquivo local (até 4 MB)
curl -X POST "https://app.lero.studio/api/v1/stt" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -F "audio=@reuniao.mp3" \
  -F "palavras_chave=Lero,Unflat,CNPJ" \
  --max-time 300

# link https público (até 500 MB) e a legenda pronta
curl -X POST "https://app.lero.studio/api/v1/stt" \
  -H "Authorization: Bearer $LERO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://exemplo.com/reuniao.mp3", "formato": "srt"}' \
  --max-time 300 \
  --output legenda.srt
```

Python:

```python
import os

import requests

H = {"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"}

# arquivo local (até 4 MB)
with open("reuniao.mp3", "rb") as f:
    r = requests.post(
        "https://app.lero.studio/api/v1/stt",
        headers=H,
        files={"audio": f},
        data={"palavras_chave": "Lero,Unflat,CNPJ"},
        timeout=300,
    )
r.raise_for_status()
t = r.json()
print(t["texto"])
for s in t["segmentos"]:
    print(s["inicio"], s["fim"], s["texto"])

# link https público (até 500 MB) e a legenda pronta
r = requests.post(
    "https://app.lero.studio/api/v1/stt",
    headers=H,
    json={"url": "https://exemplo.com/reuniao.mp3", "formato": "srt"},
    timeout=300,
)
r.raise_for_status()
with open("legenda.srt", "wb") as f:
    f.write(r.content)
```

JavaScript:

```javascript
import { readFile, writeFile } from "node:fs/promises";

const headers = { "Authorization": `Bearer ${process.env.LERO_API_KEY}` };

// arquivo local (até 4 MB)
const form = new FormData();
form.append("audio", new Blob([await readFile("reuniao.mp3")], { type: "audio/mpeg" }), "reuniao.mp3");
form.append("palavras_chave", "Lero,Unflat,CNPJ");
const res = await fetch("https://app.lero.studio/api/v1/stt", {
  method: "POST",
  headers,
  body: form,
  signal: AbortSignal.timeout(300_000),
});
const t = await res.json();
if (!res.ok) throw new Error(`${res.status} ${t.erro}`);
console.log(t.texto);
for (const s of t.segmentos) console.log(s.inicio, s.fim, s.texto);

// link https público (até 500 MB) e a legenda pronta
const srt = await fetch("https://app.lero.studio/api/v1/stt", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ url: "https://exemplo.com/reuniao.mp3", formato: "srt" }),
  signal: AbortSignal.timeout(300_000),
});
if (!srt.ok) throw new Error(`${srt.status} ${(await srt.json()).erro}`);
await writeFile("legenda.srt", await srt.text());
```

Resposta · formato=json (200):

```json
{
  "pedido": "3f9c1e0a7b2d4c6e8a1b3d5f7e9c0a2b",
  "custo": 1650,
  "saldo": 1498350,
  "model": "Lero v1",
  "idioma": "pt-BR",
  "duracao": 659.4,
  "formatado": true,
  "texto": "Bom dia, pessoal. Hoje a pauta é o lançamento da campanha de verão.\n\nPerfeito. Então...",
  "segmentos": [
    {
      "inicio": 0.0,
      "fim": 3.2,
      "texto": "Bom dia, pessoal."
    },
    {
      "inicio": 3.4,
      "fim": 7.9,
      "texto": "Hoje a pauta é o lançamento da campanha de verão."
    }
  ]
}
```

## Voice Clone

**Em breve.** Uma voz própria a partir das gravações da pessoa dona da voz, com o consentimento dela.

## Conversational Agent

Voice agent em tempo real por WebRTC, com o seu system prompt, knowledge base e live context. Em beta.

### Listar versões e LLMs

`GET /v1/agents`

O discovery do Conversational Agent: as versões do agente que a sua chave pode usar, os LLMs, as vozes e o setup recomendado. Chame antes de abrir a conversa: a lista muda. Não gasta crédito.

Autenticação: chave.

Responses:

- `200`: As opções de agora.
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`

cURL:

```bash
curl "https://app.lero.studio/api/v1/agents" \
  -H "Authorization: Bearer $LERO_API_KEY"
```

Python:

```python
import os

import requests

r = requests.get(
    "https://app.lero.studio/api/v1/agents",
    headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

JavaScript:

```javascript
const res = await fetch("https://app.lero.studio/api/v1/agents", {
  headers: { "Authorization": `Bearer ${process.env.LERO_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.erro}`);
console.log(data);
```

Resposta (200):

```json
{
  "ligado": true,
  "recomendado": {
    "versao": "v0.4",
    "llm": "oss120b",
    "voz": "rio"
  },
  "versoes": [
    {
      "id": "v0.4",
      "nome": "Agente v0.4",
      "lingua": "pt",
      "vozes": true
    }
  ],
  "llms": [
    {
      "id": "oss120b",
      "nome": "..."
    }
  ],
  "vozes": [
    "rio",
    "sp",
    "niteroi"
  ]
}
```

### Abrir conversa (WebRTC)

`POST /v1/agents/conversation`

O signaling da conversa ao vivo: você manda a SDP offer do seu client WebRTC e recebe a SDP answer. Depois, o áudio e o data channel `chat` vão peer-to-peer entre o seu client e o agente (STT, LLM e TTS em streaming, com barge-in e turn detection). Sem `versao`, `llm` e `voz`, vale o recomendado. Com `instrucoes`, o agente vira o seu: o system prompt substitui a persona do Lero. Passo a passo em [Conversational Agent: passo a passo](#agente-guia).

Autenticação: chave.

Corpo do pedido (JSON):

- `type` (string, obrigatório): Sempre `offer`.
- `sdp` (string, obrigatório): A SDP offer inteira, com o ICE completo (non-trickle), até 20 mil caracteres.
- `versao` (string, opcional): A versão do agente, um id do `GET /v1/agents`. Recomendado: `v0.4`.
- `llm` (string, opcional): O LLM, um id do `GET /v1/agents`. Recomendado: `oss120b`.
- `voz` (string, opcional): `rio`, `sp` ou `niteroi`. Ignorada na versão de voz única (`vozes: false`).
- `instrucoes` (string, opcional): System prompt, até 30 mil caracteres. Substitui a persona do Lero; as voice rules (nada de markdown, código ou URL na fala) vão junto.
- `conhecimento` (string | array, opcional): Knowledge base (RAG): texto ou `[{titulo, texto}]`, até 50 itens e 60 mil caracteres. O agente consulta e não lê em voz alta.
- `abertura` (string, opcional): First message: como o agente abre a ligação. É uma instrução, não a frase exata.
- `palavras` (string[], opcional): STT keywords: até 40 nomes e termos para reconhecer.
- `silencio_s` (false | number[], opcional): Silence prompts. Desligado com `instrucoes`; `[20, 40, 60]` liga (2 toques e a despedida).
- `pesquisa` (boolean, opcional): Web search tool. Desligada com `instrucoes`.
- `max_tokens` (integer, opcional): Tamanho máximo de cada resposta, de 60 a 1000 (padrão 260).

Responses:

- `200`: A SDP answer: aplique com `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())
```

Resposta (200):

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

## Conta

Os créditos da conta.

### Ver saldo

`GET /v1/balance`

Os créditos da conta agora, já com a franquia do mês, e o plano. Não gasta crédito.

Autenticação: chave.

Headers da resposta:

- `X-Creditos-Restantes` (integer): O saldo da conta.

Responses:

- `200`: O saldo e o plano.
- `401`: `sem_chave`
- `401`: `chave_invalida`
- `401`: `chave_vencida`
- `403`: `plano`
- `403`: `bloqueado`

cURL:

```bash
curl "https://app.lero.studio/api/v1/balance" \
  -H "Authorization: Bearer $LERO_API_KEY"
```

Python:

```python
import os

import requests

r = requests.get(
    "https://app.lero.studio/api/v1/balance",
    headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

JavaScript:

```javascript
const res = await fetch("https://app.lero.studio/api/v1/balance", {
  headers: { "Authorization": `Bearer ${process.env.LERO_API_KEY}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.erro}`);
console.log(data);
```

Resposta (200):

```json
{
  "saldo": 1499955,
  "plano": "Dev"
}
```

## Webhooks

Webhooks são o Lero chamando o **seu** servidor quando algo acontece, em vez de você perguntar de tempos em tempos. Exemplo: a conversa do Conversational Agent terminou, e o seu sistema recebe na hora o transcript, a duração e o motivo do fim, para salvar no CRM, avaliar uma entrevista ou abrir um ticket.

1. Em [app.lero.studio/desenvolvedores](https://app.lero.studio/desenvolvedores?aba=webhooks), aba Webhooks, adicione o endereço `https` do seu servidor e escolha os eventos (até 5 webhooks por conta).
2. Copie o segredo do webhook (`whsec_...`) para o seu servidor.
3. Clique em **Enviar teste**: chega o evento `webhook.teste`.
4. No seu servidor, confira a assinatura e responda `2xx` em até 10 s.

| evento | quando |
|---|---|
| `agente.conversa.encerrada` | Uma conversa do Conversational Agent da sua conta terminou. Traz versão, LLM, voz, início, fim, duração, motivo e o transcript. |
| `webhook.teste` | Você clicou em Enviar teste. |

O Speech-to-Text responde na mesma chamada, sem webhook. Eventos de Voice Clone entram quando o produto sair.

Cada envio é um `POST` com `Content-Type: application/json` e os headers `Lero-Evento`, `Lero-Evento-Id` e `Lero-Assinatura: t=<unix>,v1=<hex>`. O `v1` é o HMAC-SHA256 de `"<t>.<corpo cru>"` com o segredo do webhook. Recuse quando a assinatura não bate ou quando `t` tem mais de 5 minutos.

```json
{
  "id": "6f1c2a9e-3b7d-4e58-9a0c-2d4f6b8e1a37",
  "evento": "agente.conversa.encerrada",
  "criado": "2026-11-11T15:42:10.512Z",
  "dados": {
    "conversa_id": "2eecec700b63",
    "versao": "v0.4",
    "llm": "oss120b",
    "voz": "rio",
    "lingua": "pt",
    "inicio": "2026-11-11T15:31:07Z",
    "fim": "2026-11-11T15:42:09Z",
    "duracao_s": 662.4,
    "motivo": "a pessoa desligou",
    "transcript": [
      { "papel": "agente", "texto": "Oi! Aqui é a Bia, da Acme.", "t": 1.2 },
      { "papel": "pessoa", "texto": "Oi, tudo bem?", "t": 4.8 }
    ]
  }
}
```

Conferir a assinatura (Node):

```javascript
import crypto from "node:crypto";

// Next.js route: POST /api/lero-webhook. LERO_WEBHOOK_SECRET = o segredo do webhook (whsec_...)
export async function POST(req) {
  const corpo = await req.text();                                  // o corpo cru, antes do JSON.parse
  const [t, v1] = (req.headers.get("lero-assinatura") || "").split(",").map((p) => p.split("=")[1]);
  const esperado = crypto.createHmac("sha256", process.env.LERO_WEBHOOK_SECRET).update(`${t}.${corpo}`).digest("hex");
  const ok = v1 && v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
  if (!ok || Math.abs(Date.now() / 1000 - Number(t)) > 300) return new Response("assinatura inválida", { status: 401 });

  const evento = JSON.parse(corpo);
  if (evento.evento === "agente.conversa.encerrada") {
    // salve evento.dados.transcript, avalie a entrevista, abra o ticket...
  }
  return new Response("ok");                                       // responda 2xx rápido; o trabalho pesado vai para uma fila
}
```

Conferir a assinatura (Python):

```python
import hashlib, hmac, json, os, time

from flask import Flask, request

app = Flask(__name__)


@app.post("/lero-webhook")
def lero_webhook():
    corpo = request.get_data(as_text=True)                         # o corpo cru
    partes = dict(p.split("=", 1) for p in request.headers.get("Lero-Assinatura", "").split(",") if "=" in p)
    esperado = hmac.new(os.environ["LERO_WEBHOOK_SECRET"].encode(), f"{partes.get('t')}.{corpo}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(partes.get("v1", ""), esperado) or abs(time.time() - int(partes.get("t", 0))) > 300:
        return "assinatura inválida", 401
    evento = json.loads(corpo)
    if evento["evento"] == "agente.conversa.encerrada":
        pass  # salve evento["dados"]["transcript"]...
    return "ok"

```

- Sem `2xx`, o Lero tenta de novo depois de 2 s e de 8 s (3 tentativas). O `id` do evento é o mesmo nas tentativas: use para não processar 2 vezes.
- Redirect não é seguido: use o endereço final.
- As últimas entregas, com o status e o tempo, aparecem na aba Webhooks.

## Conversational Agent: passo a passo

O Conversational Agent é uma ligação de voz em tempo real. O HTTP só faz o discovery e o signaling; depois, o áudio e os eventos vão peer-to-peer por WebRTC. Latência típica de ~1 s, com barge-in (a pessoa interrompe e o agente para).

1. **Discover.** `GET /v1/agents` devolve as versões, os LLMs e as vozes que a sua chave pode usar.
2. **Connect.** No client WebRTC, crie o data channel `chat` antes da offer, adicione 1 track de áudio (o microfone) e espere o ICE gathering.
3. **Signaling.** Mande a SDP offer para `POST /v1/agents/conversation` (do seu backend, com a chave) e aplique a SDP answer. A conversa começa e o agente cumprimenta.
4. **Custom agent (opcional).** No mesmo POST, `instrucoes` (system prompt), `conhecimento` (knowledge base), `abertura`, `palavras` e os ajustes fazem o agente ser o seu.
5. **Live context (opcional).** Durante a ligação, mande pelo `chat` o que o agente precisa ver: o código de um live coding, o carrinho, a tela.
6. **Events.** Leia pelo `chat` o transcript, o estado e a latência de cada turno.
7. **Hang up.** Feche a `RTCPeerConnection`.

Live context e controle, pelo data channel (toda mensagem precisa do campo `type`):

```javascript
// live context: one panel per key, always the latest version (send the full text on change, ~every 2 s)
chat.send(JSON.stringify({ type: "lero-contexto", chave: "codigo", texto: editor.getValue() }));
chat.send(JSON.stringify({ type: "lero-contexto", chave: "testes", texto: "3/5 testes passaram", responder: true }));

// system note: the person does not hear it; the agent acts on the next free turn (responder: false only stores it)
chat.send(JSON.stringify({ type: "lero-instrucao", texto: "O tempo da fase 2 acabou. Passe para a fase 3." }));

// exact utterance, bypasses the LLM
chat.send(JSON.stringify({ type: "lero-falar", texto: "Faltam 5 minutos." }));
```

| mensagem | campos | o que faz |
|---|---|---|
| `lero-contexto` | `chave` (até 60), `texto` (até 20 mil), `responder` | Um painel vivo por chave (até 8). Só a versão mais nova entra na próxima resposta. `texto` vazio apaga. |
| `lero-instrucao` | `texto` (até 4 mil), `responder` | System note que a pessoa não ouve: troca de fase, tempo, dica. |
| `lero-falar` | `texto` (até 2 mil) | O agente fala exatamente o texto, sem passar pelo LLM. |

Events que chegam pelo `chat` (campo `tipo`):

| tipo | campos |
|---|---|
| `estado` | `estado`: conectado, na_fila, ouvindo, pensando, falando, aviso_fim, encerrada; `motivo` no fim |
| `fala` | `papel` (pessoa ou agente), `texto`, `final`, `interrompido`: o transcript ao vivo |
| `latencia` | `total_ms` e as etapas do turno |
| `ignorado` | um backchannel curto ("uhum") que não interrompeu o agente |

A ligação acaba quando você fecha a conexão, na despedida por silêncio (se `silencio_s` estiver ligado) ou depois de 10 minutos sem fala. Toda ligação fica gravada (áudio, transcript e a config do agente) para a revisão de qualidade.

## Exemplos

Um cliente completo, com timeout e espera no `429` e no `503`:

JavaScript:

```javascript
import { writeFile } from "node:fs/promises";

const espera = s => new Promise(ok => setTimeout(ok, s * 1000));

// gera a fala e devolve o MP3; espera o Retry-After no 429 e no 503
async function falar(texto, voz = "rio", tentativas = 3) {
  for (let i = 0; i < tentativas; i++) {
    const res = await fetch("https://app.lero.studio/api/v1/tts", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.LERO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ texto, voz }),
      signal: AbortSignal.timeout(300_000),
    });
    if (res.status === 429 || res.status === 503) {
      await espera(Number(res.headers.get("Retry-After")) || 10);
      continue;
    }
    if (!res.ok) {
      const { erro, mensagem } = await res.json();
      throw new Error(`${res.status} ${erro}: ${mensagem}`);
    }
    return Buffer.from(await res.arrayBuffer());
  }
  throw new Error("a voz seguiu ocupada");
}

await writeFile("fala.mp3", await falar("Bom dia! Seu pedido saiu pra entrega.", "sp"));
```

Python:

```python
import os
import time

import requests


def falar(texto, voz="rio", tentativas=3):
    """Gera a fala e devolve o MP3; espera o Retry-After no 429 e no 503."""
    for _ in range(tentativas):
        r = requests.post(
            "https://app.lero.studio/api/v1/tts",
            headers={"Authorization": f"Bearer {os.environ['LERO_API_KEY']}"},
            json={"texto": texto, "voz": voz},
            timeout=300,
        )
        if r.status_code in (429, 503):
            time.sleep(int(r.headers.get("Retry-After", "10")))
            continue
        if not r.ok:
            e = r.json()
            raise RuntimeError(f"{r.status_code} {e['erro']}: {e['mensagem']}")
        return r.content
    raise RuntimeError("a voz seguiu ocupada")


with open("fala.mp3", "wb") as f:
    f.write(falar("Oi! Aqui é o Lero, de Niterói.", voz="niteroi"))
```

## Suporte

Dúvidas e problemas: oi@lero.studio. Mande o `X-Pedido-Id` do pedido que deu errado.
