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
(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:
- Evento ocurre: Se crea un pedido, se registra un cobro, se actualiza una ruta, etc.
- Obrify procesa: El sistema identifica qué webhooks están configurados para ese evento
- Preparación: Se construye el payload JSON con los datos del evento
- Firma: Si está configurado un secret, se genera la firma HMAC-SHA256
- Envío: Se realiza una petición HTTP POST a la URL configurada
- Recepción: Tu endpoint recibe la petición
- Validación: Tu sistema valida la firma (si aplica) y procesa el evento
- Respuesta: Tu sistema responde con un código HTTP apropiado
- 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á notificacionesInactivo: 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:
- Obrify toma el cuerpo completo de la petición (JSON sin modificar)
- Calcula el HMAC-SHA256 usando el secret configurado
- Convierte el resultado a hexadecimal
- 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
- Extrae el valor del header
X-Webhook-Signature - Obtén el cuerpo raw de la petición (sin parsear)
- Calcula el HMAC-SHA256 del cuerpo usando tu secret
- Compara el resultado con la firma recibida
- Si coinciden, la petición es válida
- 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 UTCtenantId(string UUID): ID del tenant en Obrifydata(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 tipoupdated(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 clientecliente.updated- Se dispara cuando se actualiza un cliente existentecliente.deleted- Se dispara cuando se elimina un cliente
Eventos de Producto
producto.created- Se dispara cuando se crea un nuevo productoproducto.updated- Se dispara cuando se actualiza un producto existenteproducto.deleted- Se dispara cuando se elimina un producto
Eventos de Pedido
pedido.created- Se dispara cuando se crea un nuevo pedidopedido.updated- Se dispara cuando se actualiza un pedido existentepedido.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:
- Nombre del evento: Formato
entidad.accion(ej:pedido.created) - Descripción: Qué representa este evento
- Cuándo se dispara: En qué situación se envía este webhook
- Estructura del payload: Campos específicos en
data - 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 pedidonumeroPedido: Número de pedido legibleclienteId: ID del clientefechaPedido: Fecha de creaciónestado: 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 eventosproducto.created,producto.updated,producto.deleted,cliente.deletedypedido.deletedsiguen la misma estructura. Consulta el endpointGET /api/webhooks/eventspara 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:
- Primer intento: Inmediato cuando ocurre el evento
- Reintentos: Si falla, se reintenta según
maxReintentosconfigurado - Backoff exponencial: Los reintentos esperan progresivamente más tiempo entre intentos
- 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
- Responde rápidamente: Acepta el webhook y responde 200 OK, luego procesa asíncronamente
- Valida la firma primero: Antes de procesar, valida que la petición sea legítima
- Valida el payload: Verifica que los datos recibidos sean válidos antes de procesarlos
- Maneja errores gracefully: Si hay un error, responde con el código HTTP apropiado
- Logs: Registra todos los webhooks recibidos para debugging y auditoría
- 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
- Cuerpo raw: Asegúrate de obtener el cuerpo de la petición sin parsear para validar la firma
- Comparación segura: Usa funciones de comparación en tiempo constante para prevenir timing attacks
- Manejo de errores: Siempre maneja errores y responde con códigos HTTP apropiados
- Logging: Registra todos los eventos recibidos para debugging
- Idempotencia: Diseña tu endpoint para manejar eventos duplicados
- 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:
- Obtén el ID de la configuración del webhook que deseas probar
- Realiza una petición POST al endpoint
/api/webhooks/test/:id - Obrify enviará un webhook de prueba para cada evento configurado en ese webhook
- 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:
- 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
- Firewall o restricciones de red
- Verifica que tu servidor permita conexiones entrantes
- Revisa reglas de firewall
- Asegúrate de que el puerto esté abierto
- Webhook inactivo
- Verifica que el webhook esté marcado como "Activo" en Obrify
- 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:
- 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
- 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
- Encoding incorrecto
- Asegúrate de usar UTF-8 para el encoding del cuerpo
Problema: Timeout
Posibles causas y soluciones:
- Procesamiento lento
- Procesa eventos de forma asíncrona
- Responde rápidamente (200 OK) y procesa después
- Considera usar una cola de trabajos
- Timeout muy corto
- Aumenta el timeout en la configuración del webhook
- Pero recuerda: es mejor optimizar el procesamiento
- 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:
- Revisa los logs en Obrify para ver el error específico
- Revisa los logs de tu servidor
- Prueba el endpoint manualmente con cURL o Postman
- Verifica que tu endpoint responda con códigos HTTP apropiados
- Asegúrate de que no haya errores en tu código que causen excepciones no manejadas
Mejores Prácticas para Debugging
- Logs detallados: Registra todos los webhooks recibidos con su payload completo
- Validación paso a paso: Valida la firma, luego el JSON, luego los datos
- Respuestas informativas: Incluye mensajes de error descriptivos en las respuestas
- Monitoreo: Implementa alertas para webhooks que fallan repetidamente
- 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