Analisar
Entregas assinadas, e um e-mail que não diz nada.
Um webhook envia cada nova resposta para um URL https à sua escolha. Cada
entrega leva um cabeçalho X-NumoForms-Signature:
sha256= seguido de um HMAC-SHA256 do corpo exato do pedido,
calculado com um segredo exclusivo desse webhook, para que o seu ponto de
acesso possa provar que a carga veio de nós. Em separado, os endereços de
e-mail que escolher podem ser avisados quando chega uma resposta — e esses
e-mails não contêm, deliberadamente, resposta nenhuma.
O que chega ao seu ponto de acesso
Um único POST em JSON por resposta. O corpo tem cinco campos e mais nada, pelo que pode escrever um analisador para ele sem adivinhar.
| Campo | O que contém |
|---|---|
event | response.created |
survey_id | O inquérito a que a resposta pertence. |
response_id | O identificador único desta resposta. |
created_at | O momento em que a resposta foi registada. |
answers | As respostas, indexadas pelo identificador da pergunta. |
| Cabeçalho | Para que serve |
|---|---|
X-NumoForms-Event | O nome do evento. Hoje existe um: response.created. |
X-NumoForms-Signature | sha256= seguido do HMAC-SHA256 do corpo exato do pedido, em hexadecimal. |
X-NumoForms-Delivery | Um identificador único desta tentativa de entrega, para poder detetar um duplicado. |
A entrega é despoletada por um acionador de base de dados na tabela das respostas, e não por um ecrã concreto da aplicação. Isso conta mais do que parece: uma resposta submetida através de um formulário integrado, de uma ligação alojada ou de uma parcial retomada dias depois pelo guardar e continuar seguem todas o mesmo caminho, pelo que não há nenhuma via que nos tenhamos esquecido de ligar.
O webhook dispara quando a resposta chega. Se tiver ligado as aprovações, não espera pela decisão de aprovar ou rejeitar, e não é enviada uma segunda entrega quando essa decisão é tomada. Trate uma carga recebida como «existe uma resposta» e não como «uma resposta foi aprovada».
Porque é que a assinatura não é opcional
Um URL de webhook é um segredo que se derrama. Acaba num pedido de assistência, numa mensagem de Slack, numa captura de ecrã de um documento de transição, no histórico de um portátil partilhado. Sem assinatura, quem tiver visto o URL pode enviar-lhe o que quiser, e os seus sistemas arquivam isso como uma resposta genuína a uma consulta pública. Para uma autarquia a contar oposições a um pedido de licenciamento, isso não é um problema teórico.
Por isso, todas as entregas são assinadas. Cada webhook recebe o seu segredo gerado aleatoriamente, mostrado no editor por trás de um controlo de «mostrar segredo de assinatura». Calculamos um HMAC-SHA256 sobre os bytes exatos do corpo do pedido com esse segredo; do seu lado recalcula-o com a sua cópia e compara. Um pedido forjado falha, porque quem o forjou não tem o segredo.
O URL tem de ser https. Isso é imposto por uma restrição da
base de dados, e não por uma sugestão no formulário, porque um segredo
de assinatura a viajar por uma ligação não cifrada não é um segredo.
Verificar uma entrega em Node.js
Na prática, dois pormenores causam quase todos os problemas. Use o corpo do pedido em bruto, e não um objeto interpretado e voltado a serializar — diferenças de espaços em branco alteram o resumo. E compare em tempo constante, para que um atacante não consiga aprender a assinatura correta byte a byte a partir dos tempos de resposta.
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.NOISSIME_WEBHOOK_SECRET;
// rawBody must be the exact bytes received, before any JSON parsing.
// Re-serialising a parsed object changes the whitespace and the check fails.
function isFromNoissime(rawBody, header) {
if (typeof header !== 'string') return false;
const expected =
'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header);
// timingSafeEqual throws on a length mismatch, so compare lengths first.
return a.length === b.length && timingSafeEqual(a, b);
}
Invoque isso antes de interpretar seja o que for. Se devolver falso, responda 401 e descarte o pedido. Se devolver verdadeiro, tem uma resposta que só pode ter vindo do seu inquérito.
O registo de entregas
Um webhook que deixou de funcionar em silêncio é indistinguível de um que nunca foi invocado: não acontece nada, e nada o avisa. Por isso, todas as tentativas são escritas num registo de entregas, e o último estado HTTP é mostrado junto ao webhook no editor — verde para um 2xx, vermelho para o resto.
A entrega é assíncrona, pelo que o estado é reconciliado a posteriori em vez de adivinhado no momento do envio. O que vê é o estado que o seu servidor devolveu de facto. Cada webhook pode ser suspenso e retomado sem ser apagado, que é o que normalmente se quer enquanto o sistema recetor está a ser reinstalado.
Um limite deliberado: não há repetição com espera exponencial. A entrega é tentada uma vez, o resultado é registado, e fica por aí. Construir uma fila de repetições que respeite a ordem e a idempotência é trabalho a sério, e reclamar uma que não construímos seria pior do que dizer isto. Entretanto, a resposta nunca se perde — está na base de dados, visível em análise e relatórios, e exportável em CSV.
Um webhook a falhar também não pode partir a recolha. Se a entrega rebentar, o erro é registado e a resposta continua a ser guardada. O que importa é a resposta da pessoa; a entrega é feita na melhor das tentativas.
Notificações por e-mail, sem as respostas
Acrescente os endereços que devem saber das novas respostas e passam a receber um e-mail quando chega uma. A mensagem é HTML com a sua imagem e uma alternativa em texto simples, porque tanto os clientes só de texto como os filtros de spam leem a parte simples. Indica o inquérito, dá o total acumulado de respostas e liga aos resultados.
O que não contém são as respostas. Isso é uma decisão, não uma omissão. Um e-mail de notificação é reencaminhado a um colega, arquivado automaticamente numa caixa partilhada, sincronizado com um telemóvel e conservado numa cópia de segurança durante anos. Colocar respostas de texto livre de uma consulta pública nessa cadeia espalha dados pessoais por sistemas que ninguém avaliou e desfaz discretamente os controlos de acesso descritos na página de segurança. Os dados de quem responde ficam na base de dados, e o e-mail diz-lhe para ir ver.
Tal como os webhooks, as notificações são despoletadas por um acionador de base de dados, pelo que seguem a resposta e não uma via de submissão em concreto. Se não estiver configurado nenhum endereço, nada é enviado e nada é sequer tentado.
O que isto não faz
- Não há aplicação para o Zapier nem ligação ao Google Sheets.
- Não há repetição com espera progressiva — uma tentativa por resposta, e depois o registo.
- Não há outros eventos de saída além de
response.created. Aprovações, edições e eliminações não disparam webhooks. - Não há API de entrada para criar inquéritos ou ler respostas por via programática.
- Não há lista de IP autorizados para as entregas. A assinatura é a autenticação.
Perguntas comuns
Como verifico o cabeçalho X-NumoForms-Signature?
Calcule um HMAC-SHA256 do corpo em bruto do pedido com o segredo de assinatura desse webhook, codifique-o em hexadecimal, prefixe-o com "sha256=" e compare-o com o cabeçalho usando uma comparação de tempo constante. Verifique antes de interpretar o JSON e rejeite com um 401 tudo o que não corresponda.
O que acontece se o meu ponto de acesso estiver em baixo quando chega uma resposta?
A entrega é tentada uma vez e o resultado fica registado no registo de entregas com o estado HTTP. Não há repetição automática com espera progressiva. A resposta em si nunca está em risco: uma falha de webhook não impede que uma resposta seja guardada, e pode sempre recuperar os dados no ecrã de resultados ou numa exportação CSV.
Um webhook dispara para uma resposta submetida a partir de um formulário integrado?
Sim. A entrega é despoletada por um acionador de base de dados na tabela das respostas e não por uma página específica da aplicação, pelo que acontece venha a resposta de onde vier — de uma ligação alojada, de um formulário integrado ou de uma resposta parcial que acabou de ser concluída.
Posso enviar um webhook para um URL http://?
Não. Uma restrição de verificação na base de dados exige que o URL comece por https://, e o editor recusa qualquer outra coisa. Um segredo de assinatura enviado por uma ligação não cifrada não é um segredo.
Os e-mails de notificação contêm as respostas de quem respondeu?
Não, deliberadamente. O e-mail diz que chegou uma resposta, indica o inquérito, dá o total acumulado e liga aos resultados. As notificações são reencaminhadas e ficam em cópias de segurança de caixas de correio, pelo que os dados de quem responde permanecem na base de dados, onde estão os controlos de acesso.
Existe uma aplicação para o Zapier ou uma integração com o Google Sheets?
Não. Os webhooks são hoje a única integração de saída. Não existe aplicação para o Zapier nem ligação ao Google Sheets, e nenhuma delas tem data anunciada.
Ligue um inquérito aos seus próprios sistemas.
Aponte um webhook para um endpoint https e cada resposta chega lá assinada, para que o seu recetor consiga provar que veio de nós e não de quem encontrou o URL. Adicione os endereços que devem ser avisados por e-mail quando uma resposta chega, e as respostas ficam na base de dados, onde estão os controlos de acesso.
- Assinatura HMAC-SHA256 em cada entrega
- Registo de entregas com o último estado HTTP
- Disparado por um gatilho da base de dados, seja qual for a via de entrada