Receitas
Soluções prontas para tarefas que aparecem em quase toda integração.
Provisionar um canal do zero
Criar a instância, registrar o webhook e conectar, tudo com uma chave de API.
const BASE = 'https://apidevzap.com/api/v1';
const headers = {
Authorization: `Bearer ${process.env.DEVZAP_KEY}`,
'Content-Type': 'application/json',
};
async function provisionar(nome, urlWebhook) {
// Instância e webhook na mesma chamada: se o webhook falhar, nada é criado
const criacao = await fetch(`${BASE}/instances`, {
method: 'POST',
headers,
body: JSON.stringify({ name: nome, webhook: { url: urlWebhook } }),
}).then((r) => r.json());
if (!criacao.success) throw new Error(criacao.error.code);
const instanceId = criacao.data.id;
// Aparece só aqui, persista antes de seguir
const secret = criacao.data.webhook?.secret;
return { instanceId, secret };
}
Depois é só rodar o laço de conexão de Primeiros passos, mostrando o QR ao usuário.
Reconectar uma instância que caiu
async function garantirConectada(instanceId) {
const { data } = await fetch(`${BASE}/instances/${instanceId}`, { headers })
.then((r) => r.json());
if (data.status === 'connected') return true;
// /qr dispara a conexão. Se a sessão no disco ainda vale, ela volta sem QR novo.
const qr = await fetch(`${BASE}/instances/${instanceId}/qr`, { headers })
.then((r) => r.json());
if (qr.data.status === 'connected') return true;
if (qr.data.status === 'qr_code') {
// A sessão expirou de vez: só um humano resolve
notificarOperacao(instanceId, qr.data.qrCode);
return false;
}
return false; // 'initializing', tente de novo daqui a pouco
}
Vale rodar isso periodicamente e alertar quando devolver false com QR: significa que alguém
precisa escanear.
Receber mensagens com deduplicação
const crypto = require('crypto');
app.use('/webhooks/whatsapp', express.raw({ type: 'application/json' }));
app.post('/webhooks/whatsapp', async (req, res) => {
const esperada = crypto
.createHmac('sha256', process.env.DEVZAP_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const recebida = Buffer.from(req.headers['x-webhook-signature'] || '', 'utf8');
const calculada = Buffer.from(esperada, 'utf8');
if (recebida.length !== calculada.length || !crypto.timingSafeEqual(recebida, calculada)) {
return res.status(401).end();
}
const evento = JSON.parse(req.body.toString('utf8'));
// Responda antes de processar: o timeout é de 10s
res.status(200).end();
// A mesma entrega pode repetir, deduplique
const jaVisto = await redis.set(
`msg:${evento.data.messageId}`, '1', 'NX', 'EX', 86400
);
if (!jaVisto) return;
await filaDeProcessamento.add(evento);
});
Baixar a mídia de uma mensagem recebida
async function baixarMidia(instanceId, messageId) {
const r = await fetch(
`${BASE}/instances/${instanceId}/messages/${messageId}/media`,
{ headers }
).then((r) => r.json());
if (!r.success) {
// MEDIA_NOT_FOUND também significa "expirou no WhatsApp"
throw new Error(r.error.code);
}
return {
buffer: Buffer.from(r.data.data, 'base64'),
mimetype: r.data.mimetype,
filename: r.data.filename,
};
}
Baixe assim que o evento chegar: o WhatsApp expira anexos antigos, e depois disso não há como recuperar.
Migrar do envio direto para a fila
Vale quando você começa a se importar com entrega garantida. Os corpos são diferentes:
function paraFila(payloadDireto, chaveIdempotencia) {
const { to, message, type = 'text', mediaUrl, mediaType, filename, caption } = payloadDireto;
if (type === 'text') {
return { to, message, idempotencyKey: chaveIdempotencia, type: 'text' };
}
// No direto, o tipo do arquivo vem em `type`; na fila, em `mediaType`
return {
to,
idempotencyKey: chaveIdempotencia,
type: 'media',
mediaUrl,
mediaType: mediaType ?? type, // 'image' | 'document' | 'audio' | 'video'
filename,
caption,
};
}
A idempotencyKey precisa vir do seu domínio (pedido-4711-confirmacao), não de um gerador
aleatório, senão a proteção não existe.
Acompanhar mensagens da fila até o desfecho
async function aguardarEnvio(instanceId, chave, { timeoutMs = 60000 } = {}) {
const limite = Date.now() + timeoutMs;
while (Date.now() < limite) {
const { data } = await fetch(
`${BASE}/instances/${instanceId}/messages/queue/${chave}`, { headers }
).then((r) => r.json());
if (data.status === 'sent') return data;
if (data.status === 'failed' || data.status === 'expired') {
throw new Error(data.lastError ?? 'envio falhou');
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error('tempo esgotado, a mensagem segue na fila');
}
Um timeout aqui não significa falha: a fila continua tentando. Só significa que você parou de esperar.
Verificar a saúde da integração
async function diagnostico() {
const { data } = await fetch(`${BASE}/status`, { headers }).then((r) => r.json());
const problemas = [];
if (data.tenant.status !== 'active') problemas.push(`tenant ${data.tenant.status}`);
if (data.instances.connected < data.instances.total) {
problemas.push(`${data.instances.total - data.instances.connected} instância(s) desconectada(s)`);
}
return { ok: problemas.length === 0, problemas };
}
Uma chamada barata que serve de health check: valida a chave, o tenant e o estado das instâncias de uma vez.
Guia verificado em 05/08/2026 contra a API v1.