Pular para o artigo

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.