Webhooks de Chaindoc

Recibe notificaciones en tiempo real cuando se produzcan eventos en tu cuenta de Chaindoc. Los webhooks envían los datos de los eventos a tu servidor al instante, lo que elimina la necesidad de realizar sondeos.

Resumen

Los webhooks de Chaindoc permiten que tu aplicación reciba notificaciones en tiempo real cuando se producen eventos en tu cuenta. En lugar de sondear la API, los webhooks envían los datos de los eventos a tu servidor tan pronto como se produce una acción.

Características principales

  • Notificaciones en tiempo real: entrega instantánea de eventos a tu servidor.
  • Reintentos automáticos: hasta 3 intentos de reintento con retroceso exponencial.
  • Verificación de firma: HMAC SHA256 para la autenticidad de la carga útil.
  • Filtrado de eventos: recibe solo los eventos que te interesan.
  • Seguimiento de errores: supervisa el estado de entrega y los fallos de los webhooks.

Casos de uso

  • Enviar notificaciones por correo electrónico cuando se firmen los documentos.
  • Activa flujos de trabajo cuando los documentos se verifiquen en la cadena de bloques.
  • Actualiza tu base de datos cuando se creen solicitudes de firma.
  • Sincroniza el estado de los documentos con sistemas externos.
  • Registro de auditoría y cumplimiento normativo.

Configuración

Paso 1: Crea una clave API.

Ve a Configuración → Acceso a la API en tu panel de control de Chaindoc y crea una clave API con la configuración de webhook habilitada.

Paso 2: Configurar la URL del webhook

Utiliza la API para configurar tu punto final de webhook:

terminal
curl -X PATCH https://api.chaindoc.io/user/api-access/1/config \
  -H "Authorization: Bearer your_auth_token" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://yourapp.com/webhooks/chaindoc",
    "webhookEnabled": true,
    "webhookSecret": "your_secure_random_string"
  }'

Paso 3: Implementar el punto final de Webhook

Crea un punto final en tu servidor para recibir eventos webhook. A continuación se muestran ejemplos en diferentes lenguajes:

const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

app.post('/webhooks/chaindoc', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const eventType = req.headers['x-webhook-event'];
  
  // Verify signature
  if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process event
  console.log('Received event:', eventType, req.body);
  
  // Handle different event types
  switch (eventType) {
    case 'document.created':
      handleDocumentCreated(req.body);
      break;
    case 'document.verified':
      handleDocumentVerified(req.body);
      break;
    case 'signature.request.completed':
      handleSignatureCompleted(req.body);
      break;
  }
  
  // Always respond with 200 OK
  res.status(200).send('Webhook received');
});

function verifySignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(JSON.stringify(payload)).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}

app.listen(3000);

Tipos de eventos

Chaindoc envía webhooks para los siguientes eventos:

document.created

Se activa cuando se crea un nuevo documento a través de la API.

terminal
{
  "event": "document.created",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "name": "Service Agreement",
  "timestamp": "2024-12-04T10:30:00.000Z"
}

document.verified

Se activa cuando un documento se verifica correctamente en la cadena de bloques.

terminal
{
  "event": "document.verified",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "versionId": "f0b7721f-0399-4035-9b69-7b95d3a367f0",
  "txHash": "0x789ghi...",
  "chainId": 137,
  "timestamp": "2024-12-04T10:35:00.000Z"
}

documento.firmado

Se activa cuando se recogen todas las firmas necesarias.

terminal
{
  "event": "document.signed",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
  "signers": [
    {
      "email": "signer1@example.com",
      "signedAt": "2024-12-04T10:30:00.000Z"
    },
    {
      "email": "signer2@example.com",
      "signedAt": "2024-12-04T10:32:00.000Z"
    }
  ],
  "timestamp": "2024-12-04T10:32:00.000Z"
}

signature.request.created

Se activa cuando se crea una nueva solicitud de firma.

terminal
{
  "event": "signature.request.created",
  "signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "recipients": [
    {"email": "signer1@example.com"},
    {"email": "signer2@example.com"}
  ],
  "deadline": "2024-12-31T23:59:59.000Z",
  "timestamp": "2024-12-04T10:30:00.000Z"
}

signature.request.completed

Se activa cuando todos los firmantes completan sus firmas.

terminal
{
  "event": "signature.request.completed",
  "signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "completedAt": "2024-12-04T10:32:00.000Z",
  "timestamp": "2024-12-04T10:32:00.000Z"
}

signature.request.rejected

Se activa cuando un firmante rechaza la solicitud de firma.

terminal
{
  "event": "signature.request.rejected",
  "signatureRequestId": "req_21096b94498f4a2d9795e810edc2c9a9",
  "documentId": "86840ee4-8bf2-4a91-a289-e99d8307ec25",
  "rejectedBy": "signer1@example.com",
  "reason": "Terms not acceptable",
  "timestamp": "2024-12-04T10:30:00.000Z"
}

Seguridad

Verificación de la firma

Chaindoc firma todas las cargas útiles de webhook utilizando HMAC SHA256. Verifica siempre las firmas para garantizar su autenticidad y evitar ataques de repetición.

Cómo funciona la verificación de firmas

1Chaindoc crea firmas.Chaindoc crea una firma HMAC utilizando tu secreto de webhook.

2Firma enviada en el encabezado.La firma se envía en el encabezado X-Webhook-Signature.

3Tu servidor verificaTu servidor recalcula la firma y la compara utilizando una función segura en cuanto al tiempo.

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(JSON.stringify(payload)).digest('hex');
  
  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}

// Usage
const isValid = verifyWebhookSignature(
  req.body,
  req.headers['x-webhook-signature'],
  process.env.WEBHOOK_SECRET
);

if (!isValid) {
  return res.status(401).send('Invalid signature');
}

Lógica de reintento

Chaindoc reintenta automáticamente las entregas fallidas de webhooks con retroceso exponencial.

  • Primer reintento: después de 1 minuto.
  • Segundo intento: después de 5 minutos (total: 6 minutos).
  • Tercer intento: después de 15 minutos (total: 21 minutos).

Prueba de webhooks

Desarrollo local

Utiliza herramientas como ngrok para exponer tu servidor local para pruebas de webhook:

terminal
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Expose port 3000
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# Example: https://abc123.ngrok.io/webhooks/chaindoc

Pruebas manuales

Prueba tu punto final de webhook con una carga útil de muestra:

terminal
curl -X POST https://yourapp.com/webhooks/chaindoc \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Event: document.created" \
  -H "X-Webhook-Signature: test_signature" \
  -d '{
    "event": "document.created",
    "documentId": "test-123",
    "name": "Test Document",
    "timestamp": "2024-12-04T10:30:00.000Z"
  }'

Prácticas recomendadas

  • Verifica siempre las firmas de los webhooks antes de procesarlos.
  • Responde rápidamente (en menos de 30 segundos) para evitar tiempos de espera.
  • Procesa los webhooks de forma asíncrona en una cola.
  • Implementa la idempotencia para gestionar eventos duplicados.
  • Registra todos los eventos de webhook para depuración y auditoría.
  • Supervisa los errores de entrega de webhooks en tu panel de control.
  • Utiliza puntos finales HTTPS por motivos de seguridad.
  • Maneja todos los tipos de eventos con elegancia (ignora los eventos desconocidos).

Ejemplo completo

Aquí tienes un controlador de webhook listo para producción con integración de base de datos:

webhooks/chaindoc.ts
import express from 'express';
import crypto from 'crypto';
import { PrismaClient } from '@prisma/client';

const app = express();
const prisma = new PrismaClient();

app.use(express.json());

app.post('/webhooks/chaindoc', async (req, res) => {
  const signature = req.headers['x-webhook-signature'] as string;
  const eventType = req.headers['x-webhook-event'] as string;
  const payload = req.body;
  
  // 1. Verify signature
  if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // 2. Check for duplicate events (idempotency)
  const eventId = `${eventType}-${payload.timestamp}`;
  const existing = await prisma.webhookEvent.findUnique({
    where: { eventId },
  });
  
  if (existing) {
    console.log('Duplicate event, skipping:', eventId);
    return res.status(200).json({ status: 'duplicate' });
  }
  
  // 3. Store event
  await prisma.webhookEvent.create({
    data: {
      eventId,
      eventType,
      payload,
      processedAt: new Date(),
    },
  });
  
  // 4. Process event asynchronously
  processWebhookAsync(eventType, payload).catch((error) => {
    console.error('Error processing webhook:', error);
  });
  
  // 5. Respond immediately
  res.status(200).json({ status: 'received' });
});

async function processWebhookAsync(eventType: string, payload: any) {
  switch (eventType) {
    case 'document.verified':
      await handleDocumentVerified(payload);
      break;
    case 'signature.request.completed':
      await handleSignatureCompleted(payload);
      await sendNotificationEmail(payload);
      break;
    case 'signature.request.rejected':
      await handleSignatureRejected(payload);
      break;
  }
}

async function handleDocumentVerified(payload: any) {
  await prisma.document.update({
    where: { id: payload.documentId },
    data: {
      status: 'verified',
      blockchainTxHash: payload.txHash,
      verifiedAt: new Date(payload.timestamp),
    },
  });
}

async function handleSignatureCompleted(payload: any) {
  await prisma.signatureRequest.update({
    where: { id: payload.signatureRequestId },
    data: {
      status: 'completed',
      completedAt: new Date(payload.completedAt),
    },
  });
}

function verifySignature(payload: any, signature: string, secret: string): boolean {
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(JSON.stringify(payload)).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

app.listen(3000);