Webhooks de documentos
Cuando creas un documento 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
Sección titulada «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. |
| Cuerpo | El documento completo (ver Campos del documento). No hay un campo de “tipo de evento”: el evento se deduce de los valores (ver 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. |
¿Cuándo se envía?
Sección titulada «¿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, también envían un aviso con el estado actual.
Campos del documento
Sección titulada «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. |
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. |
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, <p>No estoy de acuerdo</p>). null si nadie ha rechazado. |
signatures | Array | Firmantes del documento. Ver 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. |
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. |
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)
Sección titulada «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
Sección titulada «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. |
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
Sección titulada «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
Sección titulada «Ejemplos por estado»Los ejemplos usan un documento con dos firmantes (Juan Perez y Maria Lopez) y un espectador. Los datos son ficticios.
Documento con el PDF ya subido y ningún firmante ha actuado.
{ "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"}Juan Perez ya firmó; falta Maria Lopez.
{ "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": "<firma digital en Base64>", "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"}Ambos firmantes firmaron. xml_with_signs ya tiene valor.
{ "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": "<firma digital en Base64>", "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": "<firma digital en Base64>", "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"}Juan Perez rechazó el documento; Maria Lopez se queda en PENDING y el proceso termina.
{ "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": "<p>El monto del contrato no es correcto.</p>", "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ó
Sección titulada «Cómo identificar qué pasó»Como el cuerpo siempre es el documento completo, compara lo que recibes con lo último que guardaste:
- Valida la firma: si no coincide, responde
401y descarta el aviso (ver Seguridad de webhooks). - Descarta avisos viejos: si
updated_ates menor o igual al último que procesaste para eseid, ignóralo. - Revisa
signed:3significa que ya puedes descargar el PDF firmado;4significa que el proceso terminó por rechazo y el motivo está enreject_commentary. - Revisa cada firmante: el que cambió de
PENDINGaSIGNEDoREJECTEDes quien acaba de actuar.
// 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);});