ID del formulario

Webhooks

Payloads de webhook firmados, verificación de la firma HMAC-SHA256, el calendario de reintentos y los registros de entrega.

Los webhooks mandan cada envío aceptado a tu servidor en cuanto llega. Las entregas se firman con el esquema Standard Webhooks, se reintentan durante unos tres días y quedan registradas intento a intento en el panel. Tienes handlers listos en Recibir webhooks.

Añade un endpoint

  1. En el panel, abre Webhooks y elige Add endpoint.
  2. Escribe una URL HTTPS y elige un formulario, o todos los formularios del espacio de trabajo.
  3. Copia el secreto de firma. Empieza con whsec_ y solo se muestra completo una vez.
  4. Envía un evento de prueba para comprobar tu handler.

URL de endpoints

  • Solo https:, en el puerto predeterminado 443, sin usuario ni contraseña en la URL, de hasta 2048 caracteres.
  • Un nombre de host público. Se rechazan las direcciones IP, localhost y los nombres que terminan en .local, .internal, .intranet, .lan, .home.arpa o .onion, igual que ikiform.com, workers.dev y pages.dev.
  • El nombre de host solo debe resolver a direcciones públicas. Se rechazan los rangos privados, de loopback, link-local y de NAT de operador.
  • La URL se comprueba al guardarla y otra vez antes de cada entrega. Un endpoint que empieza a resolver a una dirección privada se desactiva.
  • Un espacio de trabajo puede tener hasta 3 endpoints. Enterprise no tiene límite.

Eventos

TipoSe envía cuandoSe reintenta
submission.createdSe acepta un envío, o un envío marcado como spam se marca como no spamSí
endpoint.testEliges Send test en la página del endpointNo, un solo intento

El spam nunca dispara un webhook. Tampoco los envíos retenidos porque el espacio de trabajo superó su límite mensual.

La petición

Cada entrega es un POST con un cuerpo JSON y tres cabeceras de firma, enviado desde el servicio de entregas de Ikiform con el user agent Ikiform-Webhooks/1.0.

Cabeceras
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=
CabeceraValor
webhook-idEl id de la entrega. Es el mismo en cada reintento de la misma entrega, así que úsalo para ignorar duplicados
webhook-timestampSegundos Unix en que se firmó este intento. Cambia en cada intento
webhook-signaturev1, más un HMAC-SHA256 en base64. Varios valores separados por espacios mientras se rota un secreto

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
  }
}
CampoDescripción
idEl id de la entrega, igual que webhook-id
typesubmission.created o endpoint.test
timestampCuándo se creó la entrega, en ISO 8601 UTC. Es el mismo en cada reintento
api_versionLa versión del payload con la que se creó el endpoint, actualmente 2026-10-17
data.submission_idEl id del envío. Obtenlo con la API REST
data.formEl id y el name del formulario
data.fieldsLos valores guardados. Con campos definidos, son los valores validados y convertidos
data.metaLas mismas claves que el meta de la API REST. Todas las claves están presentes y cualquier valor puede ser null. La IP del visitante y su hash nunca se envían
data.meta.user_agentEl user agent del navegador, de hasta 512 caracteres
data.meta.origin, referrerDe dónde vino el envío, cuando el navegador los envió
data.meta.countryCódigo de país de dos letras, de Cloudflare
data.meta.subject, reply_toDe los campos _subject y _replyto
data.created_atCuándo se guardó el envío
data.truncatedtrue cuando el cuerpo superaría los 256 KB, en cuyo caso fields está vacío

Cualquier valor de meta puede ser null. Trata los valores de los campos como entrada no confiable, igual que un envío de formulario a tu propio servidor.

Verifica la firma

Cómo se calcula la firma del webhook
webhook-id.webhook-timestamp.cuerpo sin procesar

HMAC-SHA256, con el secreto decodificado de base64 después de whsec_ como clave

webhook-signature: v1,base64(signature)

  • Usa el cuerpo de la petición sin procesar, exactamente como llegó. Analizar y volver a serializar el JSON cambia los bytes.
  • La cabecera puede contener varias firmas separadas por espacios. Acepta la petición si coincide cualquier firma v1.
  • Compara en tiempo constante y rechaza las marcas de tiempo que se alejen más de 5 minutos de la hora actual.

Con el SDK

verifyWebhook hace todo lo anterior con Web Crypto y devuelve el evento ya analizado, o lanza WebhookVerificationError con un reason. Acepta un objeto Headers o un objeto simple como el req.headers de Node.

app/api/webhooks/ikiform/route.ts
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;
  }
}

Sin el SDK

Cualquier librería de Standard Webhooks también funciona. A mano son unas pocas líneas:

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 firmas

Ignorar el margen de 5 minutos

Usa verifyWebhook del SDK con Web Crypto en tu navegador. Nada de lo que pegues sale de esta página.

Respuesta

Tu respuestaQué hace Ikiform
Cualquier 2xx en menos de 15 segundosMarca la entrega como correcta
410 GoneMarca la entrega como fallida y desactiva el endpoint al instante
429 o 503 con Retry-AfterReintenta después de ese tiempo, pero no más tarde que el siguiente paso del calendario
Una redirecciónNo se sigue. Cuenta como un fallo
Cualquier otro estado, un timeout o un error de redReintenta según el calendario de abajo

Haz el trabajo lento después de responder, por ejemplo en una cola. Los primeros 4 KB del cuerpo de tu respuesta se guardan en el registro de entregas.

Reintentos

Un intento fallido se reintenta según este calendario, cada espera con hasta un 20% de jitter aleatorio en cada sentido. Después del 9.º intento, unos 3 d 6 h después del primero, la entrega se marca como fallida.

Calendario de reintentos de webhook, con la espera antes de cada intento en escala logarítmica
IntentoEspera previaDesde el primero
  1. 1ahora0 s
  2. 25 s5 s
  3. 35 min5 min
  4. 430 min35 min
  5. 52 h2 h 35 min
  6. 65 h7 h 35 min
  7. 710 h17 h 35 min
  8. 81 d1 d 18 h
  9. 91 d 12 h3 d 6 h

Las barras oscuras muestran la espera más corta y las claras la más larga, con un jitter del 20% en cada sentido. Después del intento 9, la entrega se marca como fallida.

Endpoints desactivados

Si todas las entregas a un endpoint fallan durante 5 días, el endpoint se desactiva y los propietarios del espacio de trabajo reciben un email. Una respuesta 410 Gone o un destino que pasa a ser privado lo desactiva al instante. Vuelve a activarlo desde la página del endpoint cuando esté arreglado.

Registros de entrega

La página de cada endpoint enumera sus entregas con su estado. Despliega una para ver cada intento: cuándo se ejecutó, si fue automático, manual o una prueba, el código de estado, la duración y los primeros 4 KB de tu respuesta o el error. Reintenta cualquier entrega desde ahí. Los intentos se conservan 30 días y las entregas 90 días.

Rotar secretos

Roll signing secret crea un secreto nuevo. Durante las siguientes 24 horas, cada entrega se firma con el secreto antiguo y con el nuevo, así que webhook-signature contiene dos valores y puedes desplegar el secreto nuevo sin perder eventos.