Pular para o conteúdo principal

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.
Quando não usar

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ícieQuem chamaCredencial
Conversa (sessões, mensagens, eventos)O seu backendCabeçalho x-api-key com a chave pak_…
Jornadas (leads e bloqueio)O seu backendCabeçalho x-api-key com a chave pak_…
Gerenciamento de chavesVocê (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-Id em 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 é 429 e 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​

EventoQuando aconteceCampos próprios
statePrimeiro evento de uma conexão nova (sem Last-Event-ID)driver (agent_driving ou human_driving), openTurn
message_deltaUm pedaço do texto da respostadelta
tool_startO agente começou a usar uma ferramentaname
tool_endA ferramenta terminouname
metadataMarcação de fim de processamento interno— (sempre vazio)
doneO turno terminoumessageId, e aborted: true quando foi interrompido
errorO turno falhouerrorCode, message (texto já apresentável ao usuário)
handoff.requestedA conversa foi para a fila de atendimento humanoreason
handoff.assignedUm atendente assumiuattendant.displayName
handoff.attendant_messageO atendente humano respondeutext, externalUserId, attachments (opcional)
handoff.returnedO atendimento voltou para o agente—
replay_gapO Last-Event-ID é antigo demaisconversationId
conversation_rolledA janela de sessão virou; conversa novaconversationId

Todo evento de conversa carrega também conversationId, ts (epoch em segundos) e correlationId; os eventos de um turno carregam messageId.

O que a API pública não entrega

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
leadsDe 1 a 1.000 leads por chamada.
leads[].external_lead_idO ID do lead no seu sistema. Obrigatório.
leads[].phoneO telefone do lead.
leads[].varsOs valores das variáveis do template.
leads[].attributesOs atributos que a jornada exige.
startnow (padrão) ou scheduled.
scheduled_atObrigatório com start: "scheduled", e só aceito nesse caso.
nameNome 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 o contact_id é o que já existia. Repetir a chamada é seguro.
  • bloqueado: o lead ficou de fora, com o motivo. Os motivos possíveis são invalid_phone, duplicate_phone, do_not_contact, missing_lead_id, invalid_lead_id, duplicate_lead, blocked_lead, cooldown, already_active (na fila de outra jornada) e missing_attribute (com a lista em atributos).

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étodoRotaO que faz
POST/keysCria uma chave — única resposta com o segredo
GET/keysLista 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}/rotateGera 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.

codeStatusO que houve
public_api_key_invalid401Chave ausente, malformada, desconhecida, revogada ou com segredo errado
public_api_invalid_external_user_id400external_user_id vazio ou acima de 256 caracteres
public_api_idempotency_key_required400Faltou o cabeçalho x-idempotency-key no envio
public_api_invalid_request400Corpo inválido (ex.: agent_id vazio, callback_url recusado)
public_api_rate_limited429Limite de requisições por minuto estourado
public_api_key_not_found404Não existe essa chave na sua organização
public_api_key_conflict409Conflito ao criar a chave — repita a chamada
unauthenticated401Token da conta ausente ou expirado (rotas de gerenciamento)
anonymous_principal_forbidden403O token não é de um usuário autenticado da organização
public_api_not_configured503A 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_id está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-key novo por mensagem, reutilizado apenas nas retentativas da mesma mensagem.
  • Guarde o último id recebido no stream e reenvie em Last-Event-ID ao reconectar.
  • Trate conversation_rolled limpando 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.