Webhooks na prática: do formulário do site ao pipeline no n8n
Dois usos de webhooks: receber contatos deste site no Discord e iniciar um pipeline de estudos no n8n.
Formulário de contato é o tipo de funcionalidade que parece simples até você precisar entregar em produção: precisa validar, precisa combater spam, precisa notificar alguém. Neste site, a mensagem passa por uma rota no servidor, vira um POST para o Discord e só então produz uma confirmação para quem enviou.
O mesmo mecanismo aparece em outro projeto meu, o youtube-study-automation. Lá, um webhook recebe a URL de um vídeo no n8n e outro envia o PDF pronto para o Discord. A URL é parecida; o papel de cada webhook no fluxo é diferente.
Um webhook é uma porta de entrada, não uma segunda aplicação
Um webhook é um endereço HTTP preparado para receber um evento. Quem envia faz uma requisição, geralmente um POST, com os dados no corpo. Quem recebe interpreta esse corpo e responde com um status HTTP.
No caso do Discord, a URL do webhook aponta para um canal específico. O site não precisa manter uma conexão aberta nem construir uma tela de caixa de entrada: ele envia um JSON para aquela URL, e o Discord transforma o conteúdo em uma mensagem no canal.
Isso não transforma o webhook em banco de dados. Se eu precisar buscar histórico, marcar mensagens como lidas ou criar regras complexas de atendimento, o Discord sozinho deixa de ser a ferramenta certa. Para uma caixa de entrada pessoal, o contrato simples resolve.
No site, o navegador não fala diretamente com o Discord
O formulário envia os dados para /api/contact. A rota valida, aplica as proteções e só então usa a URL guardada em DISCORD_WEBHOOK_URL. O segredo fica no servidor; o navegador conhece apenas a API do próprio site.
Formulário
→ POST /api/contact com JSON
→ JSON válido + honeypot + limite de envio
→ POST DISCORD_WEBHOOK_URL com JSON
→ Discord cria a mensagem no canal
→ API confirma o envio
O fluxo tem uma fronteira importante: o primeiro POST é do navegador para o meu servidor; o segundo é do meu servidor para o Discord. Essa separação permite validar e controlar os dados antes de qualquer notificação externa.
O corpo do POST vira um embed dentro do canal
Um embed é o cartão estruturado que o Discord mostra na mensagem. A rota envia título, descrição, cor, campos e horário. Nome, e-mail e telefone entram em campos separados; a preferência de retorno e o texto da mensagem ficam na descrição.
await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: AbortSignal.timeout(10_000),
body: JSON.stringify({
allowed_mentions: { parse: [] },
embeds: [
{
title: "Novo contato via site",
description: `**Retorno preferido:** ${preferenceLabel}\n\n${message}`,
color: 0xea8a3d,
fields,
timestamp: new Date().toISOString(),
},
],
}),
});
O allowed_mentions desativa menções interpretadas a partir do conteúdo enviado. É um detalhe pequeno, mas impede que um texto de contato seja tratado como comando para notificar pessoas no canal.
O primeiro caminho frágil seria retornar sucesso sem olhar a resposta
Fazer o fetch é apenas metade do trabalho. Se o servidor do Discord responder com erro, ou se a requisição estourar o limite de dez segundos, a rota precisa avisar o frontend. O código atual trata os dois casos como webhook_failed e responde com 502.
O teste da rota também fixa esse comportamento: uma resposta 404 do webhook não vira sucesso local. Uma exceção de rede também não vira silêncio. A mensagem não foi aceita, então o formulário não deve mostrar uma confirmação falsa.
O caminho contrário também existe. Se um robô preenche o campo invisível website, a rota responde 200 sem chamar o Discord. Para o robô parece que tudo funcionou; para o canal, nenhuma mensagem é criada.
A validação acontece antes da URL externa
O webhook não sabe se o nome tem tamanho válido, se a preferência é WhatsApp ou e-mail, nem se a mensagem é longa demais para o produto. Essa decisão pertence ao schema da API.
O contactSchema exige nome com pelo menos dois caracteres, mensagem entre 20 e 2.000 caracteres e uma preferência válida. Se a preferência for WhatsApp, telefone vira obrigatório. Se for e-mail, o endereço também.
Depois do schema, a rota limita cada chave de cliente a cinco envios em quinze minutos. Só depois dessa barreira ela monta os campos do embed e chama o Discord.
| Situação | Resposta da API |
|---|---|
| JSON inválido ou payload rejeitado | 400 |
| Honeypot preenchido | 200, sem chamar o Discord |
| Webhook não configurado | 500 |
| Limite de envio excedido | 429 com Retry-After |
Discord falha ou responde fora de 2xx |
502 |
| Discord aceita a mensagem | 200 com ok: true |
Quando o Discord aceita o envio, a API confirma a operação. O frontend salva o estado da submissão em um cookie e mostra um cartão de confirmação. Assim, o webhook notifica o canal sem deixar a experiência de quem enviou a mensagem sem estado.
No youtube-study-automation, o webhook aparece em duas pontas
No workflow principal do projeto, o primeiro webhook não é do Discord. É um node do n8n com método POST e caminho /webhook/study. Ele recebe a URL do YouTube e inicia o pipeline.
curl -X POST http://localhost:5678/webhook/study \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=SEU_VIDEO_AQUI"}' \
-o resumo.pdf
Depois desse evento, o n8n busca os metadados, baixa o áudio, transcreve com Whisper, divide o texto, resume os blocos com Ollama, consolida o resultado e gera o PDF com Gotenberg. O tempo de 10–15 minutos para um vídeo de 30 minutos pertence a esse pipeline inteiro, não ao webhook.
Só no final entra o webhook do Discord. O node Send PDF to Discord faz outro POST, agora para https://discord.com/api/webhooks/..., usando multipart/form-data: envia o arquivo binário no campo file e uma mensagem com nome do arquivo, canal e duração. Depois, o node Respond to Webhook devolve o resultado para quem iniciou o workflow.
O desenho fica assim:
POST /webhook/study
→ n8n orquestra download, transcrição, resumo e PDF
→ POST para o webhook do Discord com arquivo + content
→ n8n responde com o PDF
A diferença é útil para entender o conceito: o webhook do n8n é uma porta de entrada para iniciar trabalho; o webhook do Discord é uma porta de saída para entregar uma notificação. Os dois são apenas requisições HTTP, mas pertencem a etapas diferentes.
Falhas também viram uma mensagem no Discord
O workflow principal configura um workflow de erro separado. O arquivo Send Error Message Discord.json recebe o evento de falha do n8n e envia outra mensagem para o Discord com o nome do workflow, o último node executado e a mensagem do erro.
Isso não é uma tentativa de esconder a falha. É uma forma de observabilidade: o mesmo canal que recebe o resultado também avisa quando o pipeline não conseguiu chegar ao PDF.
As escolhas reduziram infraestrutura, mas não eliminaram dependências
| Decisão | Alternativa | Ganho | Custo |
|---|---|---|---|
| Usar Discord como caixa de entrada pessoal | Banco + painel próprio | Entrega rápida e nenhuma tela nova para manter | O histórico e o fluxo de atendimento ficam presos ao Discord |
| Chamar o Discord pelo servidor | Expor a URL no navegador | Mantém o segredo fora do cliente e centraliza validação | A rota do site vira mais um ponto para monitorar |
| Usar n8n como orquestrador no projeto de estudos | Implementar todo o pipeline em uma aplicação | Workflow importável e etapas visíveis | O fluxo depende de n8n, Docker e dos serviços locais |
| Verificar status e exceções | Retornar ok depois de qualquer fetch |
Evita confirmação falsa | O frontend precisa tratar mais estados de erro |
O webhook não resolveu toda a operação. Ele resolveu uma fronteira específica: receber um evento, transportar os dados e deixar outro serviço cuidar da entrega. O restante — validação, segurança, tentativas e estado — continua sendo responsabilidade de quem chama a URL.
O que aprendi
Webhook parece uma solução pequena porque a chamada é pequena. O trabalho real está em desenhar o antes e o depois: validar antes do POST, não expor o segredo, interpretar a resposta do Discord e decidir o que a pessoa vê quando a entrega falha.
No site, esse contrato substitui banco e e-mail para um caso pessoal. No youtube-study-automation, o mesmo contrato conecta o fim de um pipeline pesado a um canal onde eu realmente acompanho as saídas. A parte que eu repetiria é a simplicidade; a parte que eu não esconderia é a dependência externa: se o webhook desaparecer, a aplicação precisa saber dizer que a mensagem não foi aceita.