A API do Hugi
É o caminho para quem já tem sistema e quer o Hugi como ponte: o WhatsApp entra e sai por aqui, e o atendimento acontece na tela que a sua equipe já usa.
Tudo abaixo usa uma chave de API no cabeçalho:
Authorization: Bearer hugi_sk_...
O tenant vem da chave. Se tenantId vier no corpo, a resposta é 400 — nunca um silêncio que
faria você acreditar que o campo estava valendo.
O contrato completo, em OpenAPI, fica em GET /api/v1/openapi.json — e ele não pede chave, de
propósito: documentação atrás de credencial é documentação que ninguém lê antes de decidir.
Você RECEBE por webhook, não por consulta em laço #
Este é o ponto que decide a arquitetura do seu lado, então ele vem primeiro.
Cadastre o endereço em Webhook de saída e o Hugi chama você quando algo
acontece. Não fique consultando GET /atendimentos de minuto em minuto — você gastaria o
limite de taxa para descobrir, atrasado, o que o webhook te contaria na hora.
Os eventos:
| Evento | Quando |
|---|---|
mensagem.recebida |
o cliente escreveu — com o contato, a conversa, o protocolo e, se houver anexo, o endereço para baixar |
mensagem.recibo |
o que você enviou foi aceito, entregue, lido ou falhou |
mensagem.transcrita |
o texto de um áudio ficou pronto |
atendimento.aberto |
uma conversa nova entrou |
atendimento.finalizado |
uma conversa foi encerrada, com motivo e os dois tempos |
Você escolhe quais quer no cadastro. Não marcar nenhum significa todos, inclusive os que ainda vamos criar.
Responder #
POST /api/v1/atendimentos/{id}/mensagens
Idempotency-Key: 6f1c0f9c-...
Content-Type: application/json
{ "texto": "Recebemos o seu exame, obrigado." }
Se você indexa por telefone e não guardou o nosso id, dá para mandar por número:
POST /api/v1/mensagens
{ "para": "5527999998888", "texto": "..." }
A resposta de sucesso é 202, e não 200: aceito não é entregue. Quem diz que chegou é o
mensagem.recibo, depois.
A janela de 24 horas #
Fora dela, o WhatsApp não deixa mandar texto livre — e a recusa vem como 422 com o motivo:
{
"erro": "envio_recusado",
"mensagem": "A janela de 24h venceu. Fora de conversa aberta, use um template aprovado.",
"detalhe": { "motivo": "janela_de_resposta_expirada" }
}
O caminho de dentro da janela é o texto; o de fora é o template:
{ "template": { "nome": "lembrete_de_consulta", "idioma": "pt_BR", "variaveis": ["Ana", "14h"] } }
O que a API NÃO faz, e por quê #
Não começa conversa com quem nunca escreveu. No WhatsApp quem começa é o cliente, e um contato
do Hugi nasce quando ele manda a primeira mensagem. POST /mensagens para um número desconhecido
devolve 422 SEM_ATENDIMENTO_ABERTO, em vez de criar um contato fantasma.
Não cria atendente nem empresa. Identidade é do Cert4All, e a conta não se administra por aqui.
Administrar a conversa #
POST /api/v1/atendimentos/{id}/finalizar { "motivoId": "...", "observacao": "..." }
POST /api/v1/atendimentos/{id}/reabrir
POST /api/v1/atendimentos/{id}/transferir { "paraFilaId": "..." }
POST /api/v1/atendimentos/{id}/transferir { "paraAtendenteId": "...", "nota": "..." }
POST /api/v1/atendimentos/{id}/notas { "texto": "..." }
Encerre o que você resolveu. Uma conversa que o seu sistema já tratou e que fica aberta aqui enche a fila, conta como conversa do mês e mantém o relógio de tempo de atendimento correndo sobre algo que ninguém está esperando.
transferir é o transbordo: quando o seu sistema não sabe responder, devolver para a equipe é
melhor que encerrar uma conversa que o cliente ainda está esperando.
⚠️ Para uma pessoa, a nota é obrigatória. Para fila, não. A diferença é de efeito: a fila
inteira vê a conversa e alguém escolhe pegá-la; uma pessoa recebe um atendimento nas mãos, e sem
contexto ela recomeça a conversa do zero com o cliente.
Ler #
GET /api/v1/atendimentos?estado=&fila=&contato=&cursor=&limite=
GET /api/v1/atendimentos/{id}
GET /api/v1/atendimentos/{id}/mensagens?cursor=
GET /api/v1/atendimentos/{id}/eventos
GET /api/v1/contatos
GET /api/v1/conexoes
GET /api/v1/plano
Página por cursor, nunca por offset. Use o proximoCursor que veio na página anterior. Com
offset, uma mensagem nova chegando entre duas páginas repete um item e pula outro — e a página 2
é lida justamente enquanto chegam mensagens.
Baixar um anexo #
O mensagem.recebida traz midia.url, algo como /api/v1/midias/<id-da-mensagem>. Chame com a sua
chave e receba os bytes, com o content-type do arquivo.
O endereço da mídia dentro do WhatsApp nunca sai daqui: ele vem acompanhado das chaves de decifragem, e entregá-lo a você poria credencial nossa no seu código.
Limite de taxa #
Toda resposta traz:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 60
Passando disso, 429 com Retry-After. O limite é por chave, então o tráfego de uma integração
não derruba a outra — mais um motivo para uma chave por sistema.
Erros #
Sempre a mesma forma:
{ "erro": "codigo_estavel", "mensagem": "frase que diz o que fazer", "detalhe": {}, "traceId": "..." }
Decida por erro, mostre mensagem, e cite traceId ao falar com a gente. A frase pode mudar
quando alguém revisa o tom; o código, não.
detalhe.retentavel diz se repetir tem chance de dar certo.
Travar a versão #
Accept-Version: 1
Opcional. Mandando, você garante que um deploy nosso não muda o contrato debaixo do seu código: se
um dia servirmos só a versão 2, você recebe 406 em vez de uma resposta em formato diferente.