Skip to main content
Trabalhe com os webhooks do Loce Zap com segurança: valide assinatura, saiba o que cada evento envia e quais limites se aplicam em cada plano.

Visão geral

  • Webhooks chegam sempre assinados com sua própria apiKey. Valide a assinatura antes de processar.
  • Use express.raw somente na rota de webhook para preservar o corpo bruto.
  • Planos free recebem apenas SESSION-CONNECTED e SESSION-DISCONNECTED. Eventos MESSAGE-* são exclusivos de planos pagos com webhookMessages = true.
  • As tipagens WebhookEvent/WebhookPayloadMap entregam autocomplete imediato: explore event.payload na IDE para descobrir os campos mais recentes.

Exemplo de implementação (Express)

import express from "express";
import { LoceZap } from "loce-zap-sdk";

const app = express();
const zap = new LoceZap({ apiKey: process.env.LOCE_ZAP_API_KEY! });

app.post(
  "/webhooks/loce-zap",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body;

    if (!zap.webhooks.verifySignature({ headers: req.headers as any, rawBody })) {
      return res.status(401).json({ error: "invalid signature" });
    }

    const event = zap.webhooks.parseEvent(rawBody);

    switch (event.type) {
      case "SESSION-CONNECTED":
        console.log(`Sessão pronta: ${event.payload.name}`);
        break;
      case "MESSAGE-RECEIVED":
        console.log(
          `Msg de ${event.payload.sender.phone}:`,
          event.payload.message?.text
        );
        break;
      case "MESSAGE-SENT":
        console.log(`Mensagem ${event.payload.messageId} enviada`);
        console.log(
          `Recomendado: Armazenar o ${event.payload.key} para futuras identificações em webhooks`
        );
        break;
    }

    res.status(200).json({ received: true });
  }
);

Exemplos rápidos de cada evento

Todos seguem o mesmo envelope { apiKey, sessionId, type, payload } enviado pelo backend:

SESSION-CONNECTED

{ "apiKey": "123", "sessionId": "sessao-1", "type": "SESSION-CONNECTED", "payload": { "id": "sessao-1", "name": "Minha sessão", "phone": "5564999999999" } }

SESSION-DISCONNECTED

{ "apiKey": "123", "sessionId": "sessao-1", "type": "SESSION-DISCONNECTED", "payload": { "id": "sessao-1", "name": "Minha sessão" } }

MESSAGE-RECEIVED

Inclui respostas de botão/lista, edições/deleções e reações já normalizadas.
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-RECEIVED",
  "payload": {
    "key": "3A...",
    "fromMe": false,
    "sender": { "phone": "5564999999999", "name": "Contato", "profileImage": null },
    "type": "conversation",
    "eventType": "MESSAGE-RECEIVED",
    "referenceKey": null,
    "message": "Olá",
    "timestamp": "2024-01-01T12:00:00.000Z",
    "isBroadcast": false,
    "historic": false,
    "responseButtonId": null
  }
}

Importante sobre key

No evento MESSAGE-SENT, o payload.key é o ID principal da mensagem (string). Guarde esse valor: ele aparece como key/referenceKey nos eventos seguintes (MESSAGE-DELIVERED/READ/DELETED/EDITED/REACTION) e permite correlacionar reações, edições e deleções com a mensagem original.

MESSAGE-SENT

{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-SENT",
  "payload": {
    "messageId": "665f...",
    "key": "BAE...",
    "timestamp": "2024-01-01T12:00:00.000Z"
  }
}

MESSAGE-DELIVERED / MESSAGE-READ

{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-DELIVERED",
  "payload": {
    "key": "BAE...",
    "fromMe": true,
    "timestamp": "2024-01-01T12:01:00.000Z"
  }
}

MESSAGE-DELETED

Eventos normalizados.
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-DELETED",
  "payload": {
    "key": "BAE...",
    "fromMe": true,
    "eventType": "MESSAGE-DELETED",
    "referenceKey": "BAE...",
    "timestamp": "2024-01-01T12:02:00.000Z"
  }
}

MESSAGE-EDITED

{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-EDITED",
  "payload": {
    "key": "BAE...",
    "fromMe": true,
    "eventType": "MESSAGE-EDITED",
    "referenceKey": "BAE...",
    "message": "Nova mensagem",
    "timestamp": "2024-01-01T12:02:00.000Z"
  }
}

MESSAGE-DISCARDED

{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-DISCARDED",
  "payload": {
    "messageId": "65b3...",
    "lastError": "Não foi possível acessar a URL de mídia, verifique se não expirou."
  }
}

REACTION-MESSAGE

Normalizado como NormalizedWebhookMessage.
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "REACTION-MESSAGE",
  "payload": {
    "key": "3A...",
    "fromMe": true,
    "eventType": "REACTION-MESSAGE",
    "referenceKey": "BAE...",
    "message": "👍",
    "timestamp": "2024-01-01T12:03:00.000Z"
  }
}

Payloads por evento

EventoPayload
SESSION-CONNECTED{ id, name, phone }
SESSION-DISCONNECTED{ id, name }
MESSAGE-RECEIVEDNormalizedWebhookMessage (key/fromMe/sender, message ou mediaUrl/filename, type, eventType/referenceKey, timestamp)
MESSAGE-SENT{ messageId, key, timestamp }
MESSAGE-DELIVERED{ key, fromMe, timestamp }
MESSAGE-READ{ key, fromMe, timestamp }
MESSAGE-DISCARDED{ messageId, lastError }
MESSAGE-EDITEDNormalizedWebhookMessage (eventType=MESSAGE-EDITED, referenceKey, conteúdo editado)
MESSAGE-DELETEDNormalizedWebhookMessage (eventType=MESSAGE-DELETED, referenceKey)
REACTION-MESSAGENormalizedWebhookMessage (eventType=REACTION-MESSAGE, referenceKey, reaction text)
NormalizedWebhookMessage resume: key, fromMe, sender (phone/name/profileImage), type, eventType, referenceKey, message ou mediaUrl/filename/latitude/longitude/interact, timestamp, isBroadcast, historic, responseButtonId, etc.

Notas adicionais

  • URLs de mídia (payload.mediaUrl) expiram em até 24h. Baixe imediatamente se precisar armazenar.
  • Para testes locais você pode gerar o cabeçalho com zap.webhooks.signPayload(rawBody) e enviar x-locezap-signature/x-locezap-timestamp manualmente.