Documentación Técnica: Sistema de Webhooks de Obrify

Introducción

¿Qué son los Webhooks en Obrify?

Los webhooks son notificaciones HTTP en tiempo real que Obrify envía a tu sistema cuando ocurren eventos importantes. A diferencia de las APIs tradicionales donde tu sistema debe consultar constantemente, los webhooks permiten que Obrify te notifique automáticamente cuando algo sucede.

Casos de Uso

Los webhooks son útiles para:

  • Sincronización de datos: Mantener tu sistema de gestión actualizado automáticamente cuando se crean o modifican pedidos, clientes, cobros, etc.
  • Automatización de procesos: Activar flujos de trabajo en tu sistema cuando ocurren eventos específicos
  • Integración con sistemas externos: Conectar Obrify con ERPs, sistemas de facturación, plataformas de e-commerce, etc.
  • Notificaciones en tiempo real: Recibir alertas inmediatas sobre cambios importantes en el estado de entregas, cobros, etc.

Flujo General de Funcionamiento

sequenceDiagram participant Obrify as Obrify Sistema participant Internet as Internet participant TuSistema as Tu Sistema Note over Obrify: 1. Evento ocurre
(ej: pedido creado) Note over Obrify: 2. Obrify prepara payload
y firma con HMAC-SHA256 Obrify->>Internet: 3. POST request con JSON
+ X-Webhook-Signature header Internet->>TuSistema: 4. Request llega a tu endpoint Note over TuSistema: 5. Tu sistema valida la firma
y procesa el evento TuSistema->>Internet: 6. Respuesta HTTP 200 OK Internet->>Obrify: 7. Obrify registra éxito

Pasos del flujo:

  1. Evento ocurre: Se crea un pedido, se registra un cobro, se actualiza una ruta, etc.
  2. Obrify procesa: El sistema identifica qué webhooks están configurados para ese evento
  3. Preparación: Se construye el payload JSON con los datos del evento
  4. Firma: Si está configurado un secret, se genera la firma HMAC-SHA256
  5. Envío: Se realiza una petición HTTP POST a la URL configurada
  6. Recepción: Tu endpoint recibe la petición
  7. Validación: Tu sistema valida la firma (si aplica) y procesa el evento
  8. Respuesta: Tu sistema responde con un código HTTP apropiado
  9. Registro: Obrify registra el resultado (éxito o fallo) en los logs

Configuración de Webhooks

Acceso a la Configuración

Para configurar webhooks en Obrify, accede desde el Dashboard principal a "Configuración General" y luego selecciona Webhooks.

Parámetros de Configuración

Al crear o editar un webhook, deberás configurar los siguientes parámetros:

1. Nombre (Obligatorio)

  • Descripción: Identificador descriptivo del webhook para uso interno
  • Ejemplo: "Webhook principal ERP", "Sincronización pedidos"
  • Uso: Se muestra en el listado y ayuda a identificar el propósito del webhook

2. URL de Destino (Obligatorio)

  • Descripción: URL completa del endpoint en tu sistema que recibirá las notificaciones
  • Formato: Debe ser una URL válida con protocolo HTTPS (recomendado) o HTTP
  • Ejemplo: https://api.tusistema.com/webhooks/obrify
  • Requisitos:
    • Debe aceptar peticiones HTTP POST
    • Debe aceptar contenido JSON en el cuerpo de la petición
    • Debe responder con códigos HTTP estándar (200, 201, 400, 500, etc.)

3. Eventos Habilitados (Obligatorio)

  • Descripción: Lista de eventos que activarán este webhook
  • Formato: Selección múltiple de eventos disponibles
  • Nota: Puedes seleccionar múltiples eventos para un mismo webhook
  • Ejemplo: Seleccionar "pedido.created" y "pedido.updated"

4. Estado (Opcional)

  • Descripción: Activa o desactiva el webhook sin eliminarlo
  • Valores:
    • Activo: El webhook enviará notificaciones
    • Inactivo: El webhook no enviará notificaciones (útil para pausar temporalmente)

5. Secret (Opcional pero Recomendado)

  • Descripción: Cadena secreta utilizada para firmar los eventos con HMAC-SHA256
  • Formato: Cadena de texto alfanumérica (recomendado: mínimo 32 caracteres)
  • Ejemplo: sk_live_abc123xyz789def456ghi012jkl345
  • Seguridad:
    • Mantén este valor secreto y seguro
    • Úsalo para validar que las peticiones provienen realmente de Obrify
    • No lo compartas ni lo expongas en código público

6. Descripción (Opcional)

  • Descripción: Notas internas sobre el uso del webhook
  • Ejemplo: "Sincroniza pedidos con nuestro ERP cada vez que se crea o actualiza uno"

7. Máximo de Reintentos (Opcional)

  • Descripción: Número máximo de intentos si el webhook falla
  • Valor por defecto: 3
  • Rango: 1-10
  • Comportamiento: Si tu endpoint no responde o devuelve un error, Obrify reintentará hasta este número de veces antes de marcar el envío como fallido

8. Timeout (Opcional)

  • Descripción: Tiempo máximo de espera por respuesta en milisegundos
  • Valor por defecto: 5000 ms (5 segundos)
  • Rango mínimo: 1000 ms (1 segundo)
  • Recomendación: Ajusta según el tiempo que tarda tu endpoint en procesar y responder

9. Headers Adicionales (Opcional)

  • Descripción: Headers HTTP personalizados que se agregarán a cada petición
  • Formato: JSON con pares clave-valor
  • Ejemplo:
    {
      "Authorization": "Bearer tu_token_aqui",
      "X-Custom-Header": "valor_personalizado",
      "User-Agent": "MiSistema/1.0"
    }
  • Uso: Útil para autenticación adicional, identificación del origen, etc.

Ejemplo de Configuración Completa

Nombre: Webhook ERP Principal
URL: https://api.miempresa.com/webhooks/obrify
Eventos: pedido.created, pedido.updated, cliente.created
Estado: Activo
Secret: sk_live_abc123xyz789def456ghi012jkl345
Descripción: Sincronización automática con nuestro ERP
Máximo de Reintentos: 5
Timeout: 10000 ms
Headers Adicionales:
{
  "Authorization": "Bearer erp_api_token_12345",
  "X-Source": "Obrify"
}

Autenticación y Seguridad

Firma HMAC-SHA256

Para garantizar que las peticiones provienen realmente de Obrify y no han sido modificadas, el sistema utiliza firmas HMAC-SHA256 cuando se configura un secret.

Header X-Webhook-Signature

Cuando configuras un secret, Obrify incluye un header X-Webhook-Signature en cada petición con la firma del payload.

Formato del header:

X-Webhook-Signature: sha256=<firma_hexadecimal>

Proceso de generación:

  1. Obrify toma el cuerpo completo de la petición (JSON sin modificar)
  2. Calcula el HMAC-SHA256 usando el secret configurado
  3. Convierte el resultado a hexadecimal
  4. Lo incluye en el header como sha256=<hex>

Validación de la Firma

IMPORTANTE: Siempre valida la firma antes de procesar el evento. Esto previene ataques de suplantación y garantiza la integridad de los datos.

Algoritmo de Validación

  1. Extrae el valor del header X-Webhook-Signature
  2. Obtén el cuerpo raw de la petición (sin parsear)
  3. Calcula el HMAC-SHA256 del cuerpo usando tu secret
  4. Compara el resultado con la firma recibida
  5. Si coinciden, la petición es válida
  6. Si no coinciden, rechaza la petición con código 401

Consideraciones Importantes

  • Cuerpo raw: Usa el cuerpo de la petición tal como viene, sin modificaciones
  • Encoding: Asegúrate de usar el encoding correcto (generalmente UTF-8)
  • Comparación segura: Usa comparación constante en el tiempo para prevenir timing attacks

Estructura de las Peticiones

Método HTTP

Todas las peticiones de webhook utilizan el método POST.

Content-Type

El header Content-Type siempre será:

Content-Type: application/json

Headers Estándar

Cada petición incluye los siguientes headers:

POST /tu-endpoint HTTP/1.1
Host: api.tusistema.com
Content-Type: application/json
Content-Length: <tamaño_del_cuerpo>
X-Webhook-Signature: sha256=<firma_hexadecimal>  (si está configurado secret)
User-Agent: Obrify-Webhook/1.0

Headers personalizados: Si configuraste headers adicionales, estos se fusionarán con los headers estándar.

Estructura del Payload

El cuerpo de la petición es un objeto JSON con la siguiente estructura general:

{
  "event": "nombre.del.evento",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "tenantId": "uuid-del-tenant",
  "data": {
    // Datos específicos del evento
  },
  "previousData": {
    // Datos anteriores (solo presente en eventos updated)
  }
}

Campos del Payload

  • event (string): Nombre del evento que se disparó
  • timestamp (string ISO 8601): Fecha y hora del evento en UTC
  • tenantId (string UUID): ID del tenant en Obrify
  • data (object): Objeto con los datos específicos del evento (siempre presente)
  • previousData (object, opcional): Objeto con los datos anteriores del recurso. Solo está presente en eventos de tipo updated (ej: cliente.updated, producto.updated, pedido.updated). Útil para comparar cambios o realizar auditorías.

Campo previousData

El campo previousData es opcional y solo aparece en eventos de tipo updated. Contiene una instantánea del estado del recurso antes de que se realizara la actualización.

Cuándo está presente:

  • En eventos cliente.updated
  • En eventos producto.updated
  • En eventos pedido.updated

Cuándo NO está presente:

  • En eventos created (no hay estado anterior)
  • En eventos deleted (el recurso ya no existe)
  • Si el sistema no pudo obtener el estado anterior por alguna razón

Casos de uso:

  • Auditoría: Registrar qué cambió exactamente
  • Notificaciones: Enviar alertas solo cuando cambian campos específicos
  • Sincronización: Determinar si necesitas actualizar sistemas externos
  • Validación: Comparar valores anteriores y nuevos para lógica de negocio

Ejemplo de uso:

if (payload.previousData) {
  // Comparar estados
  if (payload.previousData.estado !== payload.data.estado) {
    console.log(`Estado cambió de ${payload.previousData.estado} a ${payload.data.estado}`);
    // Enviar notificación de cambio de estado
  }
}

Eventos Disponibles

NOTA: Esta sección debe completarse con la lista específica de eventos disponibles en tu sistema. A continuación se muestra la estructura que debe seguirse para cada evento.

Obrify soporta los siguientes eventos de webhook. Todos los eventos siguen el formato entidad.accion donde:

  • entidad: El tipo de recurso (cliente, producto, pedido)
  • accion: La acción realizada (created, updated, deleted)

Lista de Eventos

Eventos de Cliente

  • cliente.created - Se dispara cuando se crea un nuevo cliente
  • cliente.updated - Se dispara cuando se actualiza un cliente existente
  • cliente.deleted - Se dispara cuando se elimina un cliente

Eventos de Producto

  • producto.created - Se dispara cuando se crea un nuevo producto
  • producto.updated - Se dispara cuando se actualiza un producto existente
  • producto.deleted - Se dispara cuando se elimina un producto

Eventos de Pedido

  • pedido.created - Se dispara cuando se crea un nuevo pedido
  • pedido.updated - Se dispara cuando se actualiza un pedido existente
  • pedido.deleted - Se dispara cuando se elimina un pedido
NOTA: Para obtener la lista completa y actualizada de eventos disponibles, consulta el endpoint GET /api/webhooks/events de la API.

Estructura de Documentación por Evento

Para cada evento, la documentación incluye:

  1. Nombre del evento: Formato entidad.accion (ej: pedido.created)
  2. Descripción: Qué representa este evento
  3. Cuándo se dispara: En qué situación se envía este webhook
  4. Estructura del payload: Campos específicos en data
  5. Ejemplo completo: Payload JSON de ejemplo

Ejemplos de Eventos

pedido.created

Descripción: Se dispara cuando se crea un nuevo pedido en el sistema.

Cuándo se dispara: Cuando un pedido es creado y guardado exitosamente en la base de datos. Esto ocurre cuando se finaliza la creación de un pedido nuevo.

Estructura del payload data: El objeto data contiene el objeto completo del pedido con todas sus relaciones (cliente, líneas de pedido, productos, etc.). La estructura exacta depende de tu modelo de datos, pero típicamente incluye:

  • id: ID único del pedido
  • numeroPedido: Número de pedido legible
  • clienteId: ID del cliente
  • fechaPedido: Fecha de creación
  • estado: Estado actual del pedido
  • Y otros campos según tu modelo

Ejemplo completo:

{
  "event": "pedido.created",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "tenantId": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "numeroPedido": "PED-2024-001",
    "clienteId": "123e4567-e89b-12d3-a456-426614174000",
    "fechaPedido": "2024-01-15T10:30:00.000Z",
    "estado": "pendiente",
    "valorTotal": 15000.50
  }
}

pedido.updated

Descripción: Se dispara cuando se actualiza un pedido existente en el sistema.

Cuándo se dispara: Cuando un pedido es modificado y guardado exitosamente. Esto puede ocurrir cuando se agregan líneas, se cambia el estado, se actualiza información, etc.

Estructura del payload data: Similar a pedido.created, contiene el objeto completo del pedido actualizado.

Campo previousData: Este evento incluye el campo opcional previousData con los datos del pedido antes de la actualización, útil para comparar cambios.

Ejemplo completo:

{
  "event": "pedido.updated",
  "timestamp": "2024-01-15T11:45:00.000Z",
  "tenantId": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "numeroPedido": "PED-2024-001",
    "estado": "en_proceso",
    "valorTotal": 15000.50
  },
  "previousData": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "numeroPedido": "PED-2024-001",
    "estado": "pendiente",
    "valorTotal": 15000.50
  }
}

cliente.created

Descripción: Se dispara cuando se crea un nuevo cliente en el sistema.

Cuándo se dispara: Cuando un cliente es creado y guardado exitosamente en la base de datos.

Estructura del payload data: El objeto data contiene el objeto completo del cliente con todos sus campos.

Ejemplo completo:

{
  "event": "cliente.created",
  "timestamp": "2024-01-15T09:00:00.000Z",
  "tenantId": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "id": "789e0123-e45b-67c8-d901-234567890abc",
    "nombre": "Juan Pérez",
    "email": "juan@example.com",
    "telefono": "+1234567890"
  }
}

cliente.updated

Descripción: Se dispara cuando se actualiza un cliente existente.

Cuándo se dispara: Cuando un cliente es modificado y guardado exitosamente.

Campo previousData: Este evento incluye el campo opcional previousData con los datos del cliente antes de la actualización.

Ejemplo completo:

{
  "event": "cliente.updated",
  "timestamp": "2024-01-15T10:15:00.000Z",
  "tenantId": "123e4567-e89b-12d3-a456-426614174000",
  "data": {
    "id": "789e0123-e45b-67c8-d901-234567890abc",
    "nombre": "Juan Pérez",
    "email": "juan.nuevo@example.com",
    "telefono": "+1234567890"
  },
  "previousData": {
    "id": "789e0123-e45b-67c8-d901-234567890abc",
    "nombre": "Juan Pérez",
    "email": "juan@example.com",
    "telefono": "+1234567890"
  }
}
NOTA: Los eventos producto.created, producto.updated, producto.deleted, cliente.deleted y pedido.deleted siguen la misma estructura. Consulta el endpoint GET /api/webhooks/events para obtener la lista completa de eventos disponibles.

Manejo de Respuestas

Códigos HTTP Esperados

Tu endpoint debe responder con códigos HTTP apropiados para indicar el resultado del procesamiento:

Respuestas Exitosas (2xx)

  • 200 OK: El evento fue procesado exitosamente
  • 201 Created: El evento fue procesado y se creó un recurso (opcional, 200 también es válido)
  • 202 Accepted: El evento fue aceptado para procesamiento asíncrono

Recomendación: Usa 200 OK para la mayoría de casos.

Respuestas de Error del Cliente (4xx)

  • 400 Bad Request: El payload recibido es inválido o malformado
  • 401 Unauthorized: La firma no es válida o falta autenticación
  • 403 Forbidden: No tienes permisos para procesar este evento
  • 404 Not Found: El endpoint no existe (esto no debería ocurrir si está bien configurado)
  • 422 Unprocessable Entity: El payload es válido pero no se puede procesar por razones de negocio

Respuestas de Error del Servidor (5xx)

  • 500 Internal Server Error: Error interno en tu sistema
  • 502 Bad Gateway: Tu sistema actúa como gateway y recibió respuesta inválida
  • 503 Service Unavailable: Tu sistema está temporalmente no disponible
  • 504 Gateway Timeout: Tu sistema no respondió a tiempo

Comportamiento de Reintentos

Si tu endpoint responde con un código de error (4xx o 5xx) o no responde dentro del timeout configurado, Obrify intentará reenviar el webhook según la configuración:

  1. Primer intento: Inmediato cuando ocurre el evento
  2. Reintentos: Si falla, se reintenta según maxReintentos configurado
  3. Backoff exponencial: Los reintentos esperan progresivamente más tiempo entre intentos
  4. Marcado como fallido: Después de agotar los reintentos, el envío se marca como fallido

Nota sobre reintentos: Aunque el número de intento no se incluye en el payload, Obrify mantiene un registro interno de los intentos realizados. Puedes consultar esta información en los logs del webhook a través de la API.

Timeout

Si tu endpoint no responde dentro del timeout configurado (por defecto 5 segundos), Obrify considerará el envío como fallido y lo reintentará.

Recomendaciones:

  • Procesa los eventos de forma asíncrona cuando sea posible
  • Responde rápidamente (200 OK) y procesa después
  • Si el procesamiento toma tiempo, considera usar una cola de trabajos

Manejo de Errores

Mejores Prácticas

  1. Responde rápidamente: Acepta el webhook y responde 200 OK, luego procesa asíncronamente
  2. Valida la firma primero: Antes de procesar, valida que la petición sea legítima
  3. Valida el payload: Verifica que los datos recibidos sean válidos antes de procesarlos
  4. Maneja errores gracefully: Si hay un error, responde con el código HTTP apropiado
  5. Logs: Registra todos los webhooks recibidos para debugging y auditoría
  6. Idempotencia: Diseña tu endpoint para ser idempotente (puede recibir el mismo evento múltiples veces sin efectos secundarios)

Ejemplos de Implementación

Node.js (Express)

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

// Middleware para parsear JSON
app.use(express.json({
  verify: (req, res, buf) => {
    // Guardar el cuerpo raw para validar la firma
    req.rawBody = buf.toString('utf8');
  }
}));

// Tu secret configurado en Obrify
const WEBHOOK_SECRET = process.env.OBRIFY_WEBHOOK_SECRET || 'tu_secret_aqui';

// Función para validar la firma
function validateSignature(signature, body, secret) {
  if (!signature || !secret) {
    return false;
  }

  // Extraer la firma del header (formato: sha256=hex)
  const sigHash = signature.replace('sha256=', '');
  
  // Calcular HMAC-SHA256
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(body, 'utf8');
  const calculatedHash = hmac.digest('hex');
  
  // Comparación segura en tiempo constante
  return crypto.timingSafeEqual(
    Buffer.from(sigHash, 'hex'),
    Buffer.from(calculatedHash, 'hex')
  );
}

// Endpoint para recibir webhooks
app.post('/webhooks/obrify', async (req, res) => {
  try {
    // 1. Validar la firma
    const signature = req.headers['x-webhook-signature'];
    if (!validateSignature(signature, req.rawBody, WEBHOOK_SECRET)) {
      console.error('Firma inválida');
      return res.status(401).json({ error: 'Firma inválida' });
    }

    // 2. Extraer información del evento
    const event = req.body.event;
    const timestamp = req.body.timestamp;
    const tenantId = req.body.tenantId;
    const data = req.body.data;
    const previousData = req.body.previousData;

    console.log(`Evento recibido: ${event} en ${timestamp} para tenant ${tenantId}`);

    // 3. Procesar según el tipo de evento
    switch (event) {
      case 'pedido.created':
        await handlePedidoCreado(data);
        break;
      case 'pedido.updated':
        await handlePedidoActualizado(data, previousData);
        break;
      case 'cliente.created':
        await handleClienteCreado(data);
        break;
      case 'cliente.updated':
        await handleClienteActualizado(data, previousData);
        break;
      // Agregar más casos según los eventos que uses
      default:
        console.log(`Evento no manejado: ${event}`);
    }

    // 4. Responder exitosamente
    res.status(200).json({
      success: true,
      message: 'Evento procesado correctamente',
      processedAt: new Date().toISOString()
    });

  } catch (error) {
    console.error('Error procesando webhook:', error);
    res.status(500).json({ error: 'Error interno del servidor' });
  }
});

// Funciones de manejo de eventos
async function handlePedidoCreado(data) {
  console.log('Procesando pedido creado:', data.numeroPedido || data.id);
  // Aquí tu lógica para procesar el pedido
  // Ejemplo: guardar en tu base de datos, notificar a otros sistemas, etc.
}

async function handlePedidoActualizado(data, previousData) {
  console.log('Procesando pedido actualizado:', data.numeroPedido || data.id);
  if (previousData) {
    console.log('Estado anterior:', previousData.estado);
    console.log('Estado nuevo:', data.estado);
  }
  // Aquí tu lógica para procesar la actualización del pedido
}

async function handleClienteCreado(data) {
  console.log('Procesando cliente creado:', data.nombre || data.id);
  // Aquí tu lógica para procesar el cliente
}

async function handleClienteActualizado(data, previousData) {
  console.log('Procesando cliente actualizado:', data.nombre || data.id);
  if (previousData) {
    console.log('Email anterior:', previousData.email);
    console.log('Email nuevo:', data.email);
  }
  // Aquí tu lógica para procesar la actualización del cliente
}

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Servidor escuchando en puerto ${PORT}`);
});

Python (Flask)

from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import json

app = Flask(__name__)

# Tu secret configurado en Obrify
WEBHOOK_SECRET = os.getenv('OBRIFY_WEBHOOK_SECRET', 'tu_secret_aqui')

def validate_signature(signature, body, secret):
    """Valida la firma HMAC-SHA256 del webhook"""
    if not signature or not secret:
        return False
    
    # Extraer la firma del header (formato: sha256=hex)
    sig_hash = signature.replace('sha256=', '')
    
    # Calcular HMAC-SHA256
    calculated_hash = hmac.new(
        secret.encode('utf-8'),
        body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Comparación segura en tiempo constante
    return hmac.compare_digest(sig_hash, calculated_hash)

@app.route('/webhooks/obrify', methods=['POST'])
def webhook_handler():
    try:
        # 1. Obtener el cuerpo raw
        raw_body = request.get_data(as_text=True)
        
        # 2. Validar la firma
        signature = request.headers.get('X-Webhook-Signature')
        if not validate_signature(signature, raw_body, WEBHOOK_SECRET):
            return jsonify({'error': 'Firma inválida'}), 401
        
        # 3. Parsear el JSON
        payload = json.loads(raw_body)
        event = payload.get('event')
        timestamp = payload.get('timestamp')
        tenant_id = payload.get('tenantId')
        data = payload.get('data')
        previous_data = payload.get('previousData')
        
        print(f"Evento recibido: {event} en {timestamp} para tenant {tenant_id}")
        
        # 4. Procesar según el tipo de evento
        if event == 'pedido.created':
            handle_pedido_creado(data)
        elif event == 'pedido.updated':
            handle_pedido_actualizado(data, previous_data)
        elif event == 'cliente.created':
            handle_cliente_creado(data)
        elif event == 'cliente.updated':
            handle_cliente_actualizado(data, previous_data)
        # Agregar más casos según los eventos que uses
        else:
            print(f"Evento no manejado: {event}")
        
        # 5. Responder exitosamente
        return jsonify({
            'success': True,
            'message': 'Evento procesado correctamente'
        }), 200
        
    except json.JSONDecodeError:
        return jsonify({'error': 'JSON inválido'}), 400
    except Exception as e:
        print(f"Error procesando webhook: {e}")
        return jsonify({'error': 'Error interno del servidor'}), 500

def handle_pedido_creado(data):
    """Procesa un evento de pedido creado"""
    print(f"Procesando pedido creado: {data.get('numeroPedido') or data.get('id')}")
    # Aquí tu lógica para procesar el pedido

def handle_pedido_actualizado(data, previous_data):
    """Procesa un evento de pedido actualizado"""
    print(f"Procesando pedido actualizado: {data.get('numeroPedido') or data.get('id')}")
    if previous_data:
        print(f"Estado anterior: {previous_data.get('estado')}")
        print(f"Estado nuevo: {data.get('estado')}")
    # Aquí tu lógica para procesar la actualización

def handle_cliente_creado(data):
    """Procesa un evento de cliente creado"""
    print(f"Procesando cliente creado: {data.get('nombre') or data.get('id')}")
    # Aquí tu lógica para procesar el cliente

def handle_cliente_actualizado(data, previous_data):
    """Procesa un evento de cliente actualizado"""
    print(f"Procesando cliente actualizado: {data.get('nombre') or data.get('id')}")
    if previous_data:
        print(f"Email anterior: {previous_data.get('email')}")
        print(f"Email nuevo: {data.get('email')}")
    # Aquí tu lógica para procesar la actualización

if __name__ == '__main__':
    app.run(port=3000, debug=True)

PHP

<?php

// Tu secret configurado en Obrify
define('WEBHOOK_SECRET', getenv('OBRIFY_WEBHOOK_SECRET') ?: 'tu_secret_aqui');

/**
 * Valida la firma HMAC-SHA256 del webhook
 */
function validateSignature($signature, $body, $secret) {
    if (empty($signature) || empty($secret)) {
        return false;
    }
    
    // Extraer la firma del header (formato: sha256=hex)
    $sigHash = str_replace('sha256=', '', $signature);
    
    // Calcular HMAC-SHA256
    $calculatedHash = hash_hmac('sha256', $body, $secret);
    
    // Comparación segura en tiempo constante
    return hash_equals($sigHash, $calculatedHash);
}

/**
 * Maneja el webhook recibido
 */
function handleWebhook() {
    // 1. Obtener el cuerpo raw
    $rawBody = file_get_contents('php://input');
    
    // 2. Obtener headers
    $headers = getallheaders();
    $signature = $headers['X-Webhook-Signature'] ?? '';
    
    // 3. Validar la firma
    if (!validateSignature($signature, $rawBody, WEBHOOK_SECRET)) {
        http_response_code(401);
        echo json_encode(['error' => 'Firma inválida']);
        return;
    }
    
    // 4. Parsear el JSON
    $payload = json_decode($rawBody, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        http_response_code(400);
        echo json_encode(['error' => 'JSON inválido']);
        return;
    }
    
    $event = $payload['event'] ?? '';
    $timestamp = $payload['timestamp'] ?? '';
    $tenantId = $payload['tenantId'] ?? '';
    $data = $payload['data'] ?? [];
    $previousData = $payload['previousData'] ?? null;
    
    error_log("Evento recibido: {$event} en {$timestamp} para tenant {$tenantId}");
    
    // 5. Procesar según el tipo de evento
    switch ($event) {
        case 'pedido.created':
            handlePedidoCreado($data);
            break;
        case 'pedido.updated':
            handlePedidoActualizado($data, $previousData);
            break;
        case 'cliente.created':
            handleClienteCreado($data);
            break;
        case 'cliente.updated':
            handleClienteActualizado($data, $previousData);
            break;
        default:
            error_log("Evento no manejado: {$event}");
    }
    
    // 6. Responder exitosamente
    http_response_code(200);
    header('Content-Type: application/json');
    echo json_encode([
        'success' => true,
        'message' => 'Evento procesado correctamente'
    ]);
}

/**
 * Procesa un evento de pedido creado
 */
function handlePedidoCreado($data) {
    error_log("Procesando pedido creado: " . ($data['numeroPedido'] ?? $data['id'] ?? 'N/A'));
    // Aquí tu lógica para procesar el pedido
}

/**
 * Procesa un evento de pedido actualizado
 */
function handlePedidoActualizado($data, $previousData) {
    error_log("Procesando pedido actualizado: " . ($data['numeroPedido'] ?? $data['id'] ?? 'N/A'));
    if ($previousData) {
        error_log("Estado anterior: " . ($previousData['estado'] ?? 'N/A'));
        error_log("Estado nuevo: " . ($data['estado'] ?? 'N/A'));
    }
    // Aquí tu lógica para procesar la actualización
}

/**
 * Procesa un evento de cliente creado
 */
function handleClienteCreado($data) {
    error_log("Procesando cliente creado: " . ($data['nombre'] ?? $data['id'] ?? 'N/A'));
    // Aquí tu lógica para procesar el cliente
}

/**
 * Procesa un evento de cliente actualizado
 */
function handleClienteActualizado($data, $previousData) {
    error_log("Procesando cliente actualizado: " . ($data['nombre'] ?? $data['id'] ?? 'N/A'));
    if ($previousData) {
        error_log("Email anterior: " . ($previousData['email'] ?? 'N/A'));
        error_log("Email nuevo: " . ($data['email'] ?? 'N/A'));
    }
    // Aquí tu lógica para procesar la actualización
}

// Ejecutar el handler
handleWebhook();
?>

Consideraciones para Todos los Lenguajes

  1. Cuerpo raw: Asegúrate de obtener el cuerpo de la petición sin parsear para validar la firma
  2. Comparación segura: Usa funciones de comparación en tiempo constante para prevenir timing attacks
  3. Manejo de errores: Siempre maneja errores y responde con códigos HTTP apropiados
  4. Logging: Registra todos los eventos recibidos para debugging
  5. Idempotencia: Diseña tu endpoint para manejar eventos duplicados
  6. Procesamiento asíncrono: Considera procesar eventos de forma asíncrona para responder rápidamente

Testing y Debugging

Cómo Probar Webhooks

1. Usando la Función de Prueba en Obrify

Obrify incluye un endpoint de prueba que permite disparar un webhook de prueba sin necesidad de que ocurra un evento real:

Endpoint: POST /api/webhooks/test/:id

Donde :id es el ID de la configuración del webhook que deseas probar.

Proceso:

  1. Obtén el ID de la configuración del webhook que deseas probar
  2. Realiza una petición POST al endpoint /api/webhooks/test/:id
  3. Obrify enviará un webhook de prueba para cada evento configurado en ese webhook
  4. Revisa los logs en Obrify para ver el resultado de cada envío

Ejemplo con cURL:

curl -X POST https://api.obrify.com/api/webhooks/test/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer tu_token_aqui" \
  -H "Content-Type: application/json"

Respuesta:

{
  "success": true,
  "message": "Webhook de prueba enviado para los eventos configurados. Revisa los logs para ver los resultados.",
  "data": {
    "eventosDisparados": ["pedido.created", "pedido.updated"]
  }
}

2. Usando Herramientas Externas

ngrok (Para desarrollo local)

Si estás desarrollando localmente, puedes usar ngrok para exponer tu servidor local:

# Instalar ngrok
# Descargar desde https://ngrok.com/

# Exponer tu servidor local en el puerto 3000
ngrok http 3000

# ngrok te dará una URL pública como:
# https://abc123.ngrok.io
# Usa esta URL en la configuración del webhook de Obrify
Postman / cURL (Para pruebas manuales)

Puedes simular webhooks manualmente usando herramientas como Postman o cURL:

# Ejemplo con cURL
curl -X POST https://api.tusistema.com/webhooks/obrify \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: sha256=tu_firma_aqui" \
  -d '{
    "event": "pedido.created",
    "timestamp": "2024-01-15T10:30:00.000Z",
    "tenantId": "123e4567-e89b-12d3-a456-426614174000",
    "data": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "numeroPedido": "PED-2024-001",
      "estado": "pendiente"
    }
  }'

Logs y Estadísticas en Obrify

Obrify mantiene logs detallados de todos los envíos de webhooks:

Información Disponible

  • Total de intentos: Número total de veces que se intentó enviar
  • Éxitos: Número de envíos exitosos (respuesta 2xx)
  • Fallidos: Número de envíos fallidos (después de agotar reintentos)
  • Pendientes: Envíos que están siendo reintentados
  • Tasa de éxito: Porcentaje de envíos exitosos

Detalles de Cada Envío

Para cada intento de envío, puedes ver:

  • Fecha y hora: Cuándo se intentó enviar
  • Evento: Qué evento se intentó enviar
  • Estado: Éxito, fallido, reintentando, pendiente
  • Intentos: Número de intentos realizados
  • Código de respuesta: Código HTTP recibido de tu endpoint
  • Respuesta: Cuerpo de la respuesta (si aplica)
  • Payload: Datos enviados en el webhook
  • Error: Mensaje de error si falló

Solución de Problemas Comunes

Problema: Webhook no se recibe

Posibles causas y soluciones:

  1. URL incorrecta
    • Verifica que la URL esté correctamente configurada
    • Asegúrate de que la URL sea accesible desde internet (no localhost)
    • Prueba la URL manualmente con una herramienta como cURL
  2. Firewall o restricciones de red
    • Verifica que tu servidor permita conexiones entrantes
    • Revisa reglas de firewall
    • Asegúrate de que el puerto esté abierto
  3. Webhook inactivo
    • Verifica que el webhook esté marcado como "Activo" en Obrify
  4. SSL/TLS
    • Si usas HTTPS, verifica que el certificado sea válido
    • Obrify no enviará a URLs con certificados SSL inválidos

Problema: Firma inválida (401)

Posibles causas y soluciones:

  1. Secret incorrecto
    • Verifica que el secret en tu código coincida con el configurado en Obrify
    • Asegúrate de no tener espacios adicionales o caracteres especiales
  2. Cuerpo modificado
    • Asegúrate de usar el cuerpo raw de la petición para validar
    • No parsees el JSON antes de validar la firma
    • Verifica que no haya middleware que modifique el cuerpo
  3. Encoding incorrecto
    • Asegúrate de usar UTF-8 para el encoding del cuerpo

Problema: Timeout

Posibles causas y soluciones:

  1. Procesamiento lento
    • Procesa eventos de forma asíncrona
    • Responde rápidamente (200 OK) y procesa después
    • Considera usar una cola de trabajos
  2. Timeout muy corto
    • Aumenta el timeout en la configuración del webhook
    • Pero recuerda: es mejor optimizar el procesamiento
  3. Problemas de red
    • Verifica la latencia de red entre Obrify y tu servidor
    • Considera usar un servidor más cercano geográficamente

Problema: Eventos duplicados

Solución:

  • Diseña tu endpoint para ser idempotente
  • Usa el ID del evento o un campo único para detectar duplicados
  • Implementa un sistema de deduplicación basado en IDs

Problema: Webhook se marca como fallido

Pasos para diagnosticar:

  1. Revisa los logs en Obrify para ver el error específico
  2. Revisa los logs de tu servidor
  3. Prueba el endpoint manualmente con cURL o Postman
  4. Verifica que tu endpoint responda con códigos HTTP apropiados
  5. Asegúrate de que no haya errores en tu código que causen excepciones no manejadas

Mejores Prácticas para Debugging

  1. Logs detallados: Registra todos los webhooks recibidos con su payload completo
  2. Validación paso a paso: Valida la firma, luego el JSON, luego los datos
  3. Respuestas informativas: Incluye mensajes de error descriptivos en las respuestas
  4. Monitoreo: Implementa alertas para webhooks que fallan repetidamente
  5. Testing en staging: Prueba los webhooks en un ambiente de staging antes de producción

Conclusión

Este documento proporciona toda la información necesaria para integrar tu sistema con los webhooks de Obrify. Si tienes preguntas adicionales o necesitas soporte, consulta la documentación de la API o contacta al equipo de soporte.

Recuerda:

  • Siempre valida la firma de los webhooks
  • Diseña tu endpoint para ser idempotente
  • Procesa eventos de forma asíncrona cuando sea posible
  • Mantén logs detallados para debugging
  • Prueba exhaustivamente antes de poner en producción

Última actualización: Enero 2024
Versión del documento: 1.0