Webhooks — reciba eventos de firma en tiempo real
Configure una URL HTTPS en su panel de control Certyneo y reciba un POST firmado HMAC-SHA256 cada vez que ocurra un evento en sus sobres: firma, rechazo, expiración. 11 eventos soportados, 5 intentos de entrega con backoff exponencial, verificación criptográfica en 8 líneas de código.
< 5s
Tiempo mediano de entrega después del evento
5x
Intentos de entrega en total, distribuidos en aproximadamente 1 h 20
HMAC-SHA256
Algoritmo de firma de cada solicitud
Catálogo de eventos
Los 12 eventos a continuación cubren el ciclo de vida completo de un sobre Certyneo. Active los que le interesan en Configuración → Webhooks, ignore los demás — la suscripción es granular por evento.
| Evento | Disparador |
|---|---|
envelope.created | Se crea un sobre (por UI, API o plantilla) — útil para sincronizar un registro en tu CRM desde la creación. |
envelope.sent | El sobre se envía a los firmantes (primer email enviado). Marca el inicio del ciclo de firma activo. |
envelope.completed | Todos los firmantes han firmado y el PDF sellado eIDAS se encuentra almacenado. El payload contiene signedDocumentUrl, un enlace prefirmado válido por 7 días; en caso contrario, GET /v1/envelopes/id/signed-document, y la pista de auditoría mediante GET /v1/envelopes/id/audit-trail. |
envelope.declined | Un firmante rechazó el sobre. La dirección del que rechazó se encuentra en `data.declinedBy` y el motivo, si fue ingresado, en `data.reason`. |
envelope.voided | El sobre fue cancelado por el emisor antes de la firma completa. Distinto de `expired` (humano vs timeout). |
envelope.expired | La fecha de vencimiento de la envoltura ha pasado sin firma completa. Interrogue GET /v1/envelopes/id para conocer los firmantes faltantes. |
envelope.returned_to_sender | Un firmante ha devuelto la envoltura al emisor para corrección, sin rechazarla. La razón está en `data.reason` y el autor del envío en `data.returnedBy`. |
envelope.resubmitted | El emisor ha corregido y reenviado una envoltura previamente devuelta. Marca la reanudación del ciclo de firma. |
recipient.signed | Un firmante individual ha firmado (pero no necesariamente todos). Útil para seguir el progreso y encadenar con el siguiente paso de un flujo de trabajo secuencial. Atención: el PDF sellado no existe aún en esta etapa, incluso para el último firmante — una descarga iniciada aquí devuelve un HTTP 409. Utilice envelope.completed para el documento. |
recipient.viewed | Un firmante abrió el enlace de firma sin haber firmado aún. Útil para recordatorios comerciales dirigidos. |
recipient.approved | Un aprobador ha validado la envoltura sin aponer una firma (flujo de validación interna). La dirección está en `data.approvedBy`. |
recipient.bounced | El servidor de correo de un destinatario rechazó definitivamente la invitación o el reintento (buzón inexistente, dominio inactivo). El destinatario cambia al estado BOUNCED y los reintentos automáticos se detienen. La dirección está en `data.recipientEmail`: corríjala y luego reenvíe el sobre. |
recipient.signed no significa « documento disponible »
recipient.signed se emite para CADA firmante, en el momento en que termina su firma — incluido el último, antes de que el PDF sellado se ensamble y almacene. Una descarga activada desde este controlador siempre recibe un HTTP 409 « Signed document not available until the envelope is COMPLETED ». No es un error: es un « aún no listo ». Suscríbase a envelope.completed para recuperar el documento, y mantenga recipient.signed para seguir el progreso (quién firmó, y cuándo).
Formato del payload
Todos los envíos comparten el mismo esquema JSON de nivel superior: `event`, `data` e `timestamp`. El nombre del evento también se repite en el encabezado `X-Certyneo-Event`, lo que permite enrutar incluso antes de analizar el cuerpo. El contenido de `data` varía según el evento, pero siempre permanece como un objeto plano de valores simples — nunca un arreglo ni un objeto anidado. Aquí hay un envío `envelope.completed` completo.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"id": "cm8f2h6a10004qr9k5p2wm3xt",
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"sandbox": false,
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"signatureLevel": "ADVANCED",
"aesSignerCount": 2,
"completedAt": "2026-05-27T08:42:13.000Z",
"recipientCount": 2,
"recipients": [
{ "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
{ "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
],
"signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
},
"timestamp": "2026-05-27T08:42:13.521Z"
}signedDocumentUrl es un enlace prefirmado válido durante 7 días a partir del evento: evita una segunda llamada autenticada para recuperar el PDF. Está ausente — y no nulo — si la prefirma falló, o en una envoltura QES completada sin documento almacenado; vuelva a pasar por GET /v1/envelopes/id/signed-document, que sigue siendo la fuente de verdad. Tenga cuidado también con la reproducción manual desde la cola de letras muertas más de 7 días después del evento: el enlace en el payload ha expirado, el endpoint de API no.
El payload está codificado en UTF-8, sin BOM. La firma HMAC se calcula sobre el body bruto tal como se envió — no altere espacios, el re-parsing JSON a menudo modifica el orden de las claves y rompe la verificación.
Verificar la firma HMAC
Cada solicitud se firma con su secreto de webhook (mostrado una sola vez al crear la suscripción). La firma se transmite en el encabezado `X-Certyneo-Signature`: es el HMAC-SHA256 del cuerpo bruto de la solicitud, codificado en hexadecimal, sin prefijo ni marca de tiempo. SIEMPRE verifique la firma antes de procesar el payload — sin este paso, cualquiera puede falsificar un evento y llamar a su endpoint.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = Buffer.from(signatureHeader, "hex");
const computed = Buffer.from(expected, "hex");
// timingSafeEqual throws when the two buffers differ in length —
// a malformed header must return false, not crash the handler.
if (received.length !== computed.length) return false;
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(computed, received);
}Python
import hashlib
import hmac
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
`X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
raw request body, computed with your webhook secret.
"""
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)Error frecuente
NO use `===` o `==` para comparar la firma esperada con la recibida. Use una función timing-safe (`crypto.timingSafeEqual` en Node, `hmac.compare_digest` en Python). Sin esto, la diferencia de tiempo de comparación entre dos firmas revela gradualmente el secreto a un atacante paciente (timing attack).
Política de reintentos
Si tu endpoint tarda demasiado en responder, rechaza la conexión o devuelve un 5xx (o un 429), reintentamos según un backoff exponencial: 5 intentos en total, durante aproximadamente 1 h 20. Después del quinto, el evento va a la dead-letter queue — consultable y reproducible desde tu panel, pero sin reintentos automáticos. Un rechazo explícito (401, 403, 404, 410, 422…) nunca se reintenta: la respuesta no cambiaría, el evento va directamente a la dead-letter queue.
| Intento | Retraso antes del intento | Tiempo transcurrido desde el evento |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
Los plazos indicados son mínimos: el barrido de reintentos está cadenciado por un cron, por lo que un intento puede salir ligeramente después de la hora teórica. Los eventos abandonados se enumeran en Webhooks → Fallos, con un botón de reproducción manual y sin límite de duración de conservación. Un endpoint que encadena cinco fallos definitivos — o que responde 404 / 410 — se desactiva automáticamente, y se te notifica por correo electrónico: reactívalo una vez corregido, el contador vuelve a cero (cualquier entrega exitosa también lo reinicia a cero).
Probar sin enviar un sobre real
Primero cree una suscripción — la respuesta contiene el `secret` necesario para la verificación HMAC, mostrado una sola vez. Desde Configuración → Webhooks, el botón « Probar » luego envía un POST firmado exactamente como una entrega real y le muestra la respuesta bruta de su servidor: es la forma más rápida de validar su controlador, localmente a través de ngrok o en integración continua.
# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/webhooks/certyneo",
"events": ["envelope.completed", "recipient.signed"]
}'La URL debe ser públicamente accesible en HTTPS: una dirección privada o localhost es rechazada por la protección SSRF al momento de crear la suscripción.
Las suscripciones están separadas por entorno, como las claves: un webhook creado con una clave sk_test_ solo recibe los eventos de los sobres de prueba, y uno creado con una clave sk_live_ solo los de los sobres reales. Un sobre de prueba nunca llega a su URL de producción, y cada carga útil indica «sandbox» (true o false).
6 prácticas a respetar
- Verificar la firma HMAC ANTES de cualquier lectura del body — usar una comparación timing-safe.
- Deduplicar en el encabezado `X-Certyneo-Delivery-Id` (idéntico a `id` en el cuerpo) almacenando los identificadores ya vistos en la base de datos — un reintentos puede reentrega un evento que ya ha procesado si su 2xx se perdió, con el mismo identificador en cada intento.
- Responda HTTP 2xx dentro de máximo 10 segundos, luego procese de forma asincrónica (cola). Después, la entrega se corta y se cuenta como un error.
- Registrar el body bruto + la firma completa en debug — la verificación HMAC a menudo falla por un BOM o un espacio en blanco invisible.
- Monitorea la página Webhooks → Fallos: se te notifica por correo electrónico si tu endpoint está desactivado, pero no evento por evento — un evento que agota sus intentos, debes ir a reproducirlo.
- Filtre los eventos en la suscripción en lugar de en su controlador, y manténgase por debajo del límite de 5 suscripciones por cuenta.
No está obligado a alojar un punto de terminación para recibir estos eventos: el conector realiza la suscripción por usted e inicia el flujo directamente en un evento de sobre. Ver Certyneo para Power Automate y Microsoft 365.
Para ir más allá
¿Listo para conectar tus sistemas?
Los webhooks y la API REST se incluyen a partir del plan Standard. Cree su cuenta, genere una clave `sk_test_` y conecte su endpoint en sandbox antes de pasar a producción.