Ir ao contido principal
Certyneo
Documentación de desenvolvedor

Webhooks recibir eventos de sinatura en tempo real

Configure unha URL HTTPS no seu panel Certyneo e recibirá un POST asinado HMAC-SHA256 cando ocorra un evento nos seus envelopes: sinatura, rexeitamento, caducidade. 8 eventos soportados, reintento exponencial en 6 intentos, verificación criptográfica en 8 liñas de código.

< 5s

Duración media de entrega despois do evento

5x

Intentos en caso de fracaso (ata 9h despois)

HMAC-SHA256

Algoritmo de sinatura de cada solicitude

Catálogo de eventos

Os 8 eventos que se presentan a continuación cubren todo o ciclo de vida dun envelope Certyneo.

EventoDesencadeamento
envelope.createdUn sobre é creado (por UI, API ou plantilla) útil para sincronizar un rexistro do lado CRM desde a creación.
envelope.sentO sobre é enviado aos asinantes (primeiro correo electrónico enviado).
envelope.completedTodos os asinantes asinaron. O PDF selado eIDAS e o rastro de auditoría están dispoñibles a través de `proof_pdf_url` no payload.
envelope.declinedUn asinante rexeitou o envelope. A razón de rexeitamento (se a asinante a proporciona) está en `data.decline_reason`.
envelope.voidedO envelope foi cancelado polo emisor antes da firma completa.
envelope.expiredA data de caducidade do envelope pasou sen a firma completa. A lista de asinantes perdidos está en `data.missing_signers`.
recipient.signedUn asinante individual asinou (pero non todos). Útil para fluxos de traballo secuenciais: desencadear o paso ao seguinte asinante.
recipient.viewedUn asinante abriu o enlace de sinatura sen asinalo aínda, útil para reactivar o comercio.

Formato da carga útil

Todos os eventos comparten o mesmo esquema JSON de nivel superior: `event`, `envelope_id`, `occurred_at`, `data`. O contido de `data` varía segundo o evento (campos documentados por evento na referencia API).

{
  "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"
  }
}

A sinatura HMAC é calculada no corpo bruto como enviado non alterar os espazos, o re-parsing JSON moitas veces cambia a orde das claves e rompe a verificación.

Comprobar a sinatura HMAC

Cada solicitude é asinada co seu segredo webhook (visible unha vez cando se crea no panel de control e despois cifrado ao descanso).<timestamp>,v1=<hex_hmac>`. Sempre comprobar a sinatura antes de procesar a carga útil sen este paso, calquera pode forxar un evento e chamar o seu 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)

Erro frecuente

Non use `===` ou `==` para comparar a sinatura esperada coa recibida. Use unha función de tempo seguro (`crypto.timingSafeEqual` en Node, `hmac.compare_digest` en Python). Sen iso, o tempo de comparación entre dúas firmas revela gradualmente o segredo a un atacante paciente (timing attack).

Política de retry

Se o seu endpoint responde a algo máis que un HTTP 2xx (timeout, 5xx, conexión rexeitada), nós soamos segundo un backoff exponencial.

TentativaPrazo acumuladoTempo transcurrido
#100
#2+ 1 min1 min
#3+ 5 min6 min
#4+ 30 min36 min
#5+ 2 h2h 36
#6+ 6 h8h 36

Se queres reanudar manualmente un evento fallido, o panel de control webhook ofrece un botón Re-deliver en cada entrega fallida dispoñible durante 7 días despois do fallo definitivo.

Teste sen enviar un envelope real

O punto final `/v1/webhooks/test` acepta unha URL e un tipo de evento, e POSTe unha carga útil ficticia asinada exactamente como a verdadeira.

# 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 para seguir

  • Verificar a sinatura HMAC ANTES de calquera lectura do corpo usar unha comparación segura por tempo.
  • Procesar cada `envelope_id` × `event` só unha vez almacenando os IDs xa vistos na base (as re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re-re
  • Responder a HTTP 200 en 5 segundos como máximo, e despois procesar en sincronia (cola).
  • Logar o corpo bruto + a sinatura completa en depuración a verificación HMAC moitas veces falla nun BOM ou espazo branco invisible.
  • Conectar unha cola de dead-letter para eventos `failed` para reanudar manualmente en caso de incidente no seu servizo.
  • Tolerar un desfase de 5 minutos entre `t=` e a hora de recepción máis alá, rexeitar para bloquear as repeticións.

Para ir máis lonxe

¿Estades listos para conectar os sistemas?

Crea unha conta gratuíta en 2 minutos a configuración do webhook está dispoñible desde o plan Starter (gratuito, 5 sobres/més).