Implementar multi-atendentes no WhatsApp via API é essencial quando sua empresa recebe mais mensagens do que uma pessoa consegue responder. Com a ZAP API, você constrói um sistema de fila de atendimento, distribuição round-robin e transferência entre atendentes — tudo usando uma única instância WhatsApp.
Arquitetura do sistema
A ideia é simples: todas as mensagens chegam via webhook no seu servidor. Seu sistema distribui cada conversa para um atendente disponível, mantém o estado da conversa e permite transferência entre atendentes.
- Webhook: recebe todas as mensagens
- Router: distribui para o atendente correto
- Fila: quando todos estão ocupados, cliente entra na fila
- Painel: atendentes veem suas conversas e respondem
Implementação: sistema de fila e distribuição
const axios = require('axios');
const express = require('express');
const app = express();
app.use(express.json());
const api = axios.create({
baseURL: 'https://zap-api.tech/v1',
headers: { Authorization: 'Bearer tk_seu_token_aqui' }
});
const INST = 'inst_seu_id_aqui';
// Estado dos atendentes
const atendentes = [
{ id: 1, nome: 'Ana', status: 'disponivel', conversas: [] },
{ id: 2, nome: 'Bruno', status: 'disponivel', conversas: [] },
{ id: 3, nome: 'Carol', status: 'disponivel', conversas: [] }
];
// Conversas ativas: phone → atendenteId
const conversasAtivas = new Map();
const fila = [];
// Distribuição round-robin
let ultimoAtendente = -1;
function proximoAtendente() {
const disponiveis = atendentes.filter(a => a.status === 'disponivel' && a.conversas.length < 5);
if (disponiveis.length === 0) return null;
ultimoAtendente = (ultimoAtendente + 1) % disponiveis.length;
return disponiveis[ultimoAtendente];
}
// Webhook: receber mensagens
app.post('/webhook', async (req, res) => {
res.json({ ok: true });
const { event, data } = req.body;
if (event !== 'message.received' || data.fromMe) return;
const phone = data.phone;
const texto = data.body || '';
// Verificar se já tem conversa ativa
if (conversasAtivas.has(phone)) {
const atendenteId = conversasAtivas.get(phone);
// Encaminhar para o painel do atendente
notificarAtendente(atendenteId, phone, texto);
return;
}
// Distribuir para próximo atendente
const atendente = proximoAtendente();
if (atendente) {
conversasAtivas.set(phone, atendente.id);
atendente.conversas.push(phone);
notificarAtendente(atendente.id, phone, texto);
await api.post(`/instances/${INST}/send`, {
phone, type: 'text',
body: `Olá! Você está sendo atendido por ${atendente.nome}. Como posso ajudar?`
});
} else {
fila.push({ phone, texto, timestamp: Date.now() });
const posicao = fila.length;
await api.post(`/instances/${INST}/send`, {
phone, type: 'text',
body: `Todos os atendentes estão ocupados. Posição na fila: ${posicao}.\nAguarde, responderemos em breve!`
});
}
});
// API para atendente responder
app.post('/api/responder', async (req, res) => {
const { atendenteId, phone, mensagem } = req.body;
if (conversasAtivas.get(phone) !== atendenteId) {
return res.status(403).json({ error: 'Conversa não atribuída a este atendente' });
}
await api.post(`/instances/${INST}/send`, {
phone, type: 'text', body: mensagem
});
res.json({ ok: true });
});
// API para transferir conversa
app.post('/api/transferir', async (req, res) => {
const { phone, deAtendenteId, paraAtendenteId } = req.body;
const de = atendentes.find(a => a.id === deAtendenteId);
const para = atendentes.find(a => a.id === paraAtendenteId);
de.conversas = de.conversas.filter(p => p !== phone);
para.conversas.push(phone);
conversasAtivas.set(phone, paraAtendenteId);
await api.post(`/instances/${INST}/send`, {
phone, type: 'text',
body: `Você foi transferido para ${para.nome}. Como posso ajudar?`
});
res.json({ ok: true });
});
// API para encerrar conversa
app.post('/api/encerrar', async (req, res) => {
const { atendenteId, phone } = req.body;
const atendente = atendentes.find(a => a.id === atendenteId);
atendente.conversas = atendente.conversas.filter(p => p !== phone);
conversasAtivas.delete(phone);
await api.post(`/instances/${INST}/send`, {
phone, type: 'text',
body: 'Atendimento encerrado. Obrigado pelo contato! Se precisar, mande mensagem novamente.'
});
// Atender próximo da fila
if (fila.length > 0) {
const proximo = fila.shift();
conversasAtivas.set(proximo.phone, atendente.id);
atendente.conversas.push(proximo.phone);
notificarAtendente(atendente.id, proximo.phone, proximo.texto);
}
res.json({ ok: true });
});
function notificarAtendente(atendenteId, phone, texto) {
// Em produção: WebSocket, SSE ou push notification para o painel
console.log(`[Atendente ${atendenteId}] ${phone}: ${texto}`);
}
app.listen(3000);
Distribuição por departamento
// Roteamento por palavra-chave para departamento
function detectarDepartamento(texto) {
const t = texto.toLowerCase();
if (t.match(/comprar|preco|orcamento|produto/)) return 'vendas';
if (t.match(/problema|erro|defeito|trocar|devolver/)) return 'suporte';
if (t.match(/boleto|pagamento|nota|fatura/)) return 'financeiro';
return 'geral';
}
// No webhook, distribuir para atendente do departamento correto
const depto = detectarDepartamento(texto);
const atendente = atendentes.find(a =>
a.departamento === depto && a.status === 'disponivel' && a.conversas.length < 5
);
Métricas de atendimento
- Tempo médio de espera: quanto tempo o cliente ficou na fila
- Tempo de primeira resposta: do recebimento à primeira resposta do atendente
- Tempo de resolução: do início ao encerramento da conversa
- Conversas por atendente: distribuição de carga
- CSAT: pesquisa de satisfação automática ao encerrar
FAQ
- Preciso de uma instância por atendente?
Não. Todos os atendentes usam a mesma instância. O roteamento é feito no seu backend. - Quantos atendentes simultâneos?
Sem limite. O gargalo é o volume de mensagens por minuto, não o número de atendentes. - Como o atendente vê as mensagens?
Crie um painel web que consome os webhooks e usa a API /send para responder. Pode ser React, Vue ou qualquer framework. - Posso integrar com Zendesk/Freshdesk?
Sim. Use o webhook da ZAP API para criar tickets automaticamente no seu help desk.