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.createdousubmission.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>
{
"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.
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.