API pública
A API pública é o jeito de o seu próprio sistema conversar com o seu agente: o seu backend manda a mensagem do usuário final, recebe a resposta e segue a vida. Não há navegador, não há login do usuário final, não há widget — é uma integração servidor-a-servidor, autenticada por uma chave de API de longa duração.
Use a API pública quando você quiser:
- colocar o agente dentro do seu aplicativo ou site, com a sua interface;
- atender por um canal que você mesmo opera (um app próprio, um sistema interno, um bot que você já tem);
- automatizar conversas a partir do seu backend.
Endereço base
Todas as chamadas abaixo são relativas a:
https://api.lab019.ai/gateway/v1/public
Tudo é HTTPS, corpo em JSON (application/json), respostas em JSON.
Autenticação
Existem duas superfícies nesta API, com credenciais diferentes:
| Superfície | Quem chama | Credencial |
|---|---|---|
| Conversa (sessões, mensagens, eventos) | O seu backend | Cabeçalho x-api-key com a chave pak_… |
| Gerenciamento de chaves | Você (ou o seu painel/CLI) | Cabeçalho Authorization: Bearer <token da sua conta> |
A chave de API
Uma chave tem o formato:
pak_{key_id}.{secret}
key_id— a parte antes do ponto. É um identificador público: aparece nas listagens e é o que você usa nas rotas de gerenciamento.secret— a parte depois do ponto. É o segredo de verdade. Só é exibido uma vez, na resposta de criação (ou de rotação) da chave. A plataforma guarda apenas um hash — não há como recuperá-lo depois. Se perdeu, rotacione.
Cada chave carrega a configuração da integração:
- agente (
agent_id) — qual agente responde às conversas dessa chave; - transferência para humano (
handoff_enabled) — se as conversas podem ser assumidas por um atendente; - webhook de callback (
callback_url, opcional) — para onde a plataforma também envia os eventos, além do stream (veja Webhook de callback); - tempo de sessão (
session_ttl_seconds, opcional) — a janela de inatividade descrita em Conversas e sessões.
Criando a primeira chave
No painel, abra Configurações → Canais → API pública, escolha o agente e clique em criar. A chave completa aparece uma única vez — copie e guarde no cofre de segredos do seu sistema.
Pelo próprio API:
curl -X POST https://api.lab019.ai/gateway/v1/public/keys \
-H "Authorization: Bearer $MEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{"agent_id": "atendimento", "handoff_enabled": true}'
{
"key_id": "8Kd3vQpX2nRt",
"org_id": "org_abc123",
"agent_id": "atendimento",
"handoff_enabled": true,
"callback_url": "",
"prefix": "pak_9fLm2Q",
"last4": "b7Zk",
"created_at": "2026-08-06T13:20:41.512Z",
"updated_at": "2026-08-06T13:20:41.512Z",
"rotation_pending": false,
"session_ttl_seconds": null,
"api_key": "pak_8Kd3vQpX2nRt.9fLm2Q...b7Zk"
}
O campo api_key não volta em nenhuma outra resposta. prefix e last4
são apenas uma dica visual para você reconhecer a chave numa listagem.
Usando a chave
Toda chamada da superfície de conversa apresenta a chave inteira:
curl https://api.lab019.ai/gateway/v1/public/sessions \
-H "x-api-key: pak_8Kd3vQpX2nRt.9fLm2Q...b7Zk" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "cliente-4711"}'
Não há token de sessão separado, nem login, nem refresh: a chave é a credencial, reapresentada em cada chamada.
Rotação sem downtime
Ao rotacionar, a chave antiga continua funcionando por uma janela de carência (24 horas por padrão, ajustável na chamada). Isso dá tempo de o seu deploy trocar o segredo sem derrubar requisições em voo. Depois da carência, só a nova vale.
Para um corte imediato, rotacione com grace_seconds: 0 — ou revogue a chave,
o que invalida na hora tanto o segredo atual quanto qualquer segredo em
carência.
Conversas e sessões
Você não cria nem guarda um id de conversa. Quem identifica o interlocutor
é o external_user_id — o identificador estável do usuário no seu sistema
(um id de cliente, um telefone, um hash — o que fizer sentido; até 256
caracteres).
O mesmo external_user_id sempre cai na mesma conversa. Não há sessão para
guardar entre chamadas.
Janela de sessão. Depois de um período de inatividade (24 horas por
padrão, configurável por chave), o próximo contato daquele external_user_id
começa uma conversa nova — o histórico anterior não volta. Se um stream
estiver aberto quando isso acontece, ele avisa com um evento
conversation_rolled e passa a seguir a conversa nova.
Convenções
- Correlação. Você pode enviar
X-Correlation-Idem qualquer chamada; a resposta sempre devolve esse cabeçalho (ou um id novo, se você não mandar). É o identificador para citar em um chamado de suporte. - Erros seguem o formato
application/problem+json(RFC 7807) — veja Erros. - Limites de taxa. Por padrão, 120 requisições por minuto por chave e por
IP de origem (o operador da plataforma pode ajustar). Ao estourar, a
resposta é
429e o correto é aguardar e repetir. - Idempotência. O envio de mensagem exige um
x-idempotency-key— repetir a chamada com a mesma chave devolve o mesmo resultado sem gerar uma segunda resposta do agente.
Endpoints de conversa
POST /sessions — resolver a conversa
Resolve (e aquece) a conversa de um usuário. Opcional: você pode ir direto ao
envio de mensagem. Útil para descobrir o conversation_id antes de abrir o
stream, ou para saber se a transferência para humano está habilitada.
Requisição
POST /sessions
x-api-key: pak_…
Content-Type: application/json
{ "external_user_id": "cliente-4711" }
Resposta 201
{
"session_id": "cliente-4711",
"external_user_id": "cliente-4711",
"conversation_id": "conv_9c1f…",
"agent_id": "atendimento",
"handoff_enabled": true
}
session_id é o próprio external_user_id, mantido por compatibilidade de
formato — não é uma credencial e não expira.
POST /conversations/{external_user_id}/messages — enviar uma mensagem
Envia uma mensagem do usuário. A chamada retorna imediatamente: a resposta do agente é gerada em segundo plano e chega pelo stream de eventos (ou pelo webhook, no caso de atendimento humano).
Requisição
POST /conversations/cliente-4711/messages
x-api-key: pak_…
x-idempotency-key: 7f1e4b6a-2c9d-4f0e-9a11-2b3c4d5e6f70
Content-Type: application/json
{ "text": "Qual o prazo de entrega para o CEP 01310-100?" }
O cabeçalho x-idempotency-key é obrigatório — gere um valor único por
mensagem (um UUID resolve). Repetir a chamada com a mesma chave devolve o
mesmo messageId, sem rodar um segundo turno.
Resposta 202
{ "messageId": "3f9a1c8e5b2d4a67" }
Esse messageId é o que amarra a mensagem aos eventos do stream.
GET /conversations/{external_user_id}/events — stream de eventos (SSE)
Assina o fluxo de saída da conversa: os pedaços da resposta do agente, o uso de ferramentas, o fim do turno e os eventos de atendimento humano.
Requisição
GET /conversations/cliente-4711/events
x-api-key: pak_…
Accept: text/event-stream
Last-Event-ID: 1754487641512-0 # opcional, para retomar de onde parou
Resposta — text/event-stream, no formato padrão de SSE:
event: state
data: {"driver":"agent_driving","openTurn":false}
id: 1754487641512-0
event: message_delta
data: {"delta":"O prazo para o CEP ","conversationId":"conv_9c1f…","messageId":"3f9a…","ts":1754487641.512,"correlationId":"…"}
id: 1754487641512-1
event: message_delta
data: {"delta":"01310-100 é de 2 dias úteis.","conversationId":"conv_9c1f…","messageId":"3f9a…","ts":1754487641.640,"correlationId":"…"}
id: 1754487641701-0
event: done
data: {"messageId":"3f9a…","conversationId":"conv_9c1f…","ts":1754487641.701,"correlationId":"…"}
Enquanto não há nada a enviar, o stream manda um comentário de keep-alive
(: ping) — ignore-o.
Retomada. Cada evento vem com um id. Guardando o último id recebido e
reenviando-o em Last-Event-ID, você recupera o que perdeu na queda de
conexão. São retidos cerca de mil eventos por conversa: se o seu id for antigo
demais, você recebe um único evento replay_gap (houve buraco) e o stream
segue do ponto atual.
Eventos
| Evento | Quando acontece | Campos próprios |
|---|---|---|
state | Primeiro evento de uma conexão nova (sem Last-Event-ID) | driver (agent_driving ou human_driving), openTurn |
message_delta | Um pedaço do texto da resposta | delta |
tool_start | O agente começou a usar uma ferramenta | name |
tool_end | A ferramenta terminou | name |
metadata | Marcação de fim de processamento interno | — (sempre vazio) |
done | O turno terminou | messageId, e aborted: true quando foi interrompido |
error | O turno falhou | errorCode, message (texto já apresentável ao usuário) |
handoff.requested | A conversa foi para a fila de atendimento humano | reason |
handoff.assigned | Um atendente assumiu | attendant.displayName |
handoff.attendant_message | O atendente humano respondeu | text, externalUserId, attachments (opcional) |
handoff.returned | O atendimento voltou para o agente | — |
replay_gap | O Last-Event-ID é antigo demais | conversationId |
conversation_rolled | A janela de sessão virou; conversa nova | conversationId |
Todo evento de conversa carrega também conversationId, ts (epoch em
segundos) e correlationId; os eventos de um turno carregam messageId.
O raciocínio interno do agente e os detalhes de custo/roteamento não são
expostos aqui — metadata chega vazio de propósito. A ferramenta interna de
transferência para humano também não aparece em tool_start/tool_end.
Webhook de callback
Se o seu backend não mantém o stream aberto o tempo todo, configure um
callback_url na chave. A plataforma envia os eventos de atendimento humano
para lá além de publicá-los no stream — o que chegar primeiro, chegou.
Requisitos do endereço: HTTPS, host público (endereços internos e de loopback são recusados), sem usuário/senha embutidos na URL. Redirecionamentos não são seguidos.
O que a plataforma envia — POST, JSON:
{
"event": "handoff.attendant_message",
"conversationId": "conv_9c1f…",
"externalUserId": "cliente-4711",
"text": "Oi! Aqui é a Ana, vou te ajudar com esse pedido.",
"attachments": [],
"ts": 1754487999.412,
"correlationId": "b3a9…"
}
Responda com 2xx. A entrega é best-effort e não tem retentativa: o stream de
eventos continua sendo a via confiável.
Gerenciamento de chaves
Estas rotas são a área administrativa da sua organização — autenticam com o
token da sua conta (Authorization: Bearer …), nunca com a pak_…. Toda
resposta é restrita à sua organização; a chave de outra organização responde
404, não 403.
| Método | Rota | O que faz |
|---|---|---|
POST | /keys | Cria uma chave — única resposta com o segredo |
GET | /keys | Lista as suas chaves (mascaradas) |
GET | /keys/{key_id} | Detalha uma chave (mascarada) |
PATCH | /keys/{key_id} | Altera agente, handoff, webhook ou tempo de sessão |
POST | /keys/{key_id}/rotate | Gera um novo segredo, com carência |
DELETE | /keys/{key_id} | Revoga a chave (irreversível) |
POST /keys
{
"agent_id": "atendimento",
"handoff_enabled": true,
"callback_url": "https://seu-backend.exemplo.com/webhook",
"session_ttl_seconds": 3600
}
Só agent_id é obrigatório. session_ttl_seconds omitido usa o padrão da
plataforma; 0 significa "a conversa nunca expira por inatividade".
Resposta 201 — o objeto da chave com api_key.
GET /keys
{
"keys": [
{
"key_id": "8Kd3vQpX2nRt",
"org_id": "org_abc123",
"agent_id": "atendimento",
"handoff_enabled": true,
"callback_url": "https://seu-backend.exemplo.com/webhook",
"prefix": "pak_9fLm2Q",
"last4": "b7Zk",
"created_at": "2026-08-06T13:20:41.512Z",
"updated_at": "2026-08-06T15:02:07.880Z",
"rotation_pending": true,
"session_ttl_seconds": 3600
}
]
}
rotation_pending: true indica que existe um segredo anterior ainda dentro da
janela de carência.
PATCH /keys/{key_id}
Só os campos enviados mudam. "callback_url": "" remove o webhook.
{ "handoff_enabled": false, "callback_url": "" }
Resposta 200 com o objeto atualizado (mascarado).
POST /keys/{key_id}/rotate
{ "grace_seconds": 3600 }
Corpo opcional ({} usa a carência padrão de 24 h; 0 corta na hora).
Resposta 200 — o objeto da chave, api_key com o novo segredo e
previous_secret_expires_at (epoch em segundos, ou null quando não há
carência).
DELETE /keys/{key_id}
Resposta 204, sem corpo. É definitivo: não existe "desabilitar e reativar".
Erros
Toda falha responde application/problem+json:
{
"type": "about:blank",
"title": "Unauthorized",
"status": 401,
"code": "public_api_key_invalid",
"detail": "malformed api key",
"correlation_id": "9c2b…"
}
Programe contra o campo code — title e detail são texto para humano.
code | Status | O que houve |
|---|---|---|
public_api_key_invalid | 401 | Chave ausente, malformada, desconhecida, revogada ou com segredo errado |
public_api_invalid_external_user_id | 400 | external_user_id vazio ou acima de 256 caracteres |
public_api_idempotency_key_required | 400 | Faltou o cabeçalho x-idempotency-key no envio |
public_api_invalid_request | 400 | Corpo inválido (ex.: agent_id vazio, callback_url recusado) |
public_api_rate_limited | 429 | Limite de requisições por minuto estourado |
public_api_key_not_found | 404 | Não existe essa chave na sua organização |
public_api_key_conflict | 409 | Conflito ao criar a chave — repita a chamada |
unauthenticated | 401 | Token da conta ausente ou expirado (rotas de gerenciamento) |
anonymous_principal_forbidden | 403 | O token não é de um usuário autenticado da organização |
public_api_not_configured | 503 | A API pública não está habilitada nesta instalação |
Por segurança, o 401 de chave inválida não diz qual verificação falhou —
chave malformada, key_id inexistente e segredo errado devolvem a mesma
resposta.
Um fluxo completo
API=https://api.lab019.ai/gateway/v1/public
KEY="pak_8Kd3vQpX2nRt.9fLm2Q...b7Zk"
USER=cliente-4711
# 1) (opcional) resolve a conversa
curl -s "$API/sessions" \
-H "x-api-key: $KEY" -H 'Content-Type: application/json' \
-d "{\"external_user_id\": \"$USER\"}"
# 2) abre o stream ANTES de enviar, para não perder o começo da resposta
curl -N "$API/conversations/$USER/events" \
-H "x-api-key: $KEY" -H 'Accept: text/event-stream' &
# 3) envia a mensagem
curl -s -X POST "$API/conversations/$USER/messages" \
-H "x-api-key: $KEY" \
-H "x-idempotency-key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{"text": "Qual o prazo de entrega para o CEP 01310-100?"}'
A ordem importa: abra o stream antes de enviar a mensagem. Se abrir depois,
você só recebe os eventos a partir daquele instante — o começo da resposta já
terá passado (e a retomada por Last-Event-ID depende de você já ter um id).
Boas práticas
- Um
external_user_idestável por usuário final. É ele que mantém o histórico; um id novo a cada chamada gera uma conversa nova a cada vez. - Um
x-idempotency-keynovo por mensagem, reutilizado apenas nas retentativas da mesma mensagem. - Guarde o último
idrecebido no stream e reenvie emLast-Event-IDao reconectar. - Trate
conversation_rolledlimpando o que você mostrava do histórico anterior — dali em diante é outra conversa. - Guarde a chave num cofre de segredos, nunca no código nem no
front-end: quem tem a
pak_…fala com o seu agente em nome do seu sistema.