gzucob.
← posts

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.

15 de jul. de 20267 min de leitura

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.

text
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.

ts
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.

bash
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:

text
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.