formbasedocs
Ir a la appApp

Solicitudes

Callbacks y firma

Cuando una solicitud llega a su final — completada, expirada o cancelada — formbase envía por POST una notificación firmada a la URL que le dio tu automatización. Esa llamada es la que reanuda la ejecución.


Qué se dispara, y cuándo

EventoCuándo
request.completedEl destinatario envió el formulario. Incluye las respuestas.
request.expiredEl plazo pasó mientras la solicitud seguía pendiente.
request.canceledTú o tu automatización la retiraron.

Los tres llegan a la misma URL, así que decide según type antes de asumir que hay respuestas. Ese es todo el sentido de dispararse en cada final: un flujo de trabajo aparcado en un cliente se reanuda tanto si respondió, como si te ignoró, o si se anuló.

Lo que llega

Cuerpo del POST
json
{
  "id": "evt_kj7...",
  "type": "request.completed",
  "createdAt": "2026-03-04T09:31:40.000Z",
  "apiVersion": "2026-09-24",
  "test": false,
  "data": {
    "request": {
      "id": "kd7...",
      "externalId": "run-42",
      "status": "completed",
      "outcome": "approve",
      "language": "en",
      "recipient": { "email": "ada@acme.com", "name": "Ada" },
      "metadata": { "runId": "run-42" },
      "context": { "case_id": "CASE-9" },
      "createdAt": "2026-03-04T09:20:00.000Z",
      "completedAt": "2026-03-04T09:31:40.000Z"
    },
    "form": { "id": "j57...", "name": "Vendor onboarding", "snapshotId": "kx2..." },
    "submission": {
      "id": "jd7...",
      "respondentEmail": "ada@acme.com",
      "submittedAt": "2026-03-04T09:31:40.000Z",
      "updatedAt": null,
      "editCount": 0,
      "pdfUrl": null,
      "language": "en"
    },
    "answers": { "company_name": "Acme", "contacts": [{ "name": "Ada" }] },
    "display": { "company_name": "Acme", "contacts": "Ada" }
  }
}
  • data.request siempre está presente — incluidos tu externalId, metadata y context, sin cambios. Lleva la marca de tiempo del final que ocurrió (completedAt, expiredAt, o canceledAt con un cancelReason opcional).

  • outcome solo está presente cuando el destinatario respondió una pregunta de decisión — approve, decline o changes.

  • form, submission, answers y display aparecen solo al completarse, exactamente con la forma que lleva un webhook de respuesta. form.snapshotId es la versión publicada exacta que respondió el destinatario; submission.pdfUrl es una URL solo cuando el formulario conserva un PDF de la respuesta, y null en caso contrario.

  • test es true cuando la solicitud se creó en modo de prueba — bifurca a partir de él, o descarta el evento.

  • answers se indexa por clave de campo, con los grupos repetibles anidados como un objeto por instancia. Una respuesta de elección es el key de la opción según fields.list, no su etiqueta; la etiqueta está en display, bajo la misma clave.

  • El POST llega como Content-Type: application/json con User-Agent: formbase, y lleva X-formbase-Event-Id, X-formbase-Event-Type y X-formbase-Signature — para que puedas desduplicar y enrutar antes de analizar el cuerpo.

Verifica la firma

Cada callback lleva una cabecera de firma, X-formbase-Signature: t={unix seconds},sha256={hex}. El hex es un HMAC-SHA256 de la marca de tiempo, un punto, y el cuerpo bruto de la solicitud, calculado con el secreto de firma de solicitudes de tu espacio de trabajo.

Dos reglas, sea cual sea el lenguaje que uses:

  1. Aplica el hash al cuerpo bruto, antes de cualquier análisis o reserialización. Un JSON reconvertido no son los mismos bytes.

  2. Compara en tiempo constante — crypto.timingSafeEqual, hmac.compare_digest — nunca con ==.

El secreto de firma de solicitudes

Un único secreto por espacio de trabajo firma todos los callbacks que salen de él. Lo encuentras en OAuth y claves de API en la barra lateral del espacio de trabajo, en la tarjeta Secreto de firma de solicitudes. Está oculto por defecto; Mostrar secreto lo revela y el botón de copiar lo copia. No es un valor de un solo uso — puedes volver y leerlo de nuevo. El secreto se genera la primera vez que hace falta, así que un espacio de trabajo que nunca ha abierto esa tarjeta ni ha creado una solicitud con callbackUrl todavía no tiene ninguno.

Reintentos

Un callback tiene ocho intentos: el primero, y luego siete reintentos con al menos 1, 2, 4, 8, 16, 32 y 60 minutos de diferencia entre sí. formbase busca reintentos pendientes cada 30 minutos, así que un reintento puede llegar hasta media hora después de que termina su intervalo, y el último intento llega unas cuatro horas después de que terminó la solicitud. Cada intento lleva los mismos bytes y el mismo id: el payload se congela en el momento en que terminó la solicitud, así que un reintento describe lo que pasó entonces, no cómo se ve la solicitud ahora. La URL de destino y el secreto de firma se leen en cada intento, no se congelan con él.

Tu respuestaQué hace formbase
2xxHecho. El callback se marca como entregado.
408, 429, 5xxReintenta, respetando Retry-After cuando lo envías.
Otro 4xxSe detiene. Tu endpoint rechazó la llamada; reintentar el mismo cuerpo no puede ayudar.
Tiempo de espera agotado o error de conexiónReintenta según el mismo calendario.
URL bloqueadaSe detiene de inmediato. Un host que no resuelve, una dirección privada, o una URL sin HTTPS nunca pueden llegar a permitirse. Las redirecciones nunca se siguen, así que un 3xx también se detiene.

Si se agota el presupuesto — tu receptor estuvo caído toda la tarde — el callback no se pierde. La solicitud recibe una insignia Callback fallido, se avisa una vez por correo a la persona propietaria del espacio de trabajo con el host, el motivo y el número de intentos, y las respuestas siguen legibles a través de requests.get. Para reenviarlo, abre la solicitud en la página de Solicitudes y pulsa Reproducir, o llama a requests.replayCallback. Reenvía el mismo payload congelado con el mismo id, que es exactamente lo que quiere un receptor que desduplica.

Las suscripciones escuchan los mismos eventos

Una URL de callback pertenece a una sola solicitud. Cuando cada solicitud de un formulario debe llegar al mismo receptor, suscríbete una vez en su lugar: las apps de formbase para Zapier y n8n hacen esto por ti, y webhooks.create lo hace desde código con request_completed, request_expired o request_canceled como tipo de evento. Una suscripción recibe este mismo envoltorio, firmado con su propio secreto en lugar del secreto de firma de solicitudes del espacio de trabajo, con su propio id de evento y su propio presupuesto de reintentos. Una solicitud que tiene tanto una URL de callback como una suscripción correspondiente se dispara dos veces, una a cada uno. Reproducir reenvía solo el callback; una suscripción reintenta por su cuenta y se pausa tras cinco intentos fallidos.

Una URL de reanudación no es autenticación

Las herramientas de flujos de trabajo te dan una URL de reanudación difícil de adivinar, y es tentador tratarla como una prueba. Es un secreto portador — puede filtrarse en registros, y no te dice que el cuerpo no fue manipulado. Verifica también la firma en la rama reanudada.