Receber webhooks
Fazemos um POST para a sua URL quando algo acontece aqui, assinado para você saber que veio de nós. Os webhooks e o Slack estão nos planos Pro e Team.
Ou só o Slack
Se o que você quer é ficar sabendo, e não construir algo, conecte o Slack: Configurações, depois Slack. Você cola uma URL de incoming webhook do Slack (Apps → Incoming Webhooks → Add to Slack, onde você escolhe o canal), e uma mensagem chega nesse canal quando alguém responde a um formulário, quando alguém comenta, ou quando um site vai ao ar.
Para um formulário, a mensagem diz quais campos a pessoa preencheu, nunca o que ela escreveu. Para um 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 entrada, e não cabe a nós espalhar o e-mail de alguém por ele.
Adicionar um webhook
Configurações, depois Webhooks. Você informa uma URL https e escolhe sobre o que quer ser avisado. Nós damos a você um segredo de assinatura, uma vez só. Guardamos o segredo para assinar, não para mostrar, então não há como vê-lo de novo, só 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 ela 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 respondido, ou null numa conversa nova, cujo threadId é o 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 formato: t=1757155200,v1=9f86d0…. O digest é um HMAC-SHA256 do timestamp, de um ponto final e do corpo exato que enviamos. Por isso, verifique os bytes brutos, 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; // antigo demais 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 fica 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 é tentada de novo 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 no primeiro 404 faria você perder eventos enquanto corrigia um caminho.
Não lemos o corpo da sua resposta. O tempo limite é de dez segundos: responda primeiro, trabalhe depois.

