Saltar al contenido

Documentación

Lo que pasa con el documento, avisado y firmado.

Un llamado para mandarlo y un webhook que te avisa cuando está firmado. Aquí están los eventos, el cuerpo de cada aviso y cómo comprobar que viene de ValidaFirma.

01

Cuándo llega un aviso

Cada documento lleva su propia URL de aviso: la mandas en el campo webhook_url al crearlo, junto al PDF y los firmantes. Desde ahí, ValidaFirma hace un POST a esa URL cuando el documento termina —firmado, rechazado, cancelado, vencido o no emitido— y cuando algo le impide avanzar a un firmante. No hay aviso al crearlo ni por cada firma, y no tienes que consultar el estado en un bucle.

POST /api/fes/documentoscon webhook_url
curl -X POST https://api.validafirma.cl/api/fes/documentos \
  -H "X-API-Key: $VALIDAFIRMA_API_KEY" \
  -F "documento=@contrato.pdf" \
  -F 'firmantes=[{"rut":"49999901-1","canal":"whatsapp","telefono":"+56912345678"}]' \
  -F "webhook_url=https://tu-sistema.cl/webhooks/validafirma"

Qué URL se acepta

  • https, en el puerto 443.
  • Sin usuario ni clave escritos en la URL.
  • Hacia una dirección pública: ni privada, ni local, ni reservada.

Si no cumple, la creación responde 400 WEBHOOK_URL_NO_PERMITIDA con unmotivo que dice qué falta. Un nombre que todavía no resuelve no impide crear el documento: la dirección se comprueba otra vez en cada entrega.

Aparte del webhook, y en el mismo momento, al emisor le llega un correo por cada evento, haya o nowebhook_url. Hay dos excepciones: documento.expirado va sólo por webhook, y firmante.identidad_aprobada no manda correo si el firmante ya firmó. En modo de prueba ese correo queda en la bandeja de mensajes simulados, y en la cuenta corporativa no sale si no hay a quién mandarlo. Los documentos que entran por la carpeta en la nube no mandan webhook.

02

Los eventos

Diez eventos: cinco cierran el documento y cinco cuentan algo de un firmante mientras el documento sigue abierto. El nombre del evento va en evento y en la cabecera X-ValidaFirma-Event.

Los eventos del webhook, cuándo llega cada uno y qué trae en datos
EventoCuándo llegaQué trae
documento.completadoFirmó el último firmantesello, y por firmante la verificación
documento.rechazadoUn firmante rechazó{ motivo } (puede ser null)
documento.canceladoEl emisor lo canceló{}
documento.expiradoVenció el plazo sin que firmaran todos{ fecha_expiracion, firmantes_pendientes }
documento.no_emitidoEl documento no se pudo emitir al cierre{ no_emitido_at }
firmante.bloqueadoCinco verificaciones fallidas{ bloqueado_hasta }
firmante.identidad_no_coincideLa cédula presentada no es la del RUT declarado{ motivo: "rut_mismatch", fecha }
firmante.identidad_aprobadaUna revisión humana aprobó la identidad (canal enlace){ url_firma }
firmante.invitacion_no_entregadaNo llegaron ni el WhatsApp ni el SMS{ canal, intentos: ["whatsapp", "sms"] }
firmante.validacion_contacto_no_logradaNo logró validar su correo o su celular{ medio, bloqueado_hasta }
  • documento.rechazado cierra el documento para todos los firmantes, que queda en estado rechazado. Su motivo puede venir en null. En documento.cancelado, firmante viene en null.
  • documento.no_emitido: un documento que no pudo sellarse nunca se entrega como firmado: queda como no emitido y te avisamos.
  • documento.expirado llega a más tardar una hora después del plazo. Un documento vencido no se reactiva.
  • firmante.identidad_no_coincide llega cada vez que pasa, no sólo al quinto intento. Suma al mismo tope de cinco intentos que bloquea 24 horas, el documento sigue pendiente y el firmante puede verificarse de nuevo con su cédula. No trae ningún dato de quien se presentó: el firmante del aviso es el que tú declaraste.
  • firmante.identidad_aprobada es para el canal enlace: tú entregas laurl_firma que trae, para que el firmante siga.
03

El cuerpo

Cada aviso es un POST con Content-Type: application/json y estas cabeceras:

Cabeceras
POST https://tu-sistema.cl/webhooks/validafirma
Content-Type: application/json
User-Agent: ValidaFirma-Webhooks/1.0 (+https://validafirma.cl)
X-Webhook-Source: validafirma.cl
X-ValidaFirma-Event: documento.completado
X-ValidaFirma-Version: 2.0
X-ValidaFirma-Delivery: documento.completado-9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44
X-Webhook-ID: documento.completado-9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44
X-Webhook-Signature: t=1759414503,v1=6f1c0b2e9a…
User-Agent
ValidaFirma-Webhooks/1.0 (+https://validafirma.cl).
X-Webhook-Source
validafirma.cl.
X-ValidaFirma-Event
El evento, el mismo que trae el cuerpo en evento. Te deja rutear antes de leer el cuerpo.
X-ValidaFirma-Version
La versión del contrato: 2.0.
X-ValidaFirma-Delivery
Un identificador opaco de la entrega, en texto: no lo interpretes. Es el mismo en cada reintento: con él deduplicas.
X-Webhook-ID
El mismo valor que X-ValidaFirma-Delivery.
X-Webhook-Signature
t=<unix>,v1=<hex>: la hora del envío, en segundos, y la firma del cuerpo, en el mismo formato de rCAPI.

documento.completado

El aviso de que firmó el último firmante tiene un cuerpo propio: el documento, ya en estadofirmado, con su sello (la huella del certificado y la hora), los enlaces de descarga y de validación, y un arreglo firmantes con cómo se verificó cada uno. El PDF no viaja en el aviso: documento_base64 llega siempre en null e incluye_documento en false, y lo bajas conurl_descarga o con la API. La versión del contrato es la de la cabeceraX-ValidaFirma-Version (2.0); el webhook_version del cuerpo se mantuvo en 1.0 por compatibilidad, y sólo viene en este evento.

documento.completadorecortado
{
  "evento": "documento.completado",
  "timestamp": "2026-10-02T13:15:09.000Z",
  "webhook_version": "1.0",
  "es_prueba": false,
  "sandbox": false,
  "documento": {
    "id": "9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44",
    "codigo_verificacion": "mu8k2x4p-9A4C71E8",
    "nombre_original": "contrato.pdf",
    "estado": "firmado",
    "completed_at": "2026-10-02T13:15:08.000Z",
    "hash_original": "e3b0c44298fc1c149afbf4c8996fb924…",
    "hash_final": "9a1f3c7d52e0b84c6d1f2a7e3b9c0d58…",
    "sello": {
      "presente": true,
      "huella": "4b2e…c91a",
      "huella_sha1": "7f03…e2d4",
      "fecha": "2026-10-02T13:15:08.000Z"
    },
    "url_descarga": "https://app.validafirma.cl/api/fes/publico/documento/mu8k2x4p-9A4C71E8/pdf",
    "url_validacion": "https://app.validafirma.cl/public/validar?codigo=mu8k2x4p-9A4C71E8",
    "incluye_documento": false,
    "documento_base64": null,
    "size_bytes": null,
    "documento_error": null
  },
  "firmantes": [
    {
      "nombre": "ANA MARÍA PÉREZ SOTO",
      "rut": "49999901-1",
      "estado": "firmado",
      "firma_timestamp": "2026-10-02T13:15:03.000Z",
      "verificacion": {
        "metodo": "pinrut",
        "nombre_verificado": "ANA MARÍA PÉREZ SOTO",
        "identificador_comprobante": "8b5f2c1e-4d7a-4c3b-9e21-6f0a1d2b3c4d",
        "fecha": "2026-10-02T13:15:03.000Z",
        "comprobante": "<JWS firmado por PINRUT>"
      },
      "medios_validados": [{ "medio": "telefono", "origen": "invitacion" }]
    }
  ]
}

El ejemplo está recortado. El aviso trae además, entre otros campos, el correo, el teléfono y la IP de firma de cada firmante, y datos del usuario emisor. La referencia completa está en el OpenAPI.

verificacion
Cómo se verificó la identidad: el método (pinrut), el nombre que verificó PINRUT ennombre_verificado, el identificador y la fecha del comprobante, y el comprobante mismo, firmado por PINRUT. En una firma con PINRUT, el nombre del firmante ya es el verificado, no el que declaraste. Puede venir en null, o con el métodootp en documentos antiguos.
medios_validados
Los medios de contacto que quedaron probados, cada uno con su medio, email o telefono, y su origen: invitacion si la invitación salió por ese medio y el firmante firmó, o codigo si lo validó con un código, el que pides con validar_contacto.

Los demás eventos

Todos los demás comparten una forma, sin webhook_version: evento,timestamp, es_prueba, sandbox,documento, firmante y datos, que cambia según el evento (la última columna de la tabla). En documento van id, codigo_verificacion, estado, es_prueba y sandbox; en firmante, que viene en null cuando el evento no es de un firmante, van id, rut, canal, estado, es_prueba y sandbox.

documento.rechazado
{
  "evento": "documento.rechazado",
  "timestamp": "2026-10-03T12:30:00.000Z",
  "es_prueba": false,
  "sandbox": false,
  "documento": {
    "id": "9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44",
    "codigo_verificacion": "mu8k2x4p-9A4C71E8",
    "estado": "rechazado",
    "es_prueba": false,
    "sandbox": false
  },
  "firmante": {
    "id": "4a7d2c19-6e83-4f1b-a5c0-9d2e7b3f8a61",
    "rut": "49999904-6",
    "canal": "whatsapp",
    "estado": "rechazado",
    "es_prueba": false,
    "sandbox": false
  },
  "datos": { "motivo": "el monto no es el acordado" }
}

Prueba o real

es_prueba y sandbox van en la raíz de todo aviso, con el mismo valor, y dicen si el documento es de prueba. Van siempre, también con false. Rutea por ese campo antes de abrir el documento: un aviso de prueba procesado como real es un error que después nadie ve. Fuera de esa marca, un aviso de prueba es idéntico a uno real. Más en el modo de prueba.

04

Comprobar la firma

Tu URL es pública, así que cualquiera le puede mandar un POST. La cabecera X-Webhook-Signature te deja comprobar que el aviso viene de ValidaFirma y que nadie tocó el cuerpo en el camino. Es un HMAC-SHA256, con el secreto de webhooks de tu cuenta, sobre "<t>.<cuerpo crudo>".

  1. 01Lee el cuerpo crudo, los bytes tal como llegaron, antes de que tu framework lo convierta en JSON.
  2. 02Separa t y cada v1 de la cabecera X-Webhook-Signature.
  3. 03Te recomendamos rechazar el aviso si t, en segundos, se aleja más de 300 de tu reloj.
  4. 04Calcula el HMAC-SHA256 de "<t>.<cuerpo crudo>" con tu secreto y compáralo en tiempo constante con cada v1. Pueden venir hasta dos, y basta con que calce uno.
Node.js
const crypto = require('crypto');

function esDeValidaFirma(cabecera, cuerpoCrudo, secreto, ahora = Math.floor(Date.now() / 1000)) {
  const partes = cabecera.split(',').map((p) => p.trim().split('='));
  const t = (partes.find(([k]) => k === 't') || [])[1];
  const firmas = partes.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || Math.abs(ahora - Number(t)) > 300) return false;
  const esperada = crypto.createHmac('sha256', secreto)
    .update(`${t}.`).update(cuerpoCrudo).digest();
  return firmas.some((v1) => {
    const recibida = Buffer.from(v1, 'hex');
    return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada);
  });
}
Python
import hmac, hashlib, time

def es_de_validafirma(cabecera: str, cuerpo_crudo: bytes, secreto: str) -> bool:
    partes = [p.strip().split("=", 1) for p in cabecera.split(",")]
    t = next((v for k, v in partes if k == "t"), None)
    firmas = [v for k, v in partes if k == "v1"]
    if not t or abs(time.time() - int(t)) > 300:
        return False
    esperada = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(esperada, v1) for v1 in firmas)

El cuerpo crudo importa: si lo vuelves a serializar desde el JSON ya interpretado, los bytes cambian y la firma no calza. Cada reintento se firma de nuevo, con su propio t.

El secreto

Cada cuenta tiene un secreto por modo: el real empieza con whsec_live_ y el de prueba conwhsec_test_. Un aviso se firma con el secreto del modo de su documento. El secreto nace solo, la primera vez que hace falta.

Lo ve y lo rota sólo una persona que administra la cuenta, con su sesión en el panel, en la tarjeta «Secreto de webhooks»: «Revelar» lo muestra, con «Copiar» y «Ocultar», y «Rotar» lo cambia. En la cuenta personal, su titular, desde «API Keys»; en la corporativa, quien tiene el rol VALIDAFIRMA_ADMIN de administrador, y quien sólo opera o consulta no tiene entrada a la sección. Ninguna llave de API lo puede leer: en la corporativa, una credencial de máquina recibe403 SECRETO_SOLO_CON_SESION_DE_PERSONA.

Rotarlo sin perder avisos

Al rotar, en el diálogo «Rotar el secreto de webhooks», eliges qué pasa con el secreto que estaba vigente, y confirmas con «Sí, rotar»:

Deja de firmar ahora

La opción marcada. El anterior deja de firmar en el acto y queda sólo el nuevo. Es la salida si el secreto se filtró.

Sigue firmando 24 horas

Durante 24 horas cada aviso trae dos v1, uno por secreto, con el mismot. Acepta el aviso si calza cualquiera: así cambias el secreto en tu servidor sin cortar.

05

Cómo responder

  1. 01

    Responde 2xx en menos de 30 segundos

    Comprueba la firma, guarda el aviso y responde. Lo pesado, como descargar el PDF o actualizar tu sistema, va después, en una cola.

  2. 02

    Deduplica con X-ValidaFirma-Delivery

    Un aviso que falla se reintenta: hasta 5 intentos en total, con esperas crecientes (1, 2, 4 y 8 segundos), así que el mismo aviso puede llegarte más de una vez. Si ya procesaste esa entrega, responde 2xx y no hagas nada.

  3. 03

    Un aviso fallido no deshace nada

    Si tu servidor no responde, el documento sigue firmado, rechazado o vencido igual. Un 3xx cuenta como entrega fallida y no se reintenta: no se siguen redirecciones. Un 4xx tampoco se reintenta, salvo 408, 425 y 429.

Después del aviso

El aviso cuenta qué pasó; el estado lo confirma la API. Antes de actuar sobre algo importante, consulta el documento con su id. Cuando llega documento.completado, el PDF firmado no viene en el aviso: descárgalo con la API, o desde la url_descarga que trae.

GET /api/fes/documentos/{id}
curl https://api.validafirma.cl/api/fes/documentos/9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44 \
  -H "X-API-Key: $VALIDAFIRMA_API_KEY"
GET /api/fes/documentos/{id}/descargar
curl https://api.validafirma.cl/api/fes/documentos/9f1c2a7e-4b3d-4e6f-8a21-5c0d7e9b1f44/descargar \
  -H "X-API-Key: $VALIDAFIRMA_API_KEY" \
  -o contrato-firmado.pdf
06

Preguntas

¿Puedo configurar una sola URL para todos los documentos?

No. La URL va en cada documento, en el campo webhook_url de la creación. Si todos tus documentos avisan al mismo lugar, mándala igual en cada llamada.

¿El aviso trae el PDF firmado?

No. El PDF nunca viaja en el aviso: documento_base64 llega siempre en null e incluye_documento en false. Lo descargas con url_descarga, que trae el mismo aviso, o con GET /api/fes/documentos/{id}/descargar.

¿Qué pasa si mi servidor está caído cuando llega el aviso?

Se reintenta, hasta 5 intentos en total, con esperas de 1, 2, 4 y 8 segundos. Un 3xx o un 4xx no se reintentan, salvo 408, 425 y 429. La operación no se deshace: el documento sigue en su estado. Si se agotan los intentos, consulta el estado con GET /api/fes/documentos/{id}. El correo que recibe el emisor no es un respaldo de los reintentos: sale en el momento del evento, aparte del webhook.

¿Los avisos de prueba se firman con el mismo secreto?

No. Cada cuenta tiene un secreto por modo: los avisos de documentos de prueba se firman con el que empieza con whsec_test_ y los reales con el whsec_live_. El modo va en es_prueba, en la raíz del aviso.

¿Puedo leer el secreto con mi llave de API?

No. El secreto lo ve y lo rota sólo una persona que administra la cuenta, con su sesión en el panel. Ninguna llave lo alcanza: en la cuenta corporativa, una credencial de máquina recibe 403 SECRETO_SOLO_CON_SESION_DE_PERSONA.

Recibe tu primer aviso en modo de prueba

Crea un documento con una llave de prueba y el RUT 49999901-1: firma solo, y documento.completado llega a tu URL, firmado con tu secreto de prueba.