Official TypeScript SDK for the Loce Zap WhatsApp API
npm install loce-zapSDK 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.
``bash`
npm install loce-zapou
yarn add loce-zap
- Sessões – cada WhatsApp conectado. Você pode conectar (connect), desconectar, atualizar dados e listar todos.SESSION-
- Mensagens – a SDK só dispara os endpoints; o backend coloca em fila, simula digitação e envia via Baileys.
- Webhooks – eventos /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).
`ts
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();
`
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:
`ts
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);
`
`ts
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' },
],
},
],
});
`
`ts
await zap.deleteMessage('my-session-id', {
messageId: '65b3...',
});
await zap.editMessage('my-session-id', {
messageId: '65b3...',
text: 'Mensagem atualizada',
});
`
- Todos os eventos chegam assinados com sua própria apiKey. Use express.raw apenas nessa rota para preservar o rawBody.SESSION-CONNECTED
- Planos free recebem apenas e SESSION-DISCONNECTED. Eventos MESSAGE-* são exclusivos de planos pagos com webhookMessages = true.WebhookEvent
- As tipagens /WebhookPayloadMap te dão autocomplete imediato: explore event.payload na IDE para ver os campos mais recentes.
`ts
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});Msg de ${event.payload.sender.phone}:
break;
case 'MESSAGE-RECEIVED':
console.log(
,Mensagem ${event.payload.messageId} enviada
event.payload.message?.text
);
break;
case 'MESSAGE-SENT':
console.log();Recomendado: Armazenar o ${event.payload.key} para futuras identificações em webhooks
console.log();
break;
}
res.status(200).json({ received: true });
}
);
`
(mesmo formato enviado pelo backend):-
SESSION-CONNECTED
`json
{ "apiKey": "123", "sessionId": "sessao-1", "type": "SESSION-CONNECTED", "payload": { "id": "sessao-1", "name": "Minha sessão", "phone": "5564999999999" } }
`-
SESSION-DISCONNECTED
`json
{ "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)
`json
{
"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
`json
{
"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
`json
{
"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)
`json
{
"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)
`json
{
"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
`json
{
"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)
`json
{
"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",
...
}
}
`$3
| 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:`ts
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
`ts
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
`ts
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);
}
}
``MIT © Loce Zap