loce-zap
v0.2.5
Published
Official TypeScript SDK for the Loce Zap WhatsApp API
Maintainers
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-zapConceitos 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ópriaapiKey.zap.webhooksvalida e parseia o payload. Mensagens falham até 10 vezes; ao atingir o limite viramdiscardede disparam webhookMESSAGE-DISCARDEDcom 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. Useexpress.rawapenas nessa rota para preservar orawBody. - Planos free recebem apenas
SESSION-CONNECTEDeSESSION-DISCONNECTED. EventosMESSAGE-*são exclusivos de planos pagos comwebhookMessages = true. - As tipagens
WebhookEvent/WebhookPayloadMapte dão autocomplete imediato: exploreevent.payloadna 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 eventoMESSAGE-SENT, opayload.keyé o ID principal da mensagem (string). Guarde esse valor: ele aparece comokey/referenceKeynos 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émNormalizedWebhookMessage)
{
"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) |
NormalizedWebhookMessageresumo:key,fromMe,sender(phone/name/profileImage),type,eventType,referenceKey,messageoumediaUrl/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
