Webhooks en Legalesign
Qué son los webhooks
Los webhooks son tus URLs donde Legalesign enviará actualizaciones de estado en tiempo real. Puedes crear tus propias URLs o sistemas de automatización (como MS Power Automate) pueden generarlas para iniciar un flujo de trabajo.
Por ejemplo, en esta demostración creamos una URL con Microsoft Power Automate. Añadimos esa URL como webhook en Legalesign para cuando los documentos son firmados. El flujo de Power Automate se activa cuando un documento es firmado y guarda automáticamente esos PDFs recién firmados en Sharepoint. Aprende más sobre MS Power Automate.
Nota importante - debes tener permisos de Administrador de equipo para que los eventos de equipo se envíen a tu webhook.
Por qué necesitas webhooks
Los webhooks te permiten mantener un sistema basado en eventos. Los webhooks te actualizarán sobre todos los eventos dentro de tus grupos. Podrías usarlos para crear tu propio panel en vivo y/o para sincronizar con tu propia base de datos.
Como se mencionó anteriormente, los webhooks generados desde sistemas de automatización como Power Automate te permiten iniciar procedimientos de almacenamiento, correos electrónicos, mantenimiento de registros, etc., que, combinados con la API, proporcionan todo lo que necesitas para la automatización de procesos.
También está disponible un feed de suscripción de websocket en tiempo real. Esto puede ser más adecuado cuando quieres actualizaciones en vivo del lado del cliente sin exponer un endpoint de webhook. El websocket expone un conjunto más amplio de eventos que los webhooks.
Sí, los webhooks parecen un lío, pero valen mucho la pena y son geniales para tu integración. Sigue leyendo.
Tipos de webhook
- Eventos en tiempo real
- Todos los eventos cada 6 minutos (legado)
- Al ocurrir un [evento] (legado)
Para webhooks legados ver: webhooks legados
Los webhooks se usan comúnmente para descargar un PDF firmado. Cuando creas el webhook aplica el filtro de evento 'Final PDF created'. En tu código, la solicitud entrante request.body es JSON, analízala y extrae documentId = ['data']['uuid']. Ahora emite la consulta API para descargar el PDF - GET https://eu-api.legalesign.com/api/v1/pdf/${documentId}/.
Cómo añadir o eliminar un webhook
Añadir o eliminar un webhook usando la aplicación web
Ve a API dashboard y accede a la sección Webhooks. El formulario tiene controles simples para añadir o eliminar webhooks. Opcionalmente, puedes aislar eventos de webhook a ciertos equipos y/o eventos.

Tus webhooks reciben eventos de todas las cuentas donde eres administrador, tanto de desarrollo como de producción. Usa el filtro de grupo para crear diferentes webhooks para tus varios grupos.
Añadir o eliminar un webhook usando la API REST
Consulta la sección 'webhook' al final del menú lateral izquierdo en la documentación de la API: REST API webhooks
Añadir o eliminar un webhook usando GraphQL
¡Nuevo! Si usas autenticación SRP y la interfaz GraphQL, tienes poderes CRUD completos. Revisa Legalesign GraphiQL Explorer. Los webhooks pueden listarse como un atributo del tipo User, y hay mutaciones para createWebhook, updateWebhook y deleteWebhook.
¿Qué contiene un webhook?
La forma más rápida de inspeccionar tus datos es añadir un webhook, enviar/firmar/rechazar algunos documentos de prueba en la aplicación web, y luego ver el registro de webhooks en el Legalesign API Dashboard.
Si necesitas una URL de webhook temporal para pruebas revisa ngrok.
Cómo usar ngrok: una vez que descargues ngrok, ejecútalo en el terminal con ./ngrok http 80 y te dará una dirección https. Introduce eso como tu webhook. Ahora abre http://127.0.0.1:4040. Eso es todo, empezarás a ver todos los webhooks y sus datos. Empieza a enviar y firmar documentos de prueba. Nota: Ngrok puede devolver un error 5XX, por lo que puedes esperar ver múltiples intentos y mensajes de error ya que el sistema considerará que el estado 5XX es un intento fallido.
Asegúrate de que tu código receptor del webhook devuelve una respuesta de éxito 2XX. Si tienes varias excepciones posibles en tu código devuelve diferentes códigos de estado 5XX. Los registros de webhook en el dashboard de API te mostrarán cuál excepción fue activada.
El formato del webhook en tiempo real
El formato de los datos puede ser uno de dos esquemas, un esquema de 'documento' o un esquema de 'destinatario'. Inspecciona el atributo 'object' para determinar con cuál esquema estás trabajando.
Ambos esquemas también tienen un atributo 'event'. Puedes filtrar por objeto y opcionalmente por evento cuando creas el webhook.
Una tabla con todos los posibles objetos y eventos está abajo. Esta imagen ilustra un objeto documento (para un evento 'created'). Los esquemas JSON completos están añadidos al final de este artículo.

Las solicitudes llegan como una petición POST que contiene JSON. Ten en cuenta que el contenido de 'data' puede ampliarse para incluir más campos con el tiempo.
Usa los atributos 'tag'. Puedes configurar hasta 3 etiquetas cuando creas un documento. Usando etiquetas quizá no necesites almacenar los IDs de Legalesign. También puedes usar etiquetas para guardar un secreto y verificar la solicitud entrante (gracias Themis por esa sugerencia).
Tabla de posibles combinaciones para 'object' y 'event':
| objeto | evento |
|---|---|
| document | created |
| document | rejected |
| document | finalPdfCreated |
| recipient | completed |
| recipient | rejected |
| recipient | emailOpened |
| recipient | visiting |
| recipient | bounced |
| recipient | autoReminderEmail |
| recipient | emailNotification |
La mayoría de los eventos solo ocurren una vez. Las excepciones son visiting, bounced, emailNotification y autoReminderEmail, que pueden ocurrir muchas veces.
No olvides desactivar la protección CSRF para la vista que recibe estas solicitudes POST.
Aquí hay un ejemplo de un objeto 'recipient', con evento 'bounced':

Observa emailBounce y emailBounceMessage en 'data'. 'emailBounceMessage' indicará el tipo de rebote como sigue:
- Rebote duro: "Message hard bounced"
- Rebote suave: "Email soft bounced (either out of office or a timeout)"
- Retrasado: "Message delayed (check email domain exists)"
El esquema puede tener nuevos atributos añadidos en cualquier momento.
Depurar webhooks en el API dashboard
Todos los webhooks quedan registrados y puedes examinar su contenido y el código de estado http en tu API dashboard. Para aprender más consulta el tutorial del dashboard
Disparar manualmente un webhook
Dispara manualmente un webhook usando el formulario al final de la página Webhooks en tu Developer Portal.
Asegúrate de usar un destinatario o documento real, según el evento.
Códigos de estado
Haz clic en estos enlaces para ver la tabla de referencia para el estado del documento y estado del destinatario.
Verificar un webhook
El webhook firma el paquete de datos con una clave privada. Puedes verificarlo con la clave pública - descargar clave pública.
- La ubicación de la cadena de firma está en el encabezado X-Signed-SHA256.
- Los datos que se firman son todo el request.body (como una cadena).
La 'firma' está codificada en base64. Aquí un código de muestra para verificación en node.js:
const crypto = require('crypto');
const fs = require('fs');
const cert = fs.readFileSync('/location/of/downloadedCert.crt', 'utf8');
const c = new crypto.X509Certificate(cert);
const k = c.publicKey
let verifier = crypto.createVerify('SHA256');
let rawdata = 'sentdata' //JSON stringify data in request.body
let sha256signature = 'xxx' //value of 'X-Signed-Sha256' request header.
verifier.update(rawdata);
return verifier.verify(k, sha256signature, 'base64');
Opcionalmente está disponible también la verificación basada en HMAC. Para HMAC se comparte una clave secreta para tus webhooks. El valor firmado llegará en el encabezado X-HMAC-SHA256.
Hay muchos recursos en línea que demuestran cómo verificar un valor HMAC. Un ejemplo de verificación en python sería:
import hmac, hashlib
sentdata = 'sentdata' # JSON stringify data in request.body
hmacvalue = 'xxx' # value of 'X-HMAC-Sha256' in request header.
v = hmac.new('your secret value'.encode(), 'sentdata'.encode(), hashlib.sha256)
isValid = v.hexdigest() == hmacvalue
O un ejemplo de código Apex para Salesforce para verificar un HMAC es:
String message = 'JSONstring'; //sent in request.body
String privatekey = 'privateKey'; //shared value you have been given
String hmacvalue = 'xxx'; //value of 'X-HMAC-Sha256' in request header.
Boolean verified = Crypto.verifyHMac('HmacSHA256', Blob.valueOf(message), Blob.valueOf(privatekey), EncodingUtil.convertFromHex(hmacvalue));
¿Más webhooks?
Si necesitas más webhooks para eventos diferentes, o más datos en ellos, contáctanos y pon tu solicitud en la cola de desarrollo.
Referencia del esquema
Hay dos posibles esquemas, uno para el objeto 'document' y otro para el objeto 'recipient'. Ambos esquemas tienen claves de nivel superior 'object' y 'event'. Inspecciona el atributo 'object' para determinar con cuál esquema estás tratando. O usa un filtro de webhook para devolver solo objetos documento o solo destinatarios.
Ten en cuenta que al esquema se le pueden añadir nuevos atributos en cualquier momento (pero no disminuirán).
1. Esquema JSON para el objeto 'document'
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"version": {
"type": "string",
"default": "1.0.0"
},
"object": {
"type": "string",
"default": "document"
},
"created": {
"type": "integer",
"description": "unix timestamp"
},
"id": {
"type": "string",
"format": "uuid"
},
"event": {
"type": "string",
"pattern": "^(created|rejected|finalPdfCreated)$"
},
"data": {
"type": "object",
"properties": {
"tag1": {
"type": "string"
},
"recipients": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string",
"format": "email"
},
"order": {
"type": "integer"
},
"status": {
"type": "integer"
"pattern": "^(4|5|10|15|20|30|35|39|40|50|60)$"
},
"lastname": {
"type": "string"
},
"roleText": {
"type": "string",
"pattern": "^(signer|approver|witness)$"
},
"firstname": {
"type": "string"
},
"statusText": {
"type": "string",
"pattern": "^(unsent|scheduled|sent|email opened|visited|fields complete|fields complete excluding signature|witnessing required|completed|download final document|rejected|withdrawn)$"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"rejectReason": {
"type": "string"
}
},
"required": [
"uuid",
"email",
"order",
"status",
"lastname",
"roleText",
"firstname",
"statusText",
"resourceUri",
"rejectReason"
]
}
]
},
"groupResourceUri": {
"type": "string",
"format": "uri-reference"
},
"statusText": {
"type": "string",
"pattern": "^(available|fields complete|completed|removed|rejected|unknown)$"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"uuid": {
"type": "string",
"format": "uuid"
},
"tag2": {
"type": "string"
},
"group": {
"type": "string"
},
"status": {
"type": "integer",
"pattern": "^(10|20|30|40|50)$"
}
},
"required": [
"tag1",
"recipients",
"groupResourceUri",
"statusText",
"name",
"tag",
"resourceUri",
"uuid",
"tag2",
"group",
"status"
]
}
},
"required": [
"version",
"object",
"data",
"created",
"id",
"event"
]
}
2. Esquema JSON para el objeto 'recipient'
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"version": {
"type": "string",
"default": "1.0.0"
},
"object": {
"type": "string",
"default": "recipient"
},
"created": {
"type": "integer",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"event": {
"type": "string",
"pattern": "^(emailOpened|bounced|visiting|rejected|completed|autoReminderEmail|emailNotification)$"
},
"data": {
"type": "object",
"properties": {
"tag": {
"type": "string"
},
"tag1": {
"type": "string"
},
"tag2": {
"type": "string"
},
"uuid": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string",
"format": "email"
},
"group": {
"type": "string",
"format": "uuid"
},
"order": {
"type": "integer"
},
"status": {
"type": "integer",
"pattern": "^(4|5|10|15|20|30|35|39|40|50|60|70)$"
},
"document": {
"type": "string"
},
"documentName": {
"type": "string"
},
"lastname": {
"type": "string"
},
"roleText": {
"type": "string"
},
"firstname": {
"type": "string"
},
"statusText": {
"type": "string",
"pattern": "^(unsent|scheduled|sent|email opened|visited|fields complete|fields complete excluding signature|witnessing required|completed|download final document|rejected|withdrawn)$"
},
"emailBounce": {
"type": "integer"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"rejectReason": {
"type": "string"
},
"groupResourceUri": {
"type": "string",
"format": "uri-reference"
},
"emailBounceMessage": {
"type": "string"
},
"documentResourceUri": {
"type": "string",
"format": "uri-reference"
}
},
"required": [
"tag",
"tag1",
"tag2",
"uuid",
"email",
"group",
"order",
"status",
"document",
"documentName",
"lastname",
"roleText",
"firstname",
"statusText",
"emailBounce",
"resourceUri",
"rejectReason",
"groupResourceUri",
"emailBounceMessage",
"documentResourceUri"
]
}
},
"required": [
"version",
"object",
"data",
"created",
"id",
"event"
]
}