Webhooks
Para recuperar configurações anteriores após recriar uma sessão, consulte Histórico e restauração de webhooks. A restauração é manual e não interfere na conexão ou reconexão.
O ViperConnect envia payloads no formato object → entry → changes → value. Responda HTTP 2xx rapidamente e processe o evento de forma idempotente.
Eventos de conexão usam um contrato separado: Webhooks de sessões.
Mídia de visualização única
O recebimento de imagem, vídeo ou áudio de visualização única inclui message_type: "view_once" no próprio objeto de value.messages[]. type continua image, video ou audio; o ID UnoAPI e o objeto da mídia permanecem iguais. É uma extensão ViperConnect, não um evento de edição nem um novo evento posterior. Não adiciona context ou edit_timestamp; contexto de resposta já existente é preservado.
O marcador vem do payload original (imageMessage.viewOnce, equivalentes de vídeo/áudio, ou wrappers viewOnceMessage, viewOnceMessageV2 e viewOnceMessageV2Extension) e é preservado ao desembrulhar. Não há consulta adicional ao Redis, download extra ou reenvio. Mensagens comuns, citações de visualização única e mensagens apenas temporárias não recebem esse marcador. Mídia indisponível mantém o fallback existente; este campo não recupera conteúdo.
{
"id": "ID_UNOAPI_DA_MENSAGEM",
"from": "5566996269251",
"timestamp": "1790672409",
"type": "image",
"message_type": "view_once",
"image": {
"id": "5566936183915/ID_UNOAPI_DA_MENSAGEM",
"mime_type": "image/jpeg",
"url": "https://example.test/media.jpg"
}
}No ViperChat, persistir o marcador junto à mensagem/anexo e informar “Você recebeu uma mensagem de visualização única”. Deduplicar pelo ID normal; não tratar como edição ou aguardar atualização posterior. O marcador não impõe acesso único: a política atual de armazenamento e URLs não é alterada. Evitar prévia automática até a aplicação definir sua política de visualização. Webhooks antigos já entregues não são reenviados automaticamente.
Eco de edição enviada
Ao editar pela API (type: "message_edit"), o eco enviado após sucesso usa type: "text", text.body com o texto corrigido e message_type: "message_edit". context.message_id e context.id apontam para o ID UnoAPI original; id continua sendo o ID do evento de edição. edit_timestamp informa o horário de processamento em milissegundos.
O ViperChat recebe esse objeto em value.message_echoes, no campo smb_message_echoes; destinos comuns o recebem em value.messages. Isso permite atualizar e marcar o original como editado, sem criar outra mensagem. A configuração sendNewMessages continua controlando esses ecos. Eventos antigos já enfileirados no consumidor não são reparados automaticamente.
Blacklist e isolamento de histórico
POST /{phone}/blacklist/{webhook_id} aceita telefone, @s.whatsapp.net, @lid e grupo @g.us. ttl é em segundos: positivo expira, negativo persiste, zero ou omitido remove. Telefone e LID equivalem quando vinculados no store da mesma sessão; grupos não se confundem com seus participantes. Sem vínculo conhecido, só a identidade informada pode ser tratada. Chaves antigas continuam válidas, consultadas nos eventos novos, sem recadastro. A resposta confirma enfileiramento em instalações AMQP; mensagens já entregues não podem ser desfeitas.
Histórico usa filas próprias de processamento, webhook e transcrição, com dois consumidores por processo/etapa. Filtros e payloads não mudam; mensagens novas podem ultrapassar histórico. Recursos como CPU, rede e Redis continuam compartilhados. Publicações AMQP aguardam confirmação antes do ACK de consumo; isso não significa entrega exatamente uma vez, nem confirmação do destinatário WhatsApp. Uma confirmação perdida pode causar repetição. Áudio e comandos VoIP não usam esse caminho de publicação.
Mensagem de texto recebida
{
"object": "whatsapp_business_account",
"entry": [{
"id": "5511999999999",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "5511999999999",
"phone_number_id": "5511999999999"
},
"contacts": [{
"profile": {"name": "Maria"},
"wa_id": "5511912008012",
"user_id": "273877414502425@lid"
}],
"messages": [{
"from": "5511912008012",
"from_user_id": "273877414502425@lid",
"id": "wamid.HBgMNTUxMTkxMjAwODAxMhUCABIY...",
"timestamp": "1785100000",
"type": "text",
"text": {"body": "Olá"}
}],
"statuses": []
}
}]
}]
}user_id é a identidade estável quando presente. Telefone e nome são metadados complementares.
Resposta de botão
{
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": {"id": "continuar", "title": "Continuar"}
},
"context": {"id": "ID_DA_MENSAGEM_INTERATIVA"}
}Resposta de lista
{
"type": "interactive",
"interactive": {
"type": "list_reply",
"list_reply": {"id": "pro", "title": "Profissional", "description": "Plano completo"}
}
}Status de entrega
{
"statuses": [{
"id": "wamid.HBgMNTUxMTkxMjAwODAxMhUCABIY...",
"status": "delivered",
"timestamp": "1785100004",
"recipient_id": "5511912008012"
}],
"messages": []
}Estados usuais: sent, delivered, read, played e failed.
Falha
{
"statuses": [{
"id": "ID_DA_MENSAGEM",
"status": "failed",
"recipient_id": "5511912008012",
"errors": [{
"code": 131000,
"title": "Falha ao enviar",
"message": "Descrição normalizada do erro"
}]
}]
}Mensagem indisponível
Quando o conteúdo original não pode ser recuperado, o webhook entrega uma mensagem de texto explícita:
{"type":"text","text":{"body":"Mensagem indisponível nesta integração. Confira o aparelho."}}Segurança e entrega
- Use HTTPS e valide o token no cabeçalho configurado.
- Retorne
2xxantes de executar tarefas demoradas. - Deduplicate por
messages[].idoustatuses[].id. - Com
WEBHOOK_ASYNC_MODE=amqp, falhas transitórias permanecem na fila. - Configure circuit breaker e timeout abaixo do timeout global do consumidor.
Transcrição de áudio: somente provedores externos
O webhook com sendTranscribeAudio: true solicita uma transcrição adicional ao áudio original. A transcrição local com Whisper tiny está desabilitada, inclusive como fallback. O modelo não é incluído na imagem e não é baixado ou executado pelo job.
- Com Groq configurada, ela é o provedor preferido.
- Se Groq falhar, OpenAI é usada somente quando sua chave também estiver configurada.
- Com apenas OpenAI configurada, o job usa OpenAI, sem fallback local em caso de falha.
- Sem chave Groq ou OpenAI, o job termina antes de baixar o áudio e registra
TRANSCRIPTION_SKIPPED_NO_PROVIDERem debug. Não envia texto substituto ou vazio. - Resultado vazio não gera webhook de transcrição; registra
TRANSCRIPTION_EMPTY_RESULT.
O envio do áudio original continua independente da transcrição. Configurar sendTranscribeAudio não é suficiente: a sessão precisa ter um provedor externo. Não há carga de inferência local; download do áudio, buffers e envio ao provedor ainda consomem memória/rede. A cobrança do provedor externo segue o respectivo serviço.
Esta mudança não altera concorrência, filas, reconexão ou configurações persistidas. Falhas continuam sendo registradas pelo tratamento atual do job, sem introduzir recuperação automática/dead-letter. É necessário publicar o código para que a mudança entre em vigor em instalações existentes.
Respostas a transcrições e avisos de envio
Referência do áudio
A transcrição é uma mensagem sintética, não uma mensagem criada no WhatsApp. Seu ID novo tem o prefixo uno-transcription:v2: e um hash determinístico da sessão e do ID UnoAPI do áudio original. Grupos incluem também o JID do grupo; conversas individuais não incluem o alias PN/LID/username, que pode mudar entre eventos. A validação da conversa continua sendo feita pela chave original ao resolver a citação. Referências do primeiro formato continuam legíveis. Reprocessar o mesmo áudio com esses identificadores não gera um UUID novo. Isso permite deduplicar a transcrição no consumidor; não impede uma nova chamada ao serviço externo de transcrição.
Antes de publicar a transcrição, a UnoAPI grava a associação no Redis em unoapi-transcription-reference:<sessão>:<id-da-transcrição>. Não há TTL. Falha nessa gravação impede publicar a transcrição e é registrada no log; o áudio original, já encaminhado, não é cancelado. As associações são históricas: permanecem após desconexão, reconexão ou remoção da configuração da sessão. Uma exclusão definitiva desses dados exige limpeza administrativa explícita. Não se guardam texto, áudio ou credenciais nessa associação.
Ao responder, a UnoAPI resolve a associação e usa a chave do áudio original somente se pertencer à conversa de destino. Uma referência ausente, incluindo UUIDs antigos sem associação, permite enviar o conteúdo sem citação. Uma chave de outra conversa nunca muda o destinatário do envio.
Falhas de acesso ao store/Redis não são confundidas com ausência de referência. Edição, reação, exclusão, voto e referência de pedido continuam exigindo seus alvos válidos. A tradução de erros não muda essas regras nem os retries.
Contrato para o ViperChat
O HTTP de envio assíncrono confirma a entrada na fila, não a entrega. O aviso é emitido posteriormente em entry[].changes[].value.statuses[].warnings, no webhook de atualização de mensagens (sendUpdateMessages=true):
{
"id": "ID_UNOAPI_DEVOLVIDO_NO_ENVIO",
"recipient_id": "5511999999999",
"status": "sent",
"timestamp": "1789840000",
"warnings": [
{
"code": "REPLY_SENT_WITHOUT_QUOTE",
"message": "Mensagem enviada sem citação: não foi possível localizar a referência original nesta conversa."
}
]
}O exemplo representa um item de statuses, não o envelope inteiro. Se a UnoAPI já conhece um estado mais avançado, o aviso acompanha esse estado em vez de regredir para sent. Consumidores devem correlacionar pelo id, manter a progressão de status e mesclar avisos mesmo quando o status já foi processado.
Recuperação da publicação do aviso
Em envios enfileirados, o aviso é gravado em unoapi-reply-warning:<sessão>:<id-da-mensagem> antes de chamar o WhatsApp. Sem conseguir persistir essa pendência, o envio não começa. O registro contém destinatário, timestamp e aviso, sem conteúdo da mensagem ou credenciais.
Com outgoingIdempotency=true (padrão), se o envio já tiver chave/status confirmado e a publicação do status falhar, a tentativa seguinte publica somente o aviso pendente. Não chama novamente o envio ao WhatsApp. O registro só é removido após a publicação no broker e o encaminhamento aos webhooks concluírem; falhas nessa recuperação não entram no caminho de envio novamente.
A entrega do aviso é pelo menos uma vez: o consumidor deve deduplicá-lo. Se uma nova tentativa resolver uma citação antes ausente, o aviso obsoleto é removido antes do envio com citação. Falhas definitivas notificadas também descartam a pendência, sem emitir um aviso de envio bem-sucedido.
Não há varredor independente nem recuperação automática da dead letter queue. Ao esgotar retries, a pendência permanece para diagnóstico/reprocessamento controlado. A recuperação depende da chave/status do envio ainda estar disponível; não é uma garantia geral de exactly-once do WhatsApp. Não desative outgoingIdempotency se precisar dessa proteção.
O ViperChat deve persistir o aviso nos atributos da mensagem, mostrar um indicador/tooltip e não marcar falha nem repetir o envio. Deduplicar avisos por ID da mensagem e código; notificações repetidas não devem gerar novos alertas. Eventos posteriores sem warnings não apagam avisos anteriores. Não renderizar message como HTML. Não presumir que a citação exibida na interface foi enviada ao WhatsApp quando esse aviso existir. Sem suporte no ViperChat, o envio funciona, mas o indicador visual não aparece automaticamente.
Idioma dos erros
UNOAPI_API_LANGUAGE tem padrão pt-BR. Use UNOAPI_API_LANGUAGE=en para inglês. Configure o mesmo valor no web e nos workers; reinicie/recrie os processos após mudar o ambiente. A variável é independente do idioma do painel.
Erros JSON HTTP e falhas de envio Zapo são traduzidos na saída pública, sem traduzir identificadores, códigos numéricos ou alterar as exceções internas. Erros conhecidos têm explicações específicas. Erros ainda não catalogados recebem uma mensagem genérica no idioma escolhido; o diagnóstico original permanece nos logs. Falhas de envio preservam o motivo simbólico em error_data.reason, quando disponível. Erros HTTP simbólicos preservam error_code ao lado da mensagem traduzida (na raiz ou no objeto error, conforme o envelope). O painel lê esse código e mantém compatibilidade com o formato legado. Integrações não devem tomar decisões comparando title ou message.
Exemplo de erro HTTP:
{
"error_code": "contact_directory_requires_zapo_provider",
"error": "O diretório de contatos está disponível apenas para sessões Zapo."
}zapo_phone_lid_not_found indica que o destinatário não foi localizado: continua sendo uma falha, não um envio sem citação. A mensagem apresentada é: “Não foi possível localizar o identificador WhatsApp do destinatário. Verifique o número e tente novamente.” Rejeições 479 indicam recusa pelo WhatsApp; o texto traduzido não atribui uma causa não confirmada.
Validação e publicação
Testar associação persistida, referência ausente, UUID antigo, isolamento de sessão/conversa, resposta com mídia, falha do Redis e erro real do provider. Validar também webhook com status já entregue/lido e ambos os idiomas. Publicar código novo não traduz retroativamente falhas já salvas no ViperChat.