Cliente entra no Instagram da agência de viagens, vê foto de Maragogi, comenta "quanto custa?". Resposta padrão da indústria: "manda mensagem no WhatsApp com mais detalhes". 87% dos turistas preferem WhatsApp para cotação inicial — em pesquisa de 2025 com 4.200 viajantes brasileiros, foi o canal mais escolhido em todas as faixas etárias acima de 18 anos. Email ficou em quinto lugar.
Mas cotar pelo WhatsApp manualmente é lento (15-30 minutos por cotação) e perde escala. Esse artigo mostra como uma agência automatiza o ciclo completo de turismo via WhatsApp: cotação inicial em 90 segundos, envio de proposta em PDF, confirmação de reserva, vouchers, lembrete de check-in e suporte durante a viagem.
O fluxo da viagem em 6 etapas
- Cotação: cliente pergunta destino, bot coleta dados estruturados, gera proposta em segundos
- Apresentação: proposta enviada como PDF (type:document) com fotos
- Reserva: cliente confirma, bot gera link de pagamento (PIX/cartão)
- Vouchers: após pagamento, vouchers e itinerário enviados em PDF
- Pré-viagem: 48h antes, lembrete de check-in com botões
- Suporte na viagem: canal aberto 24/7 com escalada pra humano em emergência
Cotação automática
Cliente manda "Quero viagem pra Maragogi em junho, casal, 5 dias". Bot extrai entidades estruturadas (destino, mês, número de viajantes, duração) e consulta sua API de inventário (CVC, Decolar internamente, ou seu próprio sistema).
import axios from "axios";
const ZAP = axios.create({
baseURL: "https://api.zap-api.tech/v1",
headers: { Authorization: `Bearer ${process.env.ZAP_TOKEN}` },
});
async function processarCotacao(phone, texto, instanceId) {
// 1. Extrai entidades via NLP (use OpenAI, Claude ou seu próprio modelo)
const entidades = await extrairEntidades(texto);
// ex: { destino: "Maragogi", mes: "2026-06", viajantes: 2, duracao: 5 }
if (!entidades.destino) {
await replyText(phone, instanceId, "Pra onde você quer ir? (ex: Maragogi, Bariloche, Salvador)");
return;
}
// 2. Busca pacotes no inventário
const pacotes = await buscarPacotes({
destino: entidades.destino,
mes: entidades.mes,
viajantes: entidades.viajantes || 2,
duracao: entidades.duracao || 5,
});
if (pacotes.length === 0) {
await replyText(
phone,
instanceId,
`Não encontrei pacotes pra ${entidades.destino} em ${entidades.mes}. Quer testar outras datas?`
);
return;
}
// 3. Envia top 3 pacotes como cards (lista interativa)
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "list",
list: {
header: "Pacotes encontrados",
body: `${pacotes.length} opções pra ${entidades.destino}`,
footer: "Escolha uma para ver detalhes",
button: "Ver pacotes",
sections: [
{
title: "Opções",
rows: pacotes.slice(0, 3).map((p) => ({
id: `pacote_${p.id}`,
title: `${p.nomeHotel} - R$ ${p.preco}`,
description: `${p.duracao} dias, ${p.regime}`,
})),
},
],
},
});
}
Envio da proposta em PDF
Cliente clicou em um pacote? Gere PDF com proposta detalhada (logo, fotos, condições) e envie via type:document.
import PDFDocument from "pdfkit";
import fs from "fs";
async function gerarPropostaPDF(pacote, cliente) {
const filename = `/tmp/proposta_${pacote.id}_${Date.now()}.pdf`;
const doc = new PDFDocument();
doc.pipe(fs.createWriteStream(filename));
doc.image("logo.png", 50, 30, { width: 100 });
doc.fontSize(20).text(`Proposta: ${pacote.nomeHotel}`, 50, 150);
doc.fontSize(12).text(`Cliente: ${cliente.nome}`);
doc.text(`Destino: ${pacote.destino}`);
doc.text(`Período: ${pacote.checkin} a ${pacote.checkout}`);
doc.text(`Regime: ${pacote.regime}`);
doc.text(`Total: R$ ${pacote.preco}`);
doc.moveDown();
doc.text("Inclui:");
pacote.inclusoes.forEach((i) => doc.text(`• ${i}`));
doc.moveDown();
doc.text("Condições de pagamento:");
doc.text("• PIX à vista: 5% de desconto");
doc.text("• Cartão em 6x sem juros");
doc.end();
await new Promise((r) => doc.on("end", r));
return filename;
}
async function enviarProposta(phone, instanceId, pacote, cliente) {
const pdf = await gerarPropostaPDF(pacote, cliente);
// Upload pra storage e pega URL pública (S3, Cloudinary, etc)
const url = await uploadPdfPublico(pdf);
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "document",
document: {
url,
filename: `Proposta_${pacote.nomeHotel.replace(/\s/g, "_")}.pdf`,
caption: `Sua proposta personalizada! Total: R$ ${pacote.preco}. Quer fechar agora?`,
},
});
// Em seguida, oferece botões de ação
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "buttons",
buttons: {
text: "O que você decide?",
buttons: [
{ id: `fechar_${pacote.id}`, text: "Fechar agora" },
{ id: `negociar_${pacote.id}`, text: "Negociar" },
{ id: `outro_${pacote.id}`, text: "Ver outros" },
],
},
});
}
Reserva e pagamento
// Webhook recebe button "fechar_X"
case "fechar":
const pacoteId = buttonId.split("_")[1];
const pacote = await db.pacote.findUnique({ where: { id: pacoteId } });
// Cria reserva no inventário
const reserva = await criarReserva(pacote, cliente);
// Gera cobrança PIX (Woovi/Asaas/etc)
const cobranca = await criarCobranca({
valor: pacote.preco * 100,
correlationId: `reserva_${reserva.id}`,
descricao: `Pacote ${pacote.nomeHotel}`,
});
await ZAP.post(`/instances/${instanceId}/messages`, {
to: phone,
type: "text",
text: {
body:
`Reserva criada (${reserva.codigo})!\n\n` +
`Pague o PIX abaixo nas próximas 2h pra confirmar:\n\n` +
`${cobranca.brCode}\n\n` +
`Após pago, vouchers e itinerário chegam em até 5 minutos.`,
},
});
break;
Vouchers e itinerário
Webhook do gateway de pagamento confirma. Sistema gera vouchers (hotel, transfer, passeios) e envia em sequência:
app.post("/webhooks/pagamento", async (req, res) => {
res.status(200).send("ok");
if (req.body.event !== "PAYMENT_COMPLETED") return;
const reservaId = req.body.correlationID.replace("reserva_", "");
const reserva = await db.reserva.findUnique({
where: { id: reservaId },
include: { cliente: true, pacote: true },
});
// Gera vouchers (hotel, transfer, passeios)
const vouchers = await gerarVouchers(reserva);
// Envia itinerário consolidado
await ZAP.post(`/instances/${process.env.INSTANCE_ID}/messages`, {
to: reserva.cliente.telefone,
type: "document",
document: {
url: vouchers.itinerarioUrl,
filename: `Itinerario_${reserva.codigo}.pdf`,
caption:
`Pagamento confirmado!\n\n` +
`Aqui está seu itinerário completo. Boas viagens!`,
},
});
// Envia vouchers individuais como anexos separados
for (const v of vouchers.individuais) {
await ZAP.post(`/instances/${process.env.INSTANCE_ID}/messages`, {
to: reserva.cliente.telefone,
type: "document",
document: {
url: v.url,
filename: `Voucher_${v.tipo}.pdf`,
},
});
await sleep(1500); // throttle leve
}
});
Lembrete de check-in 24h antes
// Cron diário às 12h
cron.schedule("0 12 * * *", async () => {
const amanha = new Date();
amanha.setDate(amanha.getDate() + 1);
amanha.setHours(0, 0, 0, 0);
const limite = new Date(amanha);
limite.setHours(23, 59, 59);
const reservas = await db.reserva.findMany({
where: {
checkin: { gte: amanha, lte: limite },
status: "confirmada",
lembrete24h: false,
},
include: { cliente: true, pacote: true },
});
for (const r of reservas) {
await ZAP.post(`/instances/${process.env.INSTANCE_ID}/messages`, {
to: r.cliente.telefone,
type: "buttons",
buttons: {
text:
`Olá ${r.cliente.primeiroNome}! Amanhã é seu check-in em ${r.pacote.destino}.\n\n` +
`Hotel: ${r.pacote.nomeHotel}\n` +
`Endereço: ${r.pacote.endereco}\n` +
`Check-in a partir de ${r.pacote.checkinHora}\n\n` +
`Tudo certo?`,
buttons: [
{ id: `checkin_ok_${r.id}`, text: "Tudo certo" },
{ id: `checkin_duvida_${r.id}`, text: "Tenho dúvida" },
{ id: `checkin_problema_${r.id}`, text: "Tenho problema" },
],
},
});
await db.reserva.update({ where: { id: r.id }, data: { lembrete24h: true } });
}
});
Suporte durante a viagem
Durante a viagem, cliente pode ter problema (voo atrasou, hotel overbooking, transfer não chegou). Roteamento humano com escalonamento:
// Detecção de palavras-chave de emergência
const PALAVRAS_EMERGENCIA = ["emergencia", "urgente", "perdi", "roubaram", "hospital", "policia"];
app.post("/webhook/zap", async (req, res) => {
res.status(200).send("ok");
if (req.body.type !== "message.received") return;
const texto = (req.body.message.body?.text || "").toLowerCase();
const phone = req.body.message.from;
const reservaAtiva = await buscarReservaAtiva(phone);
// Se cliente tem reserva em curso e mandou palavra de emergência
if (reservaAtiva && PALAVRAS_EMERGENCIA.some((p) => texto.includes(p))) {
// Escala IMEDIATAMENTE pra atendente humano de plantão
await alertarPlantao({
cliente: reservaAtiva.cliente,
mensagem: texto,
destino: reservaAtiva.pacote.destino,
telefoneCliente: phone,
});
await replyText(
phone,
req.body.instanceId,
"Recebemos sua mensagem. Um atendente entra em contato em até 5 minutos."
);
return;
}
// Senão, fluxo normal
// ...
});
Casos práticos
Agência boutique (15 reservas/mês)
1 instância, fluxo manual + bot. Cotação automática poupa 30 min/cotação. ROI: +6 vendas adicionais/mês porque atende mais leads simultaneamente.
Operadora média (300 reservas/mês)
3 instâncias (cotação, reserva, suporte). Bot fecha 60% das vendas sem intervenção humana. Atendentes humanos focam em casos premium e resolução de problemas em viagem.
OTA (Online Travel Agency) com 5k reservas/mês
15 instâncias com pool por região (uma específica pra Nordeste, outra pra internacional). Integração direta com GDS Amadeus + bot multi-idioma (PT, EN, ES). Suporte 24/7 cobre fuso horário internacional.
FAQ
Como atender múltiplos idiomas?
Detecte idioma pela primeira mensagem (use franc-min ou OpenAI). Salve em cliente.idiomaPreferido. Templates de mensagem têm variantes em PT/EN/ES. Bot responde sempre no idioma detectado.
E se o cliente está em fuso horário diferente (Europa, Ásia)?
Não envie lembretes em horário Brasil. Use cliente.timezone (preencha pelo destino + horário do voo). Cron de 12h Brasil vira 12h destino. Suporte humano em plantão noturno (terceirizado ou rodízio interno).
Emergência fora do horário comercial?
Plantão 24/7 obrigatório pra agência que vende viagem. Fluxo: bot detecta emergência → tenta resolver com FAQ pré-cadastrado (cancelamento de voo: link da cia, rebooking automático) → escala pra humano de plantão (notificação push no celular dele) → SLA de resposta 5min.
Integração com GDS (Amadeus, Sabre)?
Use middleware Node que conecta com SOAP/XML do GDS de um lado e expõe REST pro seu bot do outro. Bibliotecas: @amadeus/amadeus-node, node-sabre. Latência 800ms-2s por consulta — exiba "buscando..." no WhatsApp pra cliente saber que está rodando.
Como pagar comissão de fornecedor?
Webhook do gateway de pagamento dispara: split automático Woovi/PIX, ou rotina mensal de transferência via gateway com NF-e. Comissão típica: 8-15% do valor da reserva. Reconciliação mensal por contador.
Pronto para automatizar cotação e reserva via WhatsApp? A ZAP API atende agências de turismo de pequeno e médio porte com sandbox pra testar fluxos antes de migrar volume real. Criar conta grátis e teste com 7 dias.