DocsAbrir Studio

Webhooks

Um POST assinado por Submission. Hoje há dois eventos: created (sempre) e accepted (depois do gate).

Configurar

Na aba Integrações de um Form. Até cinco por Form. O secret fica mascarado na tela, mas continua disponível pelo botão Copiar secret — enquanto o Studio não tem login, trate quem alcança o Studio como quem alcança os secrets.

O que chega

X-Forma-Event
submission.created ou submission.accepted. Ver Gate.
X-Forma-Delivery
UUID desta entrega. Use para idempotência.
X-Forma-Timestamp
Epoch em ms, e faz parte da assinatura.
X-Forma-Signature
sha256=<hmac hex>
corpo
{
  "event": "submission.created",
  "submission": {
    "id": "7e583034-1a0a-47d5-ab72-74d4541f8486",
    "formId": "form_survey_demo",
    "receivedAt": 1788182240254,
    "origin": "https://app.acme.com",
    "userAgent": "Mozilla/5.0 …",
    "payload": { "satisfacao": 5, "plano": "pro" }
  }
}

payload é exatamente o que chegou no ingest, sem normalizar nada.

Verificar a assinatura

HMAC-SHA256 sobre `${timestamp}.${body}`, com o corpo cru — antes de qualquer parse. O timestamp entra na string assinada para que uma requisição capturada não possa ser repetida com um horário novo.

verify.ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(req, secret) {
  const ts = req.headers["x-forma-timestamp"];
  const sent = req.headers["x-forma-signature"];

  // Reject anything older than five minutes.
  if (Math.abs(Date.now() - Number(ts)) > 300_000) return false;

  const mac = createHmac("sha256", secret)
    .update(`${ts}.${req.rawBody}`)
    .digest("hex");

  const expected = Buffer.from(`sha256=${mac}`);
  const actual = Buffer.from(sent);
  return expected.length === actual.length
    && timingSafeEqual(expected, actual);
}

Compare com timingSafeEqual, nunca com ===. E rejeite timestamps velhos — sem isso a assinatura continua válida para sempre.

Entrega

3 tentativas
Espera 0, 500 e 2000 ms entre elas.
5 s de timeout
Por tentativa.
2xx é sucesso
Qualquer outra coisa tenta de novo.
Depois da resposta
O ingest não espera pelo seu endpoint.

Cada tentativa fica registrada com status, código, duração e erro, visível na aba Integrações do Form e na página Integrações do Workspace.

A entrega é best effort: não existe fila. Se o processo morrer no meio, a entrega se perde e fica marcada como falha. Não construa em cima disto nada que não possa ser reconciliado pelo ingest.