Receber webhooks
Fazemos um POST para o seu URL quando algo acontece aqui, assinado para que saiba que veio de nós. Os webhooks e o Slack estão disponíveis nos planos Pro e Team.
Ou apenas o Slack
Se o que pretende é ficar a saber, e não construir algo, ligue antes o Slack: Definições, depois Slack. Cola um URL de incoming webhook do Slack (Apps → Incoming Webhooks → Add to Slack, onde escolhe o canal), e chega uma mensagem a esse canal quando alguém responde a um formulário, quando alguém comenta, ou quando um site fica online.
Num formulário, a mensagem diz que campos a pessoa preencheu, nunca o que escreveu. Num comentário, o que foi dito e o nome de quem comentou, com um link para a conversa. Um canal chega a mais gente do que a sua caixa de correio, e não nos cabe a nós divulgar lá o e-mail de alguém.
Adicionar um webhook
Definições, depois Webhooks. Indica um URL https e escolhe sobre o que quer ser avisado. Damos-lhe um segredo de assinatura, uma única vez. Guardamo-lo para assinar, não para mostrar, por isso não há forma de o voltar a ver, só de substituir o webhook.
O que enviamos
Um corpo JSON com um campo event, e dois cabeçalhos:solta-event e solta-signature. Os eventos são site.published, version.created, submission.received, site.deleted, comment.created e comment.resolved.
Um comentário, ou uma resposta, chega como comment.created. O autor é um nome, nunca um e-mail, e url abre a conversa na versão em que foi deixada:
{
"event": "comment.created",
"siteId": "6f1c…",
"slug": "quote-builder",
"title": "Quote builder",
"version": 3,
"versionState": "draft",
"comment": {
"id": "a41e…",
"threadId": "a41e…",
"parentId": null,
"author": "Ana",
"body": "The total should include VAT.",
"createdAt": "2026-10-12T09:41:07.000Z"
},
"url": "https://solta.now/s/quote-builder/v/3?panel=comments&thread=a41e…"
}parentId é o comentário a que se respondeu, ou null numa conversa nova, cujo threadId é o seu próprio id. comment.resolved tem os mesmos campos de site e de versão, e um comment com id, threadId e resolvedAt.
Verificar a assinatura
O cabeçalho tem este aspeto: t=1757155200,v1=9f86d0…. O digest é um HMAC-SHA256 do timestamp, de um ponto final e do corpo exato que enviámos. Por isso, verifique os bytes em bruto, antes que qualquer parsing de JSON reordene alguma coisa.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < 300)) return false; // demasiado antigo para ser legítimo
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return parts.v1.length === expected.length &&
timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}O timestamp está dentro do que é assinado, e não ao lado. É isso que impede alguém de reenviar hoje, contra o seu relógio, uma entrega que capturou na semana passada.
Quando a entrega falha
Qualquer resposta que não seja 2xx é repetida cinco vezes, com um intervalo cada vez maior. Um 4xx também é repetido: daqui, uma recusa definitiva e uma rota mal configurada parecem idênticas, e desistir ao primeiro 404 faria perder eventos enquanto corrigia um caminho.
Não lemos o corpo da sua resposta. O tempo limite é de dez segundos: responda primeiro, trabalhe depois.

