Webhooks — reciba eventos de firma en tiempo real
Configure una URL HTTPS en tu dashboard Certyneo y recibe un POST firmado HMAC-SHA256 cada vez que ocurra un evento en tus sobres: firma, rechazo, expiración. 8 eventos compatibles, reintentos exponenciales en 6 intentos, verificación criptográfica en 8 líneas de código.
< 5s
Tiempo mediano de entrega después del evento
5x
Intentos en caso de fallo (hasta ~9h después)
HMAC-SHA256
Algoritmo de firma de cada solicitud
Catálogo de eventos
Los 8 eventos siguientes cubren el ciclo de vida completo de un sobre Certyneo. Activa los que te interesen en Configuración → Webhooks, ignora 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. El PDF sellado eIDAS y el audit trail están disponibles a través de `proof_pdf_url` en el payload. |
envelope.declined | Un firmante ha rechazado el sobre. La razón de rechazo (si la proporcionó el firmante) está en `data.decline_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 expiración del sobre pasó sin firma completa. La lista de firmantes faltantes está en `data.missing_signers`. |
recipient.signed | Un firmante individual ha firmado (pero no necesariamente todos). Útil para flujos de trabajo secuenciales: desencadenar el paso al siguiente firmante. |
recipient.viewed | Un firmante abrió el enlace de firma sin haber firmado aún. Útil para recordatorios comerciales dirigidos. |
Formato del payload
Todos los eventos comparten el mismo esquema JSON de nivel superior: `event`, `envelope_id`, `occurred_at`, `data`. El contenido de `data` varía según el evento (campos documentados por evento en la referencia de API). Aquí hay un ejemplo completo de `envelope.completed`.
{
"event": "envelope.completed",
"envelope_id": "env_01HG3K8X9Y2N4P5Q6R7S8T9V0W",
"occurred_at": "2026-05-27T08:42:13.521Z",
"data": {
"title": "Contrat de prestation Acme Corp",
"status": "completed",
"created_at": "2026-05-24T14:12:00.000Z",
"completed_at": "2026-05-27T08:42:13.000Z",
"signers": [
{
"email": "client@acme.example",
"name": "Alex Client",
"signed_at": "2026-05-27T08:42:13.000Z",
"method": "eidas_aes",
"ip": "203.0.113.42"
}
],
"proof_pdf_url": "https://certyneo.com/api/envelopes/env_01HG.../proof.pdf"
}
}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 está firmada con su secreto webhook (visible una única vez al crear en el dashboard, luego cifrado en reposo). La firma se transmite en el encabezado `Certyneo-Signature` en formato `t=<timestamp>,v1=<hex_hmac>`. Verifique SIEMPRE la firma antes de procesar el payload — sin este paso, cualquiera puede falsificar un evento e invocar su endpoint.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// Header format: t=<timestamp>,v1=<hex_hmac>
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=")),
);
const ts = parts.t;
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject events older than 5 minutes (replay protection).
const age = Math.abs(Date.now() / 1000 - Number(ts));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex"),
);
}Python
import hashlib
import hmac
import time
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
Header format: `t=<timestamp>,v1=<hex_hmac>`
Rejects events older than 5 minutes (replay protection).
"""
parts = dict(p.split("=") for p in signature_header.split(","))
ts = parts.get("t")
sig = parts.get("v1")
if not ts or not sig:
return False
if abs(time.time() - int(ts)) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig)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 su endpoint responde algo que no sea HTTP 2xx (timeout, 5xx, conexión rechazada), reintentamos según un backoff exponencial. Un total de 6 intentos en ~9 horas. Después de los 6, el evento se marca como `failed` en su dashboard webhook y se envía un email de alerta.
| Intento | Retraso acumulado | Tiempo transcurrido |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 30 min | 36 min |
| #5 | + 2 h | 2h 36 |
| #6 | + 6 h | 8h 36 |
Si desea retransmitir manualmente un evento fallido, el dashboard webhook ofrece un botón «Re-deliver» en cada entrega fallida — disponible durante 7 días después del fallo definitivo.
Probar sin enviar un sobre real
El endpoint `/v1/webhooks/test` acepta una URL y un tipo de evento, y POSTea un payload ficticio firmado exactamente como uno real. Ideal para validar su handler en integración continua o durante el desarrollo local (con ngrok o equivalente).
# Trigger a test webhook to your endpoint
curl -X POST https://api.certyneo.com/v1/webhooks/test \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": "envelope.completed", "url": "https://your.app/webhooks/certyneo"}'6 prácticas a respetar
- Verificar la firma HMAC ANTES de cualquier lectura del body — usar una comparación timing-safe.
- Procesar cada `envelope_id` × `event` una única vez almacenando los IDs ya vistos en la base de datos (los reintentos pueden re-entregar el mismo evento).
- Responder HTTP 200 en máximo 5 segundos, luego procesar de forma asincrónica (queue). Si no lo hace, tendrá timeout y se contará como reintento.
- 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.
- Conectar una dead-letter queue a los eventos `failed` para retransmitir manualmente en caso de incidente en su servicio.
- Tolerar un desfase de 5 minutos entre `t=` y la hora de recepción — más allá de eso, rechazar para bloquear replays.
Para ir más allá
¿Listo para conectar tus sistemas?
Crea una cuenta gratuita en 2 minutos — la configuración webhook está disponible desde el plan Starter (gratuito, 5 sobres/mes).