DevZap · Guias de integração Voltar ao painel

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.


← Todos os guias