Webhooks
Payloads de webhook assinados, verificação da assinatura HMAC-SHA256, a agenda de novas tentativas e os logs de entrega.
Os webhooks repassam cada envio aceito para o seu servidor assim que ele chega. As entregas são assinadas com o esquema Standard Webhooks, têm novas tentativas por cerca de três dias e ficam registradas tentativa por tentativa no painel. Há handlers prontos em Recebendo webhooks.
Adicione um endpoint
- No painel, abra Webhooks e escolha Add endpoint.
- Informe uma URL HTTPS e escolha um formulário, ou todos os formulários do workspace.
- Copie o segredo de assinatura. Ele começa com
whsec_e só é exibido por completo uma vez. - Envie um evento de teste para conferir o seu handler.
URLs de endpoint
- Só
https:, na porta padrão 443, sem usuário ou senha na URL, com até 2.048 caracteres. - Um nome de host público. Endereços IP,
localhoste nomes terminados em.local,.internal,.intranet,.lan,.home.arpaou.onionsão recusados, assim como ikiform.com, workers.dev e pages.dev. - O nome de host precisa resolver só para endereços públicos. Faixas privadas, de loopback, link-local e de NAT carrier-grade são recusadas.
- A URL é verificada quando você a salva e de novo antes de cada entrega. Um endpoint que passa a resolver para um endereço privado é desativado.
- Um workspace pode ter até 3 endpoints. O Enterprise não tem limite.
Eventos
| Tipo | Enviado quando | Novas tentativas |
|---|---|---|
submission.created | Um envio é aceito, ou um envio marcado como spam é marcado como não spam | Sim |
endpoint.test | Você escolhe Send test na página do endpoint | Não, uma tentativa |
Spam nunca dispara um webhook. Envios retidos porque o workspace passou do limite mensal também não.
A requisição
Cada entrega é um POST com corpo JSON e três headers de assinatura, enviado pelo serviço de entrega do Ikiform com o user agent Ikiform-Webhooks/1.0.
POST /webhooks/ikiform HTTP/1.1
content-type: application/json
user-agent: Ikiform-Webhooks/1.0
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=| Header | Valor |
|---|---|
webhook-id | O id da entrega. É o mesmo em toda nova tentativa da mesma entrega, então use-o para ignorar duplicados |
webhook-timestamp | Segundos Unix de quando esta tentativa foi assinada. Novo a cada tentativa |
webhook-signature | v1, mais um HMAC-SHA256 em base64. Vários valores separados por espaço enquanto um segredo é trocado |
Payloads
{
"id": "dlv_01k7v2d4n8r2p6s0w3y5b7c9e1",
"type": "submission.created",
"timestamp": "2026-10-17T12:00:00.000Z",
"api_version": "2026-10-17",
"data": {
"submission_id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3",
"form": { "id": "frm_xxxxxxxxxxxxxxxx", "name": "Contact" },
"fields": { "name": "Ada", "email": "ada@example.com", "message": "Hello" },
"meta": {
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
"origin": "https://example.com",
"referrer": "https://example.com/contact",
"country": "GB",
"subject": null,
"reply_to": "ada@example.com"
},
"created_at": "2026-10-17T12:00:00.000Z",
"truncated": false
}
}| Campo | Descrição |
|---|---|
id | O id da entrega, igual a webhook-id |
type | submission.created ou endpoint.test |
timestamp | Quando a entrega foi criada, em ISO 8601 UTC. O mesmo em toda nova tentativa |
api_version | A versão do payload com que o endpoint foi criado, atualmente 2026-10-17 |
data.submission_id | O id do envio. Busque-o com a API REST |
data.form | O id e o name do formulário |
data.fields | Os valores armazenados. Com campos definidos, são os valores validados e convertidos |
data.meta | As mesmas chaves do meta da API REST. Todas as chaves estão presentes e qualquer valor pode ser null. O IP do visitante e o hash dele nunca são enviados |
data.meta.user_agent | O user agent do navegador, com até 512 caracteres |
data.meta.origin, referrer | De onde o envio veio, quando o navegador os enviou |
data.meta.country | Código de país com duas letras, vindo da Cloudflare |
data.meta.subject, reply_to | Dos campos _subject e _replyto |
data.created_at | Quando o envio foi armazenado |
data.truncated | true quando o corpo passaria de 256 KB, caso em que fields fica vazio |
Qualquer valor de meta pode ser null. Trate os valores dos campos como entrada não confiável, como um envio de formulário para o seu próprio servidor.
Verifique a assinatura
HMAC-SHA256, com o segredo decodificado de base64 depois de whsec_ como chave
webhook-signature: v1,base64(signature)
- Use o corpo bruto da requisição exatamente como foi recebido. Fazer parse e serializar o JSON de novo muda os bytes.
- O header pode conter várias assinaturas separadas por espaço. Aceite a requisição se qualquer assinatura
v1corresponder. - Compare em tempo constante e rejeite timestamps a mais de 5 minutos do horário atual.
Com o SDK
verifyWebhook faz tudo isso com Web Crypto e retorna o evento já interpretado, ou lança WebhookVerificationError com um reason. Ele aceita um objeto Headers ou um record simples, como o req.headers do Node.
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
export async function POST(request: Request) {
const payload = await request.text();
try {
const event = await verifyWebhook({
payload,
headers: request.headers,
secret: process.env.IKIFORM_WEBHOOK_SECRET!,
});
if (event.type === "submission.created") {
console.log(event.data.fields);
}
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookVerificationError) {
return new Response(error.reason, { status: 400 });
}
throw error;
}
}Sem o SDK
Qualquer biblioteca de Standard Webhooks também funciona. Na mão, são poucas linhas:
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = Buffer.from(process.env.IKIFORM_WEBHOOK_SECRET.replace(/^whsec_/, ""), "base64");
export function verify(body, headers) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${id}.${timestamp}.${body}`).digest();
return signatures.split(" ").some((entry) => {
const [version, signature] = entry.split(",");
if (version !== "v1" || !signature) return false;
const received = Buffer.from(signature, "base64");
return received.length === expected.length && timingSafeEqual(received, expected);
});
}Verificador de assinatura
Usa o verifyWebhook do SDK com Web Crypto no seu navegador. Nada do que você cola sai desta página.
Respondendo
| Sua resposta | O que o Ikiform faz |
|---|---|
Qualquer 2xx em até 15 segundos | Marca a entrega como bem-sucedida |
410 Gone | Marca a entrega como falha e desativa o endpoint na hora |
429 ou 503 com Retry-After | Tenta de novo depois desse intervalo, mas não depois da próxima etapa da agenda |
| Um redirecionamento | Não é seguido. Conta como falha |
| Qualquer outro status, um timeout ou um erro de rede | Novas tentativas conforme a agenda abaixo |
Faça o trabalho demorado depois de responder, por exemplo em uma fila. Os primeiros 4 KB do corpo da sua resposta ficam salvos no log de entrega.
Novas tentativas
Uma tentativa que falha é repetida nesta agenda, cada intervalo com até 20% de jitter aleatório para cada lado. Depois da 9ª tentativa, cerca de 3 d 6 h após a primeira, a entrega é marcada como falha.
- 1agora0 s
- 25 s5 s
- 35 min5 min
- 430 min35 min
- 52 h2 h 35 min
- 65 h7 h 35 min
- 710 h17 h 35 min
- 81 d1 d 18 h
- 91 d 12 h3 d 6 h
Barras escuras mostram a espera mais curta e barras claras a mais longa, com 20% de jitter para cada lado. Depois da tentativa 9, a entrega é marcada como falha.
Endpoints desativados
Se todas as entregas para um endpoint falharem por 5 dias, o endpoint é desativado e os proprietários do workspace recebem um e-mail. Uma resposta 410 Gone ou um destino que passa a ser privado o desativa na hora. Reative-o na página do endpoint depois de corrigir.
Logs de entrega
A página de cada endpoint lista as entregas com o status de cada uma. Expanda uma para ver todas as tentativas: quando rodou, se foi automática, manual ou um teste, o código de status, a duração e os primeiros 4 KB da sua resposta ou o erro. Reenvie qualquer entrega a partir dali. As tentativas ficam guardadas por 30 dias e as entregas por 90 dias.
Troca de segredos
Roll signing secret cria um novo segredo. Nas 24 horas seguintes, toda entrega é assinada com o segredo antigo e com o novo, então webhook-signature contém dois valores e você pode fazer deploy do novo segredo sem perder eventos.