Skip to content

Integração Z-API

Contrato HTTP do Z-API: payload do webhook, envio de mensagens e mapeamento para o domínio.


Visão Geral

O Z-API (z-api.io) é o gateway entre o WhatsApp e nosso Lambda. Ele:

  1. Recebe mensagens da instância de WhatsApp e envia para POST /webhook do nosso API Gateway
  2. Envia mensagens via API REST (POST /send-text) quando chamamos o ZapiClient

A instância Z-API é configurada no painel próprio do serviço (não em CDK). As três credenciais ficam em variáveis de ambiente do Lambda: ZAPI_INSTANCE_ID, ZAPI_TOKEN, ZAPI_CLIENT_TOKEN.


Webhook de Entrada

Payload (ZapiWebhookPayload)

Arquivo: src/adapters/zapi/zapi.types.ts

typescript
interface ZapiWebhookPayload {
  messageId?: string
  phone?: string
  fromMe?: boolean
  type?: string                       // 'text' | 'image' | 'audio' | 'video' | 'document' | outros
  momment?: number                    // unix timestamp (s ou ms)
  text?: { message?: string }
  image?: { caption?: string; imageUrl?: string }
  audio?: { audioUrl?: string }
  video?: { caption?: string; videoUrl?: string }
  document?: { documentUrl?: string; fileName?: string }
  [key: string]: unknown              // tolerância a campos extras
}

Campos descritos por documentação do Z-API. Note que momment (com dois M) é grafia oficial deles, preservamos a tipagem para evitar surpresa.

Mapper para o Domínio

Arquivo: src/adapters/zapi/zapi.mapper.ts

O mapper traduz o payload externo em IncomingMessage interno:

typescript
interface IncomingMessage {
  messageId: string
  phone: string                       // somente dígitos (normalizado)
  fromMe: boolean
  type: 'text' | 'image' | 'audio' | 'video' | 'document' | 'unknown'
  text: string | null                 // text.message → image.caption → video.caption → null
  receivedAt: Date
  raw: Record<string, unknown>        // payload original (debug/audit)
}

Regras do mapper:

RegraComportamento
Sem messageId ou sem phoneDevolve null handler ignora a mensagem
phone com máscaraStrip de tudo que não é dígito (via normalizePhone)
type ausenteVira 'unknown'
type em maiúsculas/mistasNormalizado para lowercase
type fora da lista suportadaVira 'unknown'
Texto presentetext.message (com trim) tem prioridade
Sem texto, mas com imagem/vídeoUsa image.caption ou video.caption (com trim)
Sem texto algumtext = null
momment em segundos (< 1e12)Multiplica por 1000 para virar ms
momment em milissegundos (≥ 1e12)Usa direto
momment ausente/NaN/InfinityreceivedAt = new Date()

Envio de Mensagens

Cliente HTTP

Arquivo: src/adapters/zapi/zapi.client.ts

typescript
new ZapiClient({
  instanceId: process.env.ZAPI_INSTANCE_ID,
  token: process.env.ZAPI_TOKEN,
  clientToken: process.env.ZAPI_CLIENT_TOKEN,
  baseUrl: process.env.ZAPI_BASE_URL ?? 'https://api.z-api.io'
})

Configuração do axios.create:

PropriedadeValor
baseURL{baseUrl}/instances/{instanceId}/token/{token}
timeout10_000 ms
Header Content-Typeapplication/json
Header Client-Token{clientToken} (omitido se vazio)

Método sendText

typescript
zapi.sendText({ phone: '5511999998888', message: 'olá' })

Endpoint chamado: POST /send-text

Body enviado:

json
{ "phone": "5511999998888", "message": "olá" }

Response esperada do Z-API:

json
{ "zaapId": "...", "messageId": "..." }

Em caso de falha (network, 5xx, timeout), o client converte em BotError 502 com mensagem genérica "Falha ao enviar mensagem via Z-API": detalhes do upstream não vazam para o usuário final. O detalhe real fica nos logs (Logger.warn).


Configuração no Painel Z-API

Após o deploy, configure o webhook no painel do Z-API apontando para o output BotApiUrl da stack CDK:

https://{api-id}.execute-api.us-east-2.amazonaws.com/webhook

Tipo do webhook: "ao receber mensagem" (não habilitar webhooks de status/delivery, não consumimos esses eventos).


Documento atualizado em Maio 2026