A primeira linha de defesa é nunca processar um webhook sem antes validar a forma. Com zod você declara o contrato esperado e, no momento que o payload diverge, recebe um erro descritivo apontando o campo, em vez de um null silencioso lá na frente. Abaixo, um schema para o payload de mensagem recebida do WhatsApp Cloud API.
// webhookSchema.js
const { z } = require('zod');
const messageSchema = z.object({
from: z.string().min(8),
id: z.string(),
timestamp: z.string(),
type: z.enum(['text', 'image', 'audio', 'document', 'interactive']),
text: z.object({ body: z.string() }).optional(),
});
const valueSchema = z.object({
messaging_product: z.literal('whatsapp'),
metadata: z.object({
display_phone_number: z.string(),
phone_number_id: z.string(),
}),
messages: z.array(messageSchema).optional(),
statuses: z.array(z.object({ id: z.string(), status: z.string() })).optional(),
});
const webhookSchema = z.object({
object: z.literal('whatsapp_business_account'),
entry: z.array(z.object({
id: z.string(),
changes: z.array(z.object({
field: z.literal('messages'),
value: valueSchema,
})),
})).min(1),
});
module.exports = { webhookSchema };
// no handler do webhook
const { webhookSchema } = require('./webhookSchema');
app.post('/webhook', (req, res) => {
const parsed = webhookSchema.safeParse(req.body);
if (!parsed.success) {
// contrato violado: registre, alerte, mas devolva 200
// para o parceiro nao reenviar em loop por erro de forma
logger.error('payload fora do contrato', {
issues: parsed.error.issues,
});
metrics.increment('webhook.contract_violation');
return res.sendStatus(200);
}
enqueue(parsed.data); // so dado valido entra na fila
return res.sendStatus(200);
});
O ponto fino: violação de contrato é diferente de erro de processamento. Devolva 200 para o parceiro não entrar em loop de reenvio, mas dispare métrica e alerta. Um pico em webhook.contract_violation logo após uma mudança do parceiro é o sinal mais cedo possível de que o payload mudou.