Ir al contenido principal
Certyneo
Documentación para desarrolladores

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.

EventoDisparador
envelope.createdSe crea un sobre (por UI, API o plantilla) — útil para sincronizar un registro en tu CRM desde la creación.
envelope.sentEl sobre se envía a los firmantes (primer email enviado). Marca el inicio del ciclo de firma activo.
envelope.completedTodos 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.declinedUn firmante ha rechazado el sobre. La razón de rechazo (si la proporcionó el firmante) está en `data.decline_reason`.
envelope.voidedEl sobre fue cancelado por el emisor antes de la firma completa. Distinto de `expired` (humano vs timeout).
envelope.expiredLa fecha de expiración del sobre pasó sin firma completa. La lista de firmantes faltantes está en `data.missing_signers`.
recipient.signedUn firmante individual ha firmado (pero no necesariamente todos). Útil para flujos de trabajo secuenciales: desencadenar el paso al siguiente firmante.
recipient.viewedUn 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.

IntentoRetraso acumuladoTiempo transcurrido
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 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).