Aplicativo próprio de delivery custa caro para construir, mais caro ainda para fazer o cliente baixar. Pesquisa de 2025 mostra que 73% dos brasileiros já desinstalaram pelo menos um app de comida nos últimos 6 meses por falta de uso. Em compensação, 99,2% têm WhatsApp aberto na home screen — e o ticket médio de pedidos feitos via chat é 18% maior que via app, porque o cliente conversa, esclarece dúvidas e adiciona itens com menos atrito.
Este artigo é o passo a passo de como transformar seu restaurante em uma operação de delivery completa via WhatsApp: cardápio interativo, confirmação automática, rastreamento em tempo real e avaliação no final, sem o cliente sair do chat.
Por que WhatsApp converte mais que app próprio
Os números brutos comparando os dois canais para um restaurante médio em São Paulo (15-30 pedidos/dia):
- App próprio: 22% taxa de conversão (visitante que pede), CAC de R$28, ticket médio R$54
- WhatsApp: 61% taxa de conversão, CAC de R$4, ticket médio R$67
- Marketplaces (mais caros): 35% conversão mas comissão de 23-30% por pedido
A diferença não é mágica: WhatsApp tira 3 fricções enormes. (1) Cliente não precisa baixar nada. (2) Quando ele pergunta "tem opção sem cebola?" recebe resposta humana ou via bot, não vê só o botão "personalizar". (3) Pagamento PIX direto no chat — sem digitar cartão, sem CAPTCHA, sem cadastro.
Arquitetura do bot de pedidos
O coração da solução é uma máquina de estados em Redis que rastreia em qual etapa do funil cada cliente está. Sem isso, a cada mensagem do cliente seu backend fica perdido.
Estados possíveis
IDLE— cliente apenas mandou um "oi", bot envia menu de boas-vindasBROWSING_MENU— bot mandou cardápio (list message), aguardando escolhaAWAITING_QUANTITY— cliente escolheu item, bot pergunta quantosBUILDING_ORDER— pedido em construção, cliente pode adicionar mais itensCONFIRMING— bot enviou resumo + total, aguarda confirmação (buttons sim/não)AWAITING_ADDRESS— cliente confirmou, falta endereço de entregaAWAITING_PAYMENT— endereço ok, gerou QR Pix, aguarda confirmação webhookTRACKING— pago, em preparo/saiu/entregueRATING— entregue, aguarda nota de 1-5
Persistência em Redis
// Chave: order:{telefone}
// Valor: JSON com state, items, total, address, paymentId, createdAt
import { createClient } from "redis";
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
async function getOrderState(phone) {
const raw = await redis.get(`order:${phone}`);
return raw ? JSON.parse(raw) : { state: "IDLE", items: [], total: 0 };
}
async function setOrderState(phone, state) {
// TTL de 2 horas — pedido abandonado é descartado
await redis.set(`order:${phone}`, JSON.stringify(state), { EX: 7200 });
}
Webhook de mensagem recebida
import express from "express";
import axios from "axios";
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1",
headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});
const app = express();
app.use(express.json());
app.post("/webhook/zap", async (req, res) => {
res.status(200).send("ok"); // ack imediato
const { type, instanceId, message } = req.body;
if (type !== "message.received") return;
const phone = message.from;
const text = (message.body?.text || "").trim().toLowerCase();
const order = await getOrderState(phone);
switch (order.state) {
case "IDLE":
await sendMenu(instanceId, phone);
order.state = "BROWSING_MENU";
break;
case "BROWSING_MENU":
await handleMenuChoice(instanceId, phone, message, order);
break;
case "CONFIRMING":
await handleConfirmation(instanceId, phone, text, order);
break;
// ... demais estados
}
await setOrderState(phone, order);
});
Cardápio como list message
Em vez de mandar o menu como texto comprido (cliente lê metade e desiste), use o tipo list nativo do WhatsApp. Aparece como caixinha clicável, organizado por seção.
async function sendMenu(instanceId, phone) {
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "list",
list: {
title: "🍔 Cardápio Burger House",
description: "Escolha um item para adicionar ao pedido",
buttonText: "Ver cardápio",
sections: [
{
title: "Hambúrgueres",
rows: [
{ id: "BURG001", title: "X-Burger", description: "Pão, hambúrguer 120g, queijo — R$ 22" },
{ id: "BURG002", title: "X-Bacon", description: "Pão, hambúrguer 120g, queijo, bacon — R$ 28" },
{ id: "BURG003", title: "X-Tudo", description: "Tudo incluso, ovo, alface, tomate — R$ 35" },
],
},
{
title: "Bebidas",
rows: [
{ id: "BEB001", title: "Coca-Cola 350ml", description: "R$ 7" },
{ id: "BEB002", title: "Suco de laranja 500ml", description: "R$ 12" },
],
},
],
},
});
}
Quando o cliente clica em uma opção, o webhook recebe message.body.listResponseId com o ID (ex.: "BURG002"). Você consulta o preço e o item no seu banco e adiciona ao carrinho dele.
Confirmação com buttons
Mostra resumo e oferece dois botões — confirmar ou cancelar. Sem texto livre, sem ambiguidade.
async function sendConfirmation(instanceId, phone, order) {
const itemsText = order.items
.map(i => `• ${i.qty}x ${i.name} — R$ ${(i.qty * i.price).toFixed(2)}`)
.join("\n");
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "buttons",
buttons: {
header: "Confirmar pedido",
body: `${itemsText}\n\n*Total: R$ ${order.total.toFixed(2)}*\nEntrega em 30-45 min`,
footer: "Burger House",
buttons: [
{ id: "CONFIRM_YES", title: "✅ Confirmar" },
{ id: "CONFIRM_NO", title: "❌ Cancelar" },
],
},
});
}
Rastreamento em tempo real
Após o pagamento confirmado, dispare uma sequência de notificações de status. Use webhooks da sua cozinha (ou cron job lendo banco) para detectar mudanças de estado e mandar mensagem para o cliente.
Fluxo de status
- EM PREPARO — "🍳 Seu pedido está sendo preparado! Tempo estimado: 25 min."
- SAIU PARA ENTREGA — "🛵 Joaquim saiu com seu pedido! Chegando em ~15 min." + localização do entregador
- ENTREGUE — "✅ Pedido entregue! Como foi sua experiência?" + buttons de avaliação
// Webhook chamado pelo seu sistema interno quando muda status
app.post("/internal/order-status", async (req, res) => {
const { orderId, status } = req.body;
const order = await getOrderById(orderId);
if (status === "OUT_FOR_DELIVERY") {
await ZAP.post(`/instances/${order.instanceId}/messages`, {
to: order.phone,
type: "text",
text: "🛵 *Seu pedido saiu!*\n\nNosso entregador está a caminho. Chegada estimada: 15 minutos.",
});
// Envia também a localização em tempo real
await ZAP.post(`/instances/${order.instanceId}/messages`, {
to: order.phone,
type: "location",
location: {
latitude: order.delivererLat,
longitude: order.delivererLng,
name: "Entregador a caminho",
},
});
}
if (status === "DELIVERED") {
await sendRatingButtons(order.instanceId, order.phone);
const state = await getOrderState(order.phone);
state.state = "RATING";
await setOrderState(order.phone, state);
}
res.status(200).send("ok");
});
Avaliação no chat
Pesquisa pós-entrega via WhatsApp tem 4x mais resposta que email. Use buttons de 1-5 estrelas.
async function sendRatingButtons(instanceId, phone) {
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "buttons",
buttons: {
body: "Como foi sua experiência? Sua nota nos ajuda a melhorar 🙏",
buttons: [
{ id: "RATE_5", title: "⭐⭐⭐⭐⭐" },
{ id: "RATE_4", title: "⭐⭐⭐⭐" },
{ id: "RATE_3", title: "⭐⭐⭐" },
],
},
});
}
Notas baixas (1-3) disparam follow-up automático: "Sentimos muito pela experiência. Pode contar o que aconteceu? Nosso atendente vai pessoalmente te ajudar."
Casos práticos
Caso 1: Hamburgueria de bairro
Loja em bairro de classe média, 80 pedidos/dia em pico no fim de semana. Antes usava só Instagram + WhatsApp manual (2 atendentes). Migrou para o bot acima e ficou com 1 atendente que cuida só de exceções (pedidos especiais, reclamações). Tempo médio do pedido caiu de 12 minutos para 3 minutos. Aumento de 23% no volume porque o cliente não desiste por demora.
Caso 2: Pizzaria com 3 filiais
Cada filial tem instância WhatsApp própria. Bot detecta CEP do cliente e roteia para a filial mais próxima. Cardápio idêntico, mas pedido vai para o sistema da filial certa. Reduziu 100% das brigas internas sobre "de quem é esse pedido?".
Caso 3: Marmitaria fitness
Plano semanal — cliente fecha 5 marmitas por semana, recebe WhatsApp toda segunda às 10h com o cardápio da semana e escolhe via list. Sem precisar abrir nada, só responder. Retenção subiu de 45% para 78% porque a fricção sumiu.
FAQ
Funciona junto com plataformas tipo iFood?
Sim — você pode operar nos dois canais simultaneamente. Restaurantes que fazem isso costumam usar o WhatsApp para clientes recorrentes (sem comissão) e plataformas para descoberta de novos clientes. O bot WhatsApp inclusive pode ofertar cupom para o cliente que veio do iFood pedir direto na próxima vez.
Como calcular taxa de entrega dinâmica?
Quando o cliente manda o endereço (texto ou type: location), use Google Distance Matrix API para calcular distância. Aplique tabela: até 2km grátis, 2-5km R$5, 5-10km R$10, etc. Inclua na mensagem de confirmação antes do total.
Cliente pede cupom de desconto, como aplicar?
Detecte palavras-chave no estado BUILDING_ORDER: se o cliente mandar "cupom X" ou "desconto Y", consulte sua tabela de cupons válidos no banco e aplique. Cupons one-shot devem ser invalidados após uso (registre used_at na linha do cupom). Cupons permanentes podem ter limite por CPF/telefone.
Como gerenciar múltiplas filiais com cardápios diferentes?
Cada filial = 1 instância. No primeiro contato, o bot pergunta CEP ou pede localização. Roteia o telefone para a instância da filial mais próxima e mantém ali. Use uma tabela filial_assignments(phone, instance_id) para lembrar o assignment em pedidos futuros.
É possível pedido recorrente automático?
Sim. Crie tabela recurring_orders com cron de execução (ex.: toda segunda às 18h). Cron pega clientes elegíveis, monta o pedido padrão deles e dispara confirmação via buttons "Confirmar" / "Editar" / "Pular esta semana". Quem não responder em 30min recebe cancelamento automático — não cobramos cliente sem resposta.
Próximo passo
Implemente o bot completo em uma tarde — a parte mais demorada é cadastrar o cardápio. Criar conta grátis e teste o fluxo completo por 7 dias sem custo.