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.
Para colocar o agente numa página web sem escrever backend, use o widget; para WhatsApp, Telegram ou voz, use os canais prontos. A API pública é para quem quer controlar a experiência ponta a ponta.
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_… |
| Jornadas (leads e bloqueio) | 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.
Pela própria 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.
Jornadas: carregar leads
Com uma jornada criada, o seu sistema carrega leads
nela direto, sem montar um CSV. O número, o agente, o template e o
acompanhamento vêm da jornada. A chave é a mesma x-api-key (pak_…) da
conversa, e qualquer chave da organização serve.
POST /journeys/{journey_id}/leads — carregar leads
curl -X POST https://api.lab019.ai/gateway/v1/public/journeys/$JORNADA/leads \
-H "x-api-key: pak_8Kd3vQpX2nRt.9fLm2Q...b7Zk" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"external_lead_id": "cliente-000123",
"phone": "+5519991234567",
"vars": {"nome": "Ana Paula", "protocolo": "A-1029"},
"attributes": {"plano": "anual"}
}
]
}'
| Campo | |
|---|---|
leads | De 1 a 1.000 leads por chamada. |
leads[].external_lead_id | O ID do lead no seu sistema. Obrigatório. |
leads[].phone | O telefone do lead. |
leads[].vars | Os valores das variáveis do template. |
leads[].attributes | Os atributos que a jornada exige. |
start | now (padrão) ou scheduled. |
scheduled_at | Obrigatório com start: "scheduled", e só aceito nesse caso. |
name | Nome do disparo, até 120 caracteres (opcional). |
vars e attributes somam até 50 chaves por lead. A mesma chave nos dois
com valores diferentes recusa a chamada inteira (422). Número, agente,
template e conteúdo não são aceitos no corpo: vêm da jornada.
A resposta traz um resultado por lead, na ordem do pedido:
{
"dispatch_id": "01J9ZX3Q8V5M2R7T4K6N1B0C9D",
"accepted": 1,
"rejected": 1,
"already_loaded": 0,
"leads": [
{"external_lead_id": "cliente-000123", "resultado": "aceito", "contact_id": "…"},
{"external_lead_id": "cliente-000124", "resultado": "bloqueado", "motivo": "cooldown"}
]
}
aceito: o lead entrou no disparo.ja_carregado: o lead já está na fila desta jornada. Nada é criado de novo, e ocontact_idé o que já existia. Repetir a chamada é seguro.bloqueado: o lead ficou de fora, com omotivo. Os motivos possíveis sãoinvalid_phone,duplicate_phone,do_not_contact,missing_lead_id,invalid_lead_id,duplicate_lead,blocked_lead,cooldown,already_active(na fila de outra jornada) emissing_attribute(com a lista ematributos).
A resposta é 201 quando algum lead foi gravado, e 200 com
dispatch_id: null quando nenhum foi.
GET /journeys/{journey_id}/leads — leads na fila
Lista os leads da jornada que ainda não saíram (status=pending, o padrão)
ou que estão saindo agora (status=in_flight). Pagina com limit (1 a 200,
padrão 50) e cursor:
{
"items": [
{
"external_lead_id": "cliente-000123",
"contact_id": "…",
"dispatch_id": "01J9ZX3Q8V5M2R7T4K6N1B0C9D",
"status": "pending",
"updated_at": "2026-09-28T12:00:00Z"
}
],
"next_cursor": null
}
O telefone e os atributos não voltam nesta listagem.
POST /blocked-leads e DELETE /blocked-leads/{external_lead_id} — bloquear um lead
Um lead bloqueado pelo ID não entra em nenhuma jornada da organização.
POST /blocked-leads recebe {"external_lead_id": "...", "reason": "..."}
(reason é opcional, até 200 caracteres) e responde 201.
DELETE /blocked-leads/{external_lead_id} desbloqueia e responde 204.
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á disponível |
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.