# Webhooks de documentos Fuente: /webhooks/document-webhooks/ Cuando [creas un documento](/document-management/create-document) con el campo `webhook`, Verificamex Firma te avisa cada vez que ese documento se crea o cambia: cuando subes el PDF, cuando alguien firma, cuando alguien rechaza y cuando el documento queda firmado por completo. Cada aviso es una petición `POST` a tu URL con el **estado completo del documento** en ese momento, en formato JSON. *** ### Cómo se entrega | Aspecto | Comportamiento | | :- | :- | | Método | `POST` | | Encabezados | `Content-Type: application/json`, más `X-Verificamex-Firma-Signature` (la firma del aviso) y `X-Verificamex-Firma-Event-Id` (su identificador, igual en los reintentos). Ver [Seguridad de webhooks](/webhooks/webhooks-security). | | Cuerpo | El documento completo (ver [Campos del documento](#campos-del-documento)). No hay un campo de "tipo de evento": el evento se deduce de los valores (ver [Cómo identificar qué pasó](#cómo-identificar-qué-pasó)). | | Tiempo de envío | Los avisos se envían en ciclos de un minuto, así que pueden tardar hasta \~1 minuto en llegar después del cambio. | | Éxito | Cualquier respuesta `2xx`. Responde en menos de 10 segundos; si necesitas procesar algo pesado, responde primero y procesa después. | | Reintentos | Si tu servidor responde con otro código o no responde, el aviso se reintenta en el siguiente ciclo, hasta **3 intentos en total**. Después se descarta. | > **Procesa los avisos de forma idempotente** > > Puedes recibir el mismo estado más de una vez, o recibir un estado viejo después de uno nuevo. Guarda el `updated_at` del último aviso que procesaste para cada `id` de documento e ignora los que tengan un `updated_at` menor o igual. > **Valida la firma de cada aviso** > > Cada aviso va firmado con tu clave de webhooks. Valídala antes de procesarlo para asegurarte de que lo envió Verificamex Firma y de que nadie lo modificó. Te explicamos cómo en [Seguridad de webhooks](/webhooks/webhooks-security). *** ### ¿Cuándo se envía? Se envía un aviso cada vez que el documento se guarda con cambios. Estos son los momentos principales: | Momento | `signed` | Qué cambia en el cuerpo | | :- | :-: | :- | | Se crea el documento | `1` | Pueden llegar **varios avisos casi al mismo tiempo**, con todos los firmantes en `PENDING`. Si lo creaste por API, `file` viene vacío (`""`) y `spectators` viene vacío aunque hayas enviado espectadores. | | Se sube el PDF | `1` | `file` trae el nombre del archivo y `spectators` ya incluye a los espectadores. | | Firma un firmante y aún faltan otros | `2` | Ese firmante pasa a `SIGNED` con `signedAt` y `signature`. `signed_at` del documento se actualiza. | | Firma el último firmante | `3` | Todos los firmantes en `SIGNED`. `xml_with_signs` deja de ser `null`. | | Un firmante rechaza | `4` | Ese firmante pasa a `REJECTED`. `reject_commentary` trae su comentario. El resto se queda en `PENDING`. | Otros cambios en el documento, como [actualizar sus firmantes](/document-management/update-document-signers), también envían un aviso con el estado actual. *** ### Campos del documento | Campo | Tipo | Valores posibles / Descripción | | :- | :- | :- | | `id` | `string` | Identificador del documento (UUID). | | `file` | `string` | Nombre del archivo PDF. Vacío (`""`) si el documento se creó por API y aún no se sube el PDF. | | `tries` | `int` | Número máximo de recordatorios configurado al crear el documento. | | `remember_every` | `int` | Cada cuántos días se envían los recordatorios. | | `remember_at` | `string` | Hora de los recordatorios (`HH:mm`). | | `signed` | `int` | Estado del documento. Ver [Estados del documento](#estados-del-documento-signed). | | `signed_at` | `string` \| `null` | Fecha y hora del último movimiento de firma (una firma o un rechazo). Con `signed: 3` es el momento en que se completó. `null` mientras nadie haya firmado ni rechazado. | | `xml_with_signs` | `string` \| `null` | Referencia interna al XML con las firmas. Solo tiene valor con `signed: 3`. **No es una URL**: para obtener el archivo usa [Descargar XML](/document-management/dowload-xml). | | `webhook` | `string` | La URL de webhook configurada (la tuya). | | `redirect_url` | `string` | URL de redirección del documento. **Solo aparece si la configuraste**; si no, el campo no viene. | | `reject_commentary` | `string` \| `null` | Comentario que dejó quien rechazó. Puede venir con formato HTML (por ejemplo, `

No estoy de acuerdo

`). `null` si nadie ha rechazado. | | `signatures` | `Array` | Firmantes del documento. Ver [Campos de cada firmante](#campos-de-cada-firmante). | | `spectators` | `Array` | Espectadores del documento. Puede venir vacío; en los avisos de creación siempre viene vacío y los espectadores aparecen desde el aviso de subida del PDF. Ver [Campos de cada espectador](#campos-de-cada-espectador). | | `sign_position` | `string` | `UPPER_RIGHT`, `UPPER_LEFT`, `LOWER_RIGHT`, `LOWER_LEFT`, `NONE` o `CUSTOM`. | | `mode` | `string` | Modo de firmado: `CLASSIC`, `PDF_SIGNATURE` o `CLASSIC_BATCH`. | | `notification_mode` | `string` | `ENABLED` o `DISABLED`. | | `logo_mode` | `string` | `FULL_BRANDING`, `MEDIUM_BRANDING`, `NO_BRANDING` o `NO_SIGNATURE_SHEET`. | | `ordered` | `string` | `ACTIVE` (firma en orden) o `INACTIVE`. | | `company_id` | `string` | Carpeta de trabajo del documento. | | `created_at` | `string` | Fecha de creación del documento. | | `updated_at` | `string` | Fecha del último cambio. Úsala para ordenar los avisos. | > **Algunos nombres cambian respecto a la creación** > > Tres campos se llaman distinto en el webhook que en la petición de [crear documento](/document-management/create-document): `sign_mode` llega como `mode`, `mode_logo` llega como `logo_mode` y `sign_ordered` llega como `ordered`. Las fechas vienen en formato ISO 8601 sin zona horaria (por ejemplo, `2026-05-04T20:14:56.945158`). La cantidad de decimales de los segundos puede variar. #### Estados del documento (`signed`) | Valor | Significado | | :-: | :- | | `1` | Sin firmas. Nadie ha firmado ni rechazado todavía. | | `2` | Firmado parcialmente. Al menos un firmante firmó y faltan otros. | | `3` | Firmado. Todos los firmantes firmaron. | | `4` | Rechazado. Un firmante rechazó el documento y el proceso termina. | #### Campos de cada firmante | Campo | Tipo | Valores posibles / Descripción | | :- | :- | :- | | `id` | `string` | Identificador del firmante. Es el `signatureId` que se usa en [Firma vía API](/sign-via-api/sign-via-api-introduction). | | `signatureId` | `string` | Mismo valor que `id`. | | `name` | `string` | Nombre del firmante. | | `email` | `string` | Correo del firmante. | | `phone` | `string` \| `null` | Teléfono del firmante, si se envió. | | `rfc` | `string` \| `null` | RFC del firmante, si se conoce. | | `curp` | `string` \| `null` | CURP del firmante, si se conoce. | | `nameSource`, `emailSource`, `rfcSource`, `curpSource` | `string` \| `null` | Copias de `name`, `email`, `rfc` y `curp`. Usa los campos principales. | | `order` | `int` | Posición del firmante, empezando en `0`. Importa cuando `ordered` es `ACTIVE`. | | `signed` | `string` | Estado del firmante: `PENDING`, `SIGNED` o `REJECTED`. | | `signedAt` | `string` \| `null` | Fecha en que firmó. `null` si no ha firmado (también si rechazó). | | `signature` | `string` \| `null` | Firma digital en Base64. `null` si no ha firmado. | | `try` | `int` | Número de recordatorio en el que va el firmante. Empieza en `1`. | | `rememberAt` | `string` | Fecha del siguiente recordatorio (`YYYY-MM-DD`). | | `positions` | `Array` \| `null` | Posiciones de firma si el documento usa `sign_position: "CUSTOM"`; cada una con `posX`, `posY`, `width`, `height`, `page` y `rotation`. `null` en otro caso. | | `allowedSignatureMethods` | `Array` \| `null` | Métodos de firma permitidos para este firmante (`FIREL`, `EFIRMA`, `IDENTITY`, `SIMPLE`). `null` si no se limitaron. | | `createdAt` | `string` | Fecha de alta del firmante. | | `updatedAt` | `string` | Fecha del último cambio del firmante. | #### Campos de cada espectador | Campo | Tipo | Descripción | | :- | :- | :- | | `id` | `int` | Identificador numérico del espectador. | | `name` | `string` | Nombre del espectador. | | `email` | `string` | Correo del espectador. | | `createdAt` | `string` | Fecha de alta del espectador. | | `updatedAt` | `string` | Fecha del último cambio del espectador. | *** ### Ejemplos por estado Los ejemplos usan un documento con dos firmantes (Juan Perez y Maria Lopez) y un espectador. Los datos son ficticios. **1 · Sin firmas** Documento con el PDF ya subido y ningún firmante ha actuado. ```json { "id": "ac110005-9df4-1d1b-819d-f5eac471003a", "file": "contrato-servicios.pdf", "tries": 3, "remember_every": 3, "remember_at": "12:30", "signed": 1, "signed_at": null, "xml_with_signs": null, "webhook": "https://tudominio.com/webhooks/verificamex-firma", "redirect_url": "https://tudominio.com/fin-firma", "reject_commentary": null, "signatures": [ { "id": "ac110005-9df4-1d1b-819d-f5eac478003b", "email": "jpe@gmail.com", "phone": "+522291449388", "name": "Juan Perez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": null, "signed": "PENDING", "try": 1, "createdAt": "2026-05-04T20:14:56.888302", "updatedAt": "2026-05-04T20:14:56.888310", "positions": null, "allowedSignatureMethods": null, "order": 0, "signatureId": "ac110005-9df4-1d1b-819d-f5eac478003b", "emailSource": "jpe@gmail.com", "signedAt": null, "nameSource": "Juan Perez", "rfcSource": null, "curpSource": null }, { "id": "ac110005-9df4-1d1b-819d-f5eac47a003c", "email": "mlopez@tudominio.com", "phone": null, "name": "Maria Lopez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": null, "signed": "PENDING", "try": 1, "createdAt": "2026-05-04T20:14:56.891004", "updatedAt": "2026-05-04T20:14:56.891010", "positions": null, "allowedSignatureMethods": null, "order": 1, "signatureId": "ac110005-9df4-1d1b-819d-f5eac47a003c", "emailSource": "mlopez@tudominio.com", "signedAt": null, "nameSource": "Maria Lopez", "rfcSource": null, "curpSource": null } ], "spectators": [ { "id": 88416, "name": "Hector Lavoe", "email": "hl@hotmail.com", "createdAt": "2026-05-04T20:14:56.918561", "updatedAt": "2026-05-04T20:14:56.918583" } ], "sign_position": "LOWER_RIGHT", "mode": "PDF_SIGNATURE", "notification_mode": "ENABLED", "logo_mode": "FULL_BRANDING", "ordered": "INACTIVE", "company_id": "ac110005-9d0c-1243-819d-0c3b71220000", "created_at": "2026-05-04T20:14:56.945158", "updated_at": "2026-05-04T20:16:10.402771" } ``` **2 · Firmado parcialmente** Juan Perez ya firmó; falta Maria Lopez. ```json { "id": "ac110005-9df4-1d1b-819d-f5eac471003a", "file": "contrato-servicios.pdf", "tries": 3, "remember_every": 3, "remember_at": "12:30", "signed": 2, "signed_at": "2026-05-05T17:02:41.633120415", "xml_with_signs": null, "webhook": "https://tudominio.com/webhooks/verificamex-firma", "redirect_url": "https://tudominio.com/fin-firma", "reject_commentary": null, "signatures": [ { "id": "ac110005-9df4-1d1b-819d-f5eac478003b", "email": "jpe@gmail.com", "phone": "+522291449388", "name": "Juan Perez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": "", "signed": "SIGNED", "try": 1, "createdAt": "2026-05-04T20:14:56.888302", "updatedAt": "2026-05-05T17:02:40.715", "positions": null, "allowedSignatureMethods": null, "order": 0, "signatureId": "ac110005-9df4-1d1b-819d-f5eac478003b", "emailSource": "jpe@gmail.com", "signedAt": "2026-05-05T17:02:40.702", "nameSource": "Juan Perez", "rfcSource": null, "curpSource": null }, { "id": "ac110005-9df4-1d1b-819d-f5eac47a003c", "email": "mlopez@tudominio.com", "phone": null, "name": "Maria Lopez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": null, "signed": "PENDING", "try": 1, "createdAt": "2026-05-04T20:14:56.891004", "updatedAt": "2026-05-04T20:14:56.891010", "positions": null, "allowedSignatureMethods": null, "order": 1, "signatureId": "ac110005-9df4-1d1b-819d-f5eac47a003c", "emailSource": "mlopez@tudominio.com", "signedAt": null, "nameSource": "Maria Lopez", "rfcSource": null, "curpSource": null } ], "spectators": [ { "id": 88416, "name": "Hector Lavoe", "email": "hl@hotmail.com", "createdAt": "2026-05-04T20:14:56.918561", "updatedAt": "2026-05-04T20:14:56.918583" } ], "sign_position": "LOWER_RIGHT", "mode": "PDF_SIGNATURE", "notification_mode": "ENABLED", "logo_mode": "FULL_BRANDING", "ordered": "INACTIVE", "company_id": "ac110005-9d0c-1243-819d-0c3b71220000", "created_at": "2026-05-04T20:14:56.945158", "updated_at": "2026-05-05T17:02:41.633201" } ``` **3 · Firmado** Ambos firmantes firmaron. `xml_with_signs` ya tiene valor. ```json { "id": "ac110005-9df4-1d1b-819d-f5eac471003a", "file": "contrato-servicios.pdf", "tries": 3, "remember_every": 3, "remember_at": "12:30", "signed": 3, "signed_at": "2026-05-06T10:21:05.118402331", "xml_with_signs": "dueno@tudominio.com/2026/5/ac110005-9df4-1d1b-819d-f5eac471003a.xml", "webhook": "https://tudominio.com/webhooks/verificamex-firma", "redirect_url": "https://tudominio.com/fin-firma", "reject_commentary": null, "signatures": [ { "id": "ac110005-9df4-1d1b-819d-f5eac478003b", "email": "jpe@gmail.com", "phone": "+522291449388", "name": "Juan Perez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": "", "signed": "SIGNED", "try": 1, "createdAt": "2026-05-04T20:14:56.888302", "updatedAt": "2026-05-05T17:02:40.715", "positions": null, "allowedSignatureMethods": null, "order": 0, "signatureId": "ac110005-9df4-1d1b-819d-f5eac478003b", "emailSource": "jpe@gmail.com", "signedAt": "2026-05-05T17:02:40.702", "nameSource": "Juan Perez", "rfcSource": null, "curpSource": null }, { "id": "ac110005-9df4-1d1b-819d-f5eac47a003c", "email": "mlopez@tudominio.com", "phone": null, "name": "Maria Lopez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": "", "signed": "SIGNED", "try": 1, "createdAt": "2026-05-04T20:14:56.891004", "updatedAt": "2026-05-06T10:21:04.092", "positions": null, "allowedSignatureMethods": null, "order": 1, "signatureId": "ac110005-9df4-1d1b-819d-f5eac47a003c", "emailSource": "mlopez@tudominio.com", "signedAt": "2026-05-06T10:21:04.080", "nameSource": "Maria Lopez", "rfcSource": null, "curpSource": null } ], "spectators": [ { "id": 88416, "name": "Hector Lavoe", "email": "hl@hotmail.com", "createdAt": "2026-05-04T20:14:56.918561", "updatedAt": "2026-05-04T20:14:56.918583" } ], "sign_position": "LOWER_RIGHT", "mode": "PDF_SIGNATURE", "notification_mode": "ENABLED", "logo_mode": "FULL_BRANDING", "ordered": "INACTIVE", "company_id": "ac110005-9d0c-1243-819d-0c3b71220000", "created_at": "2026-05-04T20:14:56.945158", "updated_at": "2026-05-06T10:21:05.118477" } ``` **4 · Rechazado** Juan Perez rechazó el documento; Maria Lopez se queda en `PENDING` y el proceso termina. ```json { "id": "ac110005-9df4-1d1b-819d-f5eac471003a", "file": "contrato-servicios.pdf", "tries": 3, "remember_every": 3, "remember_at": "12:30", "signed": 4, "signed_at": "2026-05-05T17:02:41.633120415", "xml_with_signs": null, "webhook": "https://tudominio.com/webhooks/verificamex-firma", "redirect_url": "https://tudominio.com/fin-firma", "reject_commentary": "

El monto del contrato no es correcto.

", "signatures": [ { "id": "ac110005-9df4-1d1b-819d-f5eac478003b", "email": "jpe@gmail.com", "phone": "+522291449388", "name": "Juan Perez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": null, "signed": "REJECTED", "try": 1, "createdAt": "2026-05-04T20:14:56.888302", "updatedAt": "2026-05-05T17:02:40.715", "positions": null, "allowedSignatureMethods": null, "order": 0, "signatureId": "ac110005-9df4-1d1b-819d-f5eac478003b", "emailSource": "jpe@gmail.com", "signedAt": null, "nameSource": "Juan Perez", "rfcSource": null, "curpSource": null }, { "id": "ac110005-9df4-1d1b-819d-f5eac47a003c", "email": "mlopez@tudominio.com", "phone": null, "name": "Maria Lopez", "rememberAt": "2026-05-07", "rfc": null, "curp": null, "signature": null, "signed": "PENDING", "try": 1, "createdAt": "2026-05-04T20:14:56.891004", "updatedAt": "2026-05-04T20:14:56.891010", "positions": null, "allowedSignatureMethods": null, "order": 1, "signatureId": "ac110005-9df4-1d1b-819d-f5eac47a003c", "emailSource": "mlopez@tudominio.com", "signedAt": null, "nameSource": "Maria Lopez", "rfcSource": null, "curpSource": null } ], "spectators": [ { "id": 88416, "name": "Hector Lavoe", "email": "hl@hotmail.com", "createdAt": "2026-05-04T20:14:56.918561", "updatedAt": "2026-05-04T20:14:56.918583" } ], "sign_position": "LOWER_RIGHT", "mode": "PDF_SIGNATURE", "notification_mode": "ENABLED", "logo_mode": "FULL_BRANDING", "ordered": "INACTIVE", "company_id": "ac110005-9d0c-1243-819d-0c3b71220000", "created_at": "2026-05-04T20:14:56.945158", "updated_at": "2026-05-05T17:02:41.633201" } ``` *** ### Cómo identificar qué pasó Como el cuerpo siempre es el documento completo, compara lo que recibes con lo último que guardaste: 1. **Valida la firma:** si no coincide, responde `401` y descarta el aviso (ver [Seguridad de webhooks](/webhooks/webhooks-security)). 2. **Descarta avisos viejos:** si `updated_at` es menor o igual al último que procesaste para ese `id`, ignóralo. 3. **Revisa `signed`:** `3` significa que ya puedes [descargar el PDF firmado](/document-management/download-signed-pdf); `4` significa que el proceso terminó por rechazo y el motivo está en `reject_commentary`. 4. **Revisa cada firmante:** el que cambió de `PENDING` a `SIGNED` o `REJECTED` es quien acaba de actuar. ```js // Ejemplo con Express. verifyVerificamexFirmaSignature está en "Seguridad de webhooks". app.post('/webhooks/verificamex-firma', express.raw({ type: 'application/json' }), async (req, res) => { if (!verifyVerificamexFirmaSignature(req.body, req.get('X-Verificamex-Firma-Signature'), WEBHOOK_SECRET)) { return res.sendStatus(401); // No lo envió Verificamex Firma o lo modificaron. } res.sendStatus(200); // Responde rápido; procesa después. const doc = JSON.parse(req.body.toString('utf8')); const previous = await db.documents.find(doc.id); if (previous && previous.updated_at >= doc.updated_at) return; // Aviso viejo o repetido. switch (doc.signed) { case 1: /* Sin firmas */ break; case 2: /* Firmado parcialmente */ break; case 3: /* Firmado: descarga el PDF firmado */ break; case 4: /* Rechazado: doc.reject_commentary */ break; } await db.documents.save(doc); }); ```