> For the complete documentation index, see [llms.txt](https://docs.mensajerodigital.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.mensajerodigital.app/formularios/webhooks-enviar-los-registros-de-un-formulario-a-otro-sistema.md).

# Webhooks: enviar los registros de un formulario a otro sistema

Envíe automáticamente cada registro de un formulario a otro sistema, a una integración o a un RPA, en el momento en que se diligencia.

Un **webhook** hace que Mensajero Digital envíe cada registro nuevo de un formulario a una dirección web que usted define, en el momento en que alguien lo guarda. Así su sistema de nómina, su ERP o su RPA reciben los datos directamente, sin tener que ingresar a la plataforma ni leerlos desde la pantalla.

{% hint style="info" %}
**¿Cuándo usarlo?** Cuando otro sistema necesita los datos de un formulario apenas se diligencian. Si solo necesita consultarlos de vez en cuando, puede [exportar los registros a Excel](/formularios/exportar-los-registros-de-un-formulario-a-excel.md).
{% endhint %}

## Configurar un webhook

{% stepper %}
{% step %}

### Abra el formulario

Ingrese al módulo de formularios, busque el formulario y ábralo para editarlo. Si el formulario es nuevo, guárdelo primero: los webhooks solo se pueden configurar en formularios guardados.
{% endstep %}

{% step %}

### Cree el webhook

En la pestaña **Webhooks**, presione **Nuevo Webhook** y complete:

* **URL del Webhook**: la dirección donde su sistema recibirá los datos. Debe empezar por `https://`.
* **Método HTTP**: seleccione **POST** (recomendado).
* **Enviar desde la IP fija de Mensajero Digital**: márquela solo si su sistema acepta conexiones únicamente desde direcciones IP autorizadas en su firewall. Vea [IP de salida fija](#ip-de-salida-fija).

Presione **Crear Webhook**.
{% endstep %}

{% step %}

### Copie la clave de firma

En la lista de webhooks, presione el botón del candado. Se abre la ventana **Clave de firma**: presione **Copiar** y entregue la clave solo a quien administra el sistema que recibe los datos. Con ella, ese sistema puede comprobar que cada envío viene de Mensajero Digital.

{% hint style="warning" %}
**Generar clave nueva** deja sin validez la clave anterior de inmediato. Úsela solo si la clave se compartió con alguien que no debía tenerla, y actualícela enseguida en el sistema que recibe los datos.
{% endhint %}
{% endstep %}

{% step %}

### Envíe una prueba

En la lista de webhooks, presione el botón del avión de papel. Mensajero Digital envía a la URL un registro con datos de ejemplo y le muestra si su sistema lo recibió. Vea [Enviar una prueba](#enviar-una-prueba).
{% endstep %}
{% endstepper %}

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FPjwFl59Bv3ZEi5M68lFf%2F5.png?alt=media&amp;token=d7629bb8-8c49-473b-904f-ed113e0c8abf" alt=""><figcaption></figcaption></figure>

## Ver la estructura de su formulario

No necesita diligenciar el formulario para saber qué datos va a recibir su sistema. En la pestaña **Webhooks**, presione **Ver estructura del envío**. La ventana tiene dos vistas:

* **Campos**: una fila por cada campo que envía datos, con la clave con la que llega en `valores` (el título del campo), su identificador (`id_campo`), el tipo de campo, el formato en que llega y un valor de ejemplo.
* **Ejemplo JSON**: el cuerpo completo de un envío, con valores de ejemplo. Presione **Copiar JSON** para pegarlo en su herramienta de integración (n8n, Make, Power Automate, su RPA…) y mapear los campos antes de recibir el primer registro real.

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FA0senaHAL3wFvpWPNHkX%2F2.jpg?alt=media&amp;token=59948b03-7397-478b-8037-e1f70c6a8129" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
La ventana se arma con el formulario tal como está en el editor, incluidos los cambios que aún no ha guardado. Guarde el formulario para que los envíos lleguen así.
{% endhint %}

## Enviar una prueba

Con el botón del avión de papel, en la lista de webhooks, Mensajero Digital envía a la URL un registro con datos de ejemplo, firmado igual que uno real. Sirve para comprobar la integración de punta a punta sin diligenciar el formulario.

La ventana **Enviar prueba** muestra:

* Si el envío quedó **Entregado** o si su sistema no lo aceptó, con el código HTTP de la respuesta y cuánto tardó.
* Lo que respondió su sistema, para revisar errores sin salir de la plataforma.
* El motivo cuando no se pudo enviar, por ejemplo si el servidor de la URL no existe.

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FcUQC6dJPDH0yxdnEGknC%2F6.jpg?alt=media&amp;token=179d6cf5-06b1-4ed7-9521-17fc8e6f6ccb" alt=""><figcaption></figcaption></figure>

Presione **Enviar otra vez** para repetir la prueba después de corregir algo.

Características de la prueba:

* El cuerpo tiene la misma forma que un envío real, con `"prueba": true` y la cabecera `X-Mensajero-Prueba: 1`. Si su sistema guarda los datos, descarte los envíos de prueba.
* Los enlaces de los adjuntos son de ejemplo y no descargan nada.
* No se guarda en el historial ni se reintenta si falla.
* Si el webhook tiene activa la [IP fija](#ip-de-salida-fija), la prueba sale desde ella, igual que los envíos reales. Así puede comprobar que su firewall la deja pasar.
* Solo se envía a direcciones públicas de internet: se rechazan las URL que apuntan a `localhost` o a redes privadas, y no se siguen redirecciones. Los envíos reales tampoco salen hacia redes privadas, ni siquiera después de una redirección.

## Historial de envíos

Cada webhook guarda sus envíos de los últimos **30 días**, hayan salido bien o no. En la lista de webhooks, presione el botón del reloj para abrir el **Historial de envíos**:

* Una fila por cada registro enviado, con la fecha, el número del registro, el estado, el código HTTP de la respuesta, el tiempo que tardó su sistema y el número de intentos.
* Filtre por estado: **Entregados**, **Reintentando** o **Fallidos**.
* Presione el botón del ojo para ver el detalle: el cuerpo que se envió y lo que respondió su sistema. Los enlaces de los adjuntos no se guardan: en el detalle aparece una nota en su lugar, porque cada envío genera los suyos.

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FbcFw0Ijfx8yppfff5rDJ%2F7.jpg?alt=media&amp;token=2ef71894-852c-48c9-af14-2e86b18fe8b7" alt=""><figcaption></figcaption></figure>

| Estado       | Qué significa                                                                                          |
| ------------ | ------------------------------------------------------------------------------------------------------ |
| Entregado    | Su sistema respondió con un código 2xx.                                                                |
| Reintentando | El envío falló y Mensajero Digital lo volverá a intentar solo. Se muestra la hora del próximo intento. |
| Fallido      | Se agotaron los reintentos sin que su sistema lo aceptara.                                             |

### Reenviar un envío

Si su sistema estuvo caído o rechazó un registro por un error que ya corrigió, presione el botón de reenviar en esa fila (o **Reenviar** en el detalle). Mensajero Digital manda otra vez los mismos datos con el mismo `id_entrega`, para que su sistema pueda descartarlo si ya lo había procesado. Los adjuntos llegan con enlaces nuevos, vigentes por una hora.

* El reenvío es un solo intento: si falla, queda como **Fallido** y no se reintenta solo.
* No se puede reenviar un envío que está **Reintentando**: espere a que termine.

{% hint style="info" %}
Los envíos de más de 30 días se borran automáticamente. Los envíos de prueba no aparecen en el historial.
{% endhint %}

## Qué recibe su sistema

Una petición por cada registro **nuevo**, en el momento en que se guarda. Las ediciones y anulaciones de registros no generan envío.

El cuerpo llega en formato JSON:

```json
{
  "evento": "registro.creado",
  "id_entrega": "3f0c8a0e-5b8f-4c1e-9a53-2f7c1d9b6e10",
  "fecha_registro": "2026-09-16T15:42:10.123Z",
  "id_documento": null,
  "id_documento_formulario": 184233,
  "id_formulario_dinamico": 6946,
  "nombre_formulario": "Registro de ausencias",
  "identificacion_remitente": "1020304050",
  "nombre_remitente": "Nombre de quien diligencia",
  "codigo_seguimiento": "Ab12Cd34Ef",
  "correo_seguimiento": "persona@empresa.com",
  "valores": {
    "Nombre del colaborador": "Nombre de ejemplo",
    "Fecha de inicio": "2026-09-15",
    "Días": "2",
    "Tipo de ausencia": "Incapacidad",
    "Soporte": "incapacidad.pdf"
  },
  "campos": [
    { "id_campo": "ca_1f…", "titulo": "Fecha de inicio", "tipo_campo": "date", "valor": "2026-09-15" },
    {
      "id_campo": "ca_7b…", "titulo": "Soporte", "tipo_campo": "file", "valor": "incapacidad.pdf",
      "archivos": [
        {
          "nombre": "incapacidad.pdf",
          "tipo": "application/pdf",
          "tamano": 183402,
          "url": "https://storage.googleapis.com/…?X-Goog-Expires=3600&X-Goog-Signature=…",
          "url_vence": "2026-09-16T16:42:10.000Z"
        }
      ]
    }
  ]
}
```

{% hint style="success" %}
Para ver este mismo ejemplo con los campos de **su** formulario, use [Ver estructura del envío](#ver-la-estructura-de-su-formulario).
{% endhint %}

| Dato           | Para qué sirve                                                                                                                                                                                                                       |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `valores`      | Lo más fácil de usar: cada clave es el título del campo tal como aparece en el formulario. Si dos campos tienen el mismo título, el segundo llega como `"Título (2)"`.                                                               |
| `campos`       | Los mismos datos en lista, con el identificador y el tipo de cada campo. Úselo si los títulos del formulario pueden cambiar: el identificador no cambia. Los campos de archivo traen además `archivos`, con los enlaces de descarga. |
| `id_entrega`   | Identificador del envío. Sirve para no procesar dos veces el mismo registro.                                                                                                                                                         |
| `id_documento` | Número interno del radicado, si el formulario radica en un proceso; `null` si no.                                                                                                                                                    |
| `prueba`       | Solo aparece en los [envíos de prueba](#enviar-una-prueba), con el valor `true`.                                                                                                                                                     |

{% hint style="warning" %}
**Cambio de octubre de 2026:** los envíos ya no incluyen `formulario`, la estructura completa del formulario con grupos, filas y campos. Si su integración la leía, use `campos`, que trae el identificador, el título, el tipo y el valor de cada campo.
{% endhint %}

Formato de los valores:

| Tipo de campo                          | Valor que llega                                                                         |
| -------------------------------------- | --------------------------------------------------------------------------------------- |
| Texto, correo, teléfono, lista, opción | Texto                                                                                   |
| Número                                 | Texto con el número, por ejemplo `"2"`                                                  |
| Fecha                                  | `AAAA-MM-DD`                                                                            |
| Casillas de selección múltiple         | Opciones marcadas, separadas por coma                                                   |
| Archivo                                | Nombres de los archivos, separados por coma. Los enlaces van en [`archivos`](#adjuntos) |
| Firma                                  | `"Firmado"` si se firmó; vacío si no                                                    |
| Imagen                                 | `"(imagen)"` si se cargó una imagen                                                     |
| Casilla única y términos y condiciones | `true` o `false`                                                                        |
| Tabla                                  | Lista de filas. Las celdas de archivo traen el mismo objeto que [`archivos`](#adjuntos) |

Los elementos de diseño del formulario (títulos, textos e imágenes fijas) no se envían. Las firmas y las imágenes tampoco viajan en el envío: se consultan en Mensajero Digital.

### Adjuntos

Cada archivo de un campo de archivo llega en `archivos`, y cada celda de archivo de una tabla trae el mismo objeto:

| Dato        | Contenido                                         |
| ----------- | ------------------------------------------------- |
| `nombre`    | Nombre del archivo, con su extensión.             |
| `tipo`      | Tipo de contenido, por ejemplo `application/pdf`. |
| `tamano`    | Tamaño en bytes.                                  |
| `url`       | Enlace de descarga. Vence en **una hora**.        |
| `url_vence` | Momento en que vence el enlace (UTC).             |

{% hint style="warning" %}
**Descargue los archivos al recibir el envío.** El enlace sirve solo una hora y no se puede renovar desde su lado: guarde el archivo, no el enlace. Si su sistema procesa los envíos en segundo plano, hágalo dentro de esa hora.
{% endhint %}

* Cada intento genera enlaces nuevos: si el envío se reintenta o alguien lo reenvía desde el [historial](#historial-de-envios), los enlaces llegan vigentes.
* Si un enlace no se pudo generar, `url` llega en `null`. El archivo sigue disponible en Mensajero Digital.

## Verificar que el envío viene de Mensajero Digital

Cada envío trae estas cabeceras:

| Cabecera                | Contenido                                                                 |
| ----------------------- | ------------------------------------------------------------------------- |
| `X-Mensajero-Evento`    | `registro.creado`                                                         |
| `X-Mensajero-Entrega`   | Identificador del envío. No cambia si el envío se reintenta o se reenvía. |
| `X-Mensajero-Intento`   | `1` en el primer envío, `2` en el primer reintento, y así sucesivamente.  |
| `X-Mensajero-Timestamp` | Momento del envío, en segundos desde 1970 (UTC).                          |
| `X-Mensajero-Firma`     | `sha256=` seguido de la firma.                                            |
| `X-Mensajero-Prueba`    | `1`. Solo en los [envíos de prueba](#enviar-una-prueba).                  |

Para verificar la firma:

1. Tome el valor de `X-Mensajero-Timestamp` y el **cuerpo exacto** de la petición, tal como llegó, sin volver a convertirlo a JSON.
2. Calcule HMAC-SHA256 con la clave de firma sobre el texto `timestamp` + `.` + `cuerpo`, en hexadecimal.
3. Compare el resultado con lo que viene después de `sha256=` en `X-Mensajero-Firma`. Si no coinciden, rechace el envío.
4. Recomendado: rechace también los envíos cuyo timestamp tenga más de 5 minutos de diferencia con su reloj.

{% tabs %}
{% tab title="Node.js" %}

```javascript
import crypto from "crypto";
import express from "express";

const app = express();
const CLAVE = process.env.MENSAJERO_WEBHOOK_CLAVE;

app.post("/mensajero", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Mensajero-Timestamp");
  const firma = (req.get("X-Mensajero-Firma") || "").replace("sha256=", "");
  const cuerpo = req.body.toString("utf8");

  const esperada = crypto
    .createHmac("sha256", CLAVE)
    .update(`${timestamp}.${cuerpo}`)
    .digest("hex");

  const valida =
    firma.length === esperada.length &&
    crypto.timingSafeEqual(Buffer.from(firma), Buffer.from(esperada));
  const reciente = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;

  if (!valida || !reciente) return res.sendStatus(401);

  const datos = JSON.parse(cuerpo);
  // Procese datos.valores
  res.sendStatus(200);
});
```

{% endtab %}

{% tab title="Python" %}

```python
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
CLAVE = os.environ["MENSAJERO_WEBHOOK_CLAVE"]


@app.post("/mensajero")
def recibir():
    timestamp = request.headers.get("X-Mensajero-Timestamp", "")
    firma = request.headers.get("X-Mensajero-Firma", "").removeprefix("sha256=")
    cuerpo = request.get_data()  # bytes exactos, tal como llegaron

    esperada = hmac.new(
        CLAVE.encode(), f"{timestamp}.".encode() + cuerpo, hashlib.sha256
    ).hexdigest()

    reciente = timestamp.isdigit() and abs(time.time() - int(timestamp)) < 300
    if not hmac.compare_digest(firma, esperada) or not reciente:
        abort(401)

    datos = json.loads(cuerpo)
    # Procese datos["valores"]
    return "", 200
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**¿Su herramienta no puede calcular la firma?** Algunas plataformas de automatización sin código no lo permiten. En ese caso, use al menos una URL con `https://`, difícil de adivinar, y compártala solo con quien la necesita. Si su firewall lo permite, active además la [IP de salida fija](#ip-de-salida-fija) y acepte solo esa dirección.
{% endhint %}

## IP de salida fija

Si el sistema que recibe los datos solo acepta conexiones desde direcciones IP autorizadas en su firewall, active la IP fija en el webhook. Desde ese momento, todos sus envíos salen desde la misma dirección:

```
136.116.202.210
```

{% stepper %}
{% step %}

### Active la casilla

En la pestaña **Webhooks**, presione el botón del lápiz del webhook (o **Nuevo Webhook** si aún no existe). Marque **Enviar desde la IP fija de Mensajero Digital** y presione **Guardar Cambios**.

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FLp4GRvQ2Ucy6DYC9eIDo%2Fips4.jpg?alt=media&amp;token=3dc2e6a9-67e0-4c97-976a-e950c960d282" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Autorice la IP en su firewall

Pídale a quien administra el sistema que recibe los datos que autorice `136.116.202.210`.
{% endstep %}

{% step %}

### Compruébelo con una prueba

Presione el botón del avión de papel. La ventana **Enviar prueba** indica que el envío sale desde la IP fija. Si su firewall la deja pasar, la prueba queda **Entregada**.
{% endstep %}
{% endstepper %}

En la lista de webhooks, la etiqueta **IP fija** junto a la URL indica cuáles la tienen activa.

<figure><img src="https://2099257791-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LT3uJQOuTNAS8sLraYy%2Fuploads%2FLoEbElD7CRvcUifPJOdv%2Fips3.jpg?alt=media&amp;token=dbd5e7e5-0263-4043-a01b-11b858d1b364" alt=""><figcaption></figcaption></figure>

* Aplica a todo lo que envía ese webhook: los registros nuevos, los reintentos, los reenvíos del historial y las pruebas.
* Sin la casilla, los envíos salen desde direcciones de Google Cloud que cambian: no sirve autorizar una de ellas.
* Si en algún momento la IP fija no está disponible, el envío se reintenta como cualquier otro fallo. Nunca sale desde otra dirección.

{% hint style="warning" %}
**La IP fija no reemplaza la firma.** Es la misma para todos los clientes de Mensajero Digital que la activan: indica desde qué red llegó el envío, no quién lo mandó. Verifique también la [firma](#verificar-que-el-envio-viene-de-mensajero-digital).
{% endhint %}

## Respuestas, reintentos y duplicados

* Su sistema debe responder con un código **2xx en menos de 30 segundos**. Si tiene que procesar algo pesado, guarde el envío y responda primero (y descargue los adjuntos dentro de la hora siguiente).
* Cualquier otra respuesta (un error, una demora de más de 30 segundos o la URL caída) cuenta como fallo. Mensajero Digital reintenta el envío **5 veces más**: a 1 minuto, 5 minutos, 30 minutos, 2 horas y 6 horas. Después deja de intentarlo y el envío queda como **Fallido** en el [historial](#historial-de-envios), desde donde lo puede reenviar.
* Un mismo registro puede llegar más de una vez, por ejemplo si su sistema respondió tarde o si alguien lo reenvía desde el historial. Use `X-Mensajero-Entrega` o `id_entrega` para no procesarlo dos veces.
* Los envíos de prueba no se reintentan.
