npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

loce-zap

v0.2.5

Published

Official TypeScript SDK for the Loce Zap WhatsApp API

Readme

Loce Zap SDK

SDK oficial em TypeScript para integrar com a API multi-sessão do Loce Zap. Esta documentação resume o fluxo completo (instalação → mensagens → webhooks) para quem busca a referência direto no npm.

Instalação

npm install loce-zap
# ou
yarn add loce-zap

Conceitos rápidos

  • Sessões – cada WhatsApp conectado. Você pode conectar (connect), desconectar, atualizar dados e listar todos.
  • Mensagens – a SDK só dispara os endpoints; o backend coloca em fila, simula digitação e envia via Baileys.
  • Webhooks – eventos SESSION-*/MESSAGE-* são assinados com HMAC usando a própria apiKey. zap.webhooks valida e parseia o payload. Mensagens falham até 10 vezes; ao atingir o limite viram discarded e disparam webhook MESSAGE-DISCARDED com o último erro. Também há eventos de entrega/leitura/edição/remoção/reação (MESSAGE-DELIVERED, MESSAGE-READ, MESSAGE-EDITED, MESSAGE-DELETED, REACTION-MESSAGE).

Uso rápido

import { LoceZap } from 'loce-zap-sdk';

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

// Conecta (modo padrão = QR Code)
const connectResponse = await zap.connect({
  sessionName: 'Whatsapp do Zé',
  webhookUrl: 'https://example.com/webhook',
  webhookMessages: true, //Evento de mensagens (não disponível no plano free)
});

if (connectResponse.status === 'waiting_for_pairing') {
  console.log('Código de pareamento:', connectResponse.pairingCode);
} else {
  console.log('QR Code gerado (base64):', connectResponse.qrCode);
}

const { sessionId } = connectResponse;

await zap.sendMessageText(sessionId, {
  to: '5564999999999',
  text: 'Olá 👋',
});
// Obs.: os números devem incluir o DDI (formato E.164 sem o '+', 6 a 15 dígitos)

const { sessions } = await zap.listSessions();

Conexão via Pairing Code (sem QR)

Use o mesmo connect, mas envie mode: 'pairing' e o número (com DDI, só dígitos, 11–13 caracteres). O backend retorna status = 'waiting_for_pairing' e pairingCode para você digitar no WhatsApp:

const { pairingCode, sessionId } = await zap.connect({
  sessionName: 'Suporte',
  webhookUrl: 'https://example.com/webhook',
  mode: 'pairing',
  pairingNumber: '5564999999999', // DDI + número (somente dígitos)
});

console.log('Código para pareamento:', pairingCode);

Mensagens com mídia

await zap.sendMessageImage('my-session-id', {
  to: '5564999999999',
  imageUrl: 'https://files.loce.io/promo.png',
  caption: 'Confira o novo recurso!',
});

await zap.sendMessageDocument('my-session-id', {
  to: '5564999999999',
  fileUrl: 'https://files.loce.io/contrato.pdf',
  fileName: 'Contrato.pdf',
});

await zap.sendMessageLocation('my-session-id', {
  to: '5564999999999',
  latitude: -16.7033,
  longitude: -49.263,
});

await zap.sendMessageAudio('my-session-id', {
  to: '5564999999999',
  audioUrl: 'https://files.loce.io/saudacao.ogg',
  ptt: true, // (ptt = true manda como gravado no app; defina false para áudio comum)
});

// Botões rápidos (máx. 5) (Só disponível em planos pagos)
await zap.sendMessageButtons('my-session-id', {
  to: '5564999999999',
  message: 'Escolha uma opção',
  footer: 'Selecione abaixo',
  buttons: [
    { id: 'op1', text: 'Opção 1' },
    { id: 'op2', text: 'Opção 2' },
  ],
});

// Lista interativa (máx. 5 opções no total) (Só disponível em planos pagos)
await zap.sendMessageList('my-session-id', {
  to: '5564999999999',
  text: 'Escolha uma opção',
  buttonText: 'Abrir menu',
  title: 'Menu principal',
  footer: 'Selecione uma das opções',
  sections: [
    {
      title: 'Opções disponíveis',
      rows: [
        { title: 'Ver pedidos', rowId: 'pedidos' },
        { title: 'Ver cobranças', rowId: 'cobrancas' },
      ],
    },
  ],
});

Edição e exclusão

await zap.deleteMessage('my-session-id', {
  messageId: '65b3...',
});

await zap.editMessage('my-session-id', {
  messageId: '65b3...',
  text: 'Mensagem atualizada',
});

Webhooks

  • Todos os eventos chegam assinados com sua própria apiKey. Use express.raw apenas nessa rota para preservar o rawBody.
  • Planos free recebem apenas SESSION-CONNECTED e SESSION-DISCONNECTED. Eventos MESSAGE-* são exclusivos de planos pagos com webhookMessages = true.
  • As tipagens WebhookEvent/WebhookPayloadMap te dão autocomplete imediato: explore event.payload na IDE para ver os campos mais recentes.
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 { apiKey, sessionId, type, payload } (mesmo formato 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, edits/deletes/reactions normalizados)
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-RECEIVED",
  "payload": {
    "key": "3A...", //Use para verificar duplicidade
    "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...", //Salve para futuras identificações
    "timestamp": "2024-01-01T12:00:00.000Z"
  }
}
  • MESSAGE-DELIVERED / MESSAGE-READ
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-DELIVERED",
  "payload": {
    "key": "BAE...", //Use para identificar a mensagem entregue/lida
    "fromMe": true,
    "timestamp": "2024-01-01T12:01:00.000Z"
  }
}
  • MESSAGE-DELETED (normalizados)
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-DELETED",
  "payload": {
    "key": "BAE...",
    "fromMe": true,
    "eventType": "MESSAGE-DELETED",
    "referenceKey": "BAE...", //Use para identificar a mensagem apagada
    "timestamp": "2024-01-01T12:02:00.000Z",
    ...
  }
}
  • MESSAGE-EDITED (normalizado)
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "MESSAGE-EDITED",
  "payload": {
    "key": "BAE...",
    "fromMe": true,
    "eventType": "MESSAGE-EDITED",
    "referenceKey": "BAE...", //Use para identificar a mensagem editada
    "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 (também NormalizedWebhookMessage)
{
  "apiKey": "123",
  "sessionId": "sessao-1",
  "type": "REACTION-MESSAGE",
  "payload": {
    "key": "3A...",
    "fromMe": true,
    "eventType": "REACTION-MESSAGE",
    "referenceKey": "BAE...", //Use para identificar a mensagem reagida
    "message": "👍", //Reação (se null, é remoção)
    "timestamp": "2024-01-01T12:03:00.000Z",
    ...
  }
}

Payloads

| Evento | Payload | |-----------------------|------------------------------------------------------------------------------------------------------------------------| | SESSION-CONNECTED | { id, name, phone } | | SESSION-DISCONNECTED| { id, name } | | MESSAGE-RECEIVED | NormalizedWebhookMessage (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-EDITED | NormalizedWebhookMessage (eventType=MESSAGE-EDITED, referenceKey, conteúdo editado) | | MESSAGE-DELETED | NormalizedWebhookMessage (eventType=MESSAGE-DELETED, referenceKey) | | REACTION-MESSAGE | NormalizedWebhookMessage (eventType=REACTION-MESSAGE, referenceKey, reaction text) |

NormalizedWebhookMessage resumo: key, fromMe, sender (phone/name/profileImage), type, eventType, referenceKey, message ou mediaUrl/filename/latitude/longitude/interact, timestamp, isBroadcast, historic, responseButtonId, etc.

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.

Erros

Todas as requisições passam por tratamento único. Em caso de falha o SDK lança LoceZapAPIError:

import { isLoceZapAPIError } from 'loce-zap-sdk';

try {
  await zap.sendMessageText('session-id', { to: '5564...', text: 'Oi' });
} catch (error) {
  if (isLoceZapAPIError(error)) {
    console.error(error.status, error.body);
  }
}

API HTTP com Express

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

const app = express();
app.use(express.json());

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

app.get('/sessions', async (_req, res) => {
  try {
    const sessions = await zap.listSessions();
    res.status(200).json(sessions);
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  }
});

app.post('/sessions/:id/message', async (req, res) => {
  try {
    const result = await zap.sendMessageText(req.params.id, {
      to: req.body.to,
      text: req.body.text,
    });
    res.status(200).json(result);
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  }
});

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

Tratamento de erros

import { LoceZap, isLoceZapAPIError } from 'loce-zap-sdk';

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

try {
  await zap.sendMessageText('session-id', { to: '5564...', text: 'Oi' });
} catch (error) {
  if (isLoceZapAPIError(error)) {
    console.error(error.status, error.body);
  }
}

Licença

MIT © Loce Zap