Webhooks y verificación de firma
Los webhooks de Frihet permiten recibir notificaciones HTTP en tiempo real cuando ocurren eventos en tu cuenta. En lugar de hacer polling a la API, configura una URL y Frihet enviará un POST con los datos del evento en el momento en que se produzca.
Recorrido rápido: de tu primera lectura API a un webhook verificado
Si ya completaste tu primera lectura autenticada con GET /v1/clients?limit=5 en Desarrolladores: primeros pasos, este recorrido valida el siguiente hito de integración: comprobar permisos y plan, verificar criptográficamente la firma X-Frihet-Signature (HMAC-SHA256) sobre el cuerpo crudo (rawBody) antes de abrir tráfico externo y confirmar la entrega desde el panel o vía API REST.
1. Requisitos previos, permisos y límite de plan
Antes de registrar un endpoint en POST /v1/webhooks o desde Integraciones → Webhooks, verifica estos cuatro requisitos de contrato:
| Requisito | Contrato en producción | Dónde comprobarlo |
|---|---|---|
| Lectura API previa | Haber recibido 200 OK en GET https://api.frihet.io/v1/clients?limit=5 con tu cabecera X-API-Key: fri_... | Desarrolladores: primeros pasos |
| Permisos de la clave | Las API keys actuales tienen acceso completo a los recursos de la cuenta (read y write sobre /v1/*). No existen scopes granulares por recurso en v1 | API Keys (Integraciones → API Keys) |
| Plan requerido y cupo | Los webhooks requieren el plan Premium (o Enterprise). Límites por cuenta: máximo 20 webhooks registrados y 100 KB por payload entregado | Ajustes → Plan y Facturación y Planes y facturación |
URL y secreto (secret) | url pública con https:// (http://localhost o 127.0.0.1 solo en desarrollo local; IPs privadas RFC 1918 bloqueadas contra SSRF). El campo secret (máximo 256 caracteres) es solo de escritura en POST /v1/webhooks y PATCH /v1/webhooks/:id: las lecturas devuelven hasSecret: true sin exponer el texto plano | Configuración de webhooks |
2. Recorrido determinista: verificar firma HMAC-SHA256 en local
El fallo más frecuente al integrar webhooks es calcular el HMAC sobre un objeto JSON ya parseado y re-serializado por el framework web, lo que altera espacios o claves y rompe la firma. El flujo completo de firma, verificación en tiempo constante y deduplicación por X-Frihet-Delivery-Id sigue esta secuencia:
Flujo de verificación: Evento webhook → Firma HMAC → Validación → Rechazo o aceptación
X-Frihet-Signature (HMAC-SHA256) + X-Frihet-Delivery-Idinvoice.paidPOST HTTPSFrihet serializa el evento (máx. 100 KB) y genera un identificador único de entrega X-Frihet-Delivery-Id.
HMAC-SHA256X-Frihet-SignatureCon el secret del endpoint, Frihet calcula sha256=<hex> exactamente sobre los bytes crudos UTF-8 del cuerpo HTTP.
timingSafeEqualDeduplicaciónTu servidor lee el rawBody antes de parsear JSON, recalcula el HMAC-SHA256 y comprueba si X-Frihet-Delivery-Id ya fue procesado.
401 Rechazo200 AceptaciónSi la firma falla responde 401; si es válida responde 200 OK rápido y ejecuta la lógica de negocio una sola vez.
El digest calculado no coincide con X-Frihet-Signature. El receptor descarta el payload sin tocar base de datos; Frihet registra el fallo y aplica hasta 3 reintentos con backoff exponencial.
La firma coincide en tiempo constante. Si X-Frihet-Delivery-Id es nuevo, se guarda y se procesa el evento; si ya existía por un reintento de red, se devuelve 200 OK sin duplicar efectos.
Ver especificación textual y fuente Mermaid de la secuencia de validación
sequenceDiagram
participant F as Frihet ERP
participant R as Receptor HTTPS (rawBody)
F->>R: POST payload + X-Frihet-Signature (sha256=...) + X-Frihet-Delivery-Id
R->>R: Calcular HMAC-SHA256(secret, rawBody) y comparar con timingSafeEqual
alt Firma inválida o cuerpo alterado
R-->>F: 401 Unauthorized (Rechazar entrega, desencadena reintento con backoff)
else Firma válida y X-Frihet-Delivery-Id ya procesado
R-->>F: 200 OK idempotente (Ignorar duplicado sin reprocesar negocio)
else Firma válida y X-Frihet-Delivery-Id nuevo
R-->>F: 200 OK (< 30s) + registrar X-Frihet-Delivery-Id + encolar trabajo
endPuedes comprobar en vivo los tres escenarios (firma válida 200 OK, cuerpo alterado 401 Unauthorized e identificador de entrega repetido 200 OK idempotente) directamente en el navegador con el simulador local determinista, o ejecutar el comando de Node.js en tu terminal:
Laboratorio local de verificación de firma e idempotencia
100 % local · Web Crypto HMAC-SHA256 · Sin redPrueba los tres casos sobre el fixture sintético de Estudio Norte S.L. (F-2026-0001) o edita el rawBody para recalcular el HMAC-SHA256 con TextEncoder + Web Crypto. La lista en memoria simula la deduplicación por X-Frihet-Delivery-Id (no sustituye persistencia duradera ni control de concurrencia en tu base de datos).
El digest HMAC-SHA256 calculado con Web Crypto sobre los bytes UTF-8 del rawBody coincide con X-Frihet-Signature y X-Frihet-Delivery-Id no figura en la lista en memoria.
- HMAC-SHA256 calculado con Web Crypto sobre rawBody (esperado):
sha256=970173bb5ff9661383456c2737b482c7f654992ec27204d88fa87bf9fc53f10a - Cabecera X-Frihet-Signature verificada con crypto.subtle.verify (en tu servidor Node usa crypto.timingSafeEqual):
sha256=970173bb5ff9661383456c2737b482c7f654992ec27204d88fa87bf9fc53f10a - Efecto simulado en el receptor:Registrar del_01J8X9N8Q4 y procesar el cobro de F-2026-0001.
Antes de exponer tu receptor HTTPS en internet, ejecuta también esta prueba determinista en local con Node.js (crypto nativo, sin dependencias externas). Utiliza el fixture sintético de documentación (Estudio Norte S.L., NIF B12345674, facturacion@example.com, factura F-2026-0001) y el secreto de ejemplo whsec_test_ejemplo_doc_no_usar_en_produccion:
node -e '
const crypto = require("crypto");
const secret = "whsec_test_ejemplo_doc_no_usar_en_produccion";
const rawBody = "{\"event\":\"invoice.paid\",\"timestamp\":\"2026-10-15T10:00:00.000Z\",\"data\":{\"id\":\"inv_01J8X9N4P2\",\"invoiceNumber\":\"F-2026-0001\",\"clientId\":\"cli_01J8X9M2K4\",\"clientName\":\"Estudio Norte S.L.\",\"clientTaxId\":\"B12345674\",\"clientEmail\":\"facturacion@example.com\",\"subtotal\":1500,\"taxAmount\":315,\"total\":1815,\"status\":\"paid\"}}";
const headerSig = "sha256=970173bb5ff9661383456c2737b482c7f654992ec27204d88fa87bf9fc53f10a";
const expectedHex = crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const receivedHex = headerSig.replace(/^sha256=/, "");
const isValid =
expectedHex.length === receivedHex.length &&
crypto.timingSafeEqual(Buffer.from(expectedHex, "hex"), Buffer.from(receivedHex, "hex"));
console.log(JSON.stringify({ isValid, event: "invoice.paid", signaturePrefix: headerSig.slice(0, 23) + "..." }));
'
- Por qué
B12345674yexample.com:B12345674supera la validación algorítmica del dígito de control español (4), pero no acredita existencia, inexistencia ni titularidad en el censo de la AEAT; yexample.comes un dominio reservado para documentación por IANA (RFC 6761), no un buzón controlado de QA ni una garantía de bloqueo de envío. No uses estos datos para emitir o enviar facturas reales. - Qué prueba este comando y qué requiere red real: este comando verifica de forma determinista tu lógica de firma (
createHmacsobrerawBody+ comparación en tiempo constantetimingSafeEqual). Para comprobar la conectividad de red, TLS y cortafuegos desde Google Cloud (us-central1), registra tu URL en Integraciones → Webhooks y pulsa Enviar prueba (que entrega un payloadwebhook.test).
3. Resultado esperado: verificación local y entrega de prueba
- En tu terminal local (comando anterior): obtienes exactamente esta salida con código de salida
0:
{ "isValid": true, "event": "invoice.paid", "signaturePrefix": "sha256=970173bb5ff96613..." }
- Al registrar el webhook (
POST /v1/webhooks): la API responde201 Createdcon el identificador del webhook (id),active: true,hasSecret: truey la lista de los tipos de evento suscritos (dentro del catálogo de 15 tipos de evento). - Al pulsar Enviar prueba en el panel (Integraciones → Webhooks): tu servidor recibe una petición
POSTcon las cabecerasX-Frihet-Event: webhook.test,X-Frihet-Delivery-Id,X-Frihet-TimestampyX-Frihet-Signature. Si tu endpoint responde200 OKen menos de 30 segundos, la pestaña Entregas registra el intento comodelivered.
Puedes registrar el endpoint y disparar la entrega de prueba desde cualquiera de los tres canales equivalentes:
- Interfaz web
- API REST
- MCP
- Ve a Integraciones → Webhooks y pulsa Crear webhook.
- Introduce tu URL HTTPS (
https://...), seleccionainvoice.paidy define tusecretde verificación. - Abre el webhook recién creado, pulsa Enviar prueba (
webhook.test) y comprueba en la pestaña Entregas que aparece comodelivered(200 OK).
Registra el webhook mediante POST /v1/webhooks enviando la cabecera X-API-Key:
curl -X POST "https://api.frihet.io/v1/webhooks" \
-H "X-API-Key: $FRIHET_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/frihet",
"events": ["invoice.paid"],
"secret": "whsec_test_ejemplo_doc_no_usar_en_produccion",
"active": true
}'
Desde tu agente conectado a @frihet/mcp-server, usa create_webhook para dar de alta la suscripción y test_webhook para lanzar el evento webhook.test:
{
"tool": "create_webhook",
"arguments": {
"url": "https://hooks.example.com/frihet",
"events": ["invoice.paid"],
"secret": "whsec_test_ejemplo_doc_no_usar_en_produccion"
}
}
4. Diagnóstico y recuperación de webhooks
Si el registro del webhook o la verificación de entregas falla, aplica esta tabla de recuperación directa:
| Síntoma o código | Causa raíz verificable | Acción de recuperación |
|---|---|---|
isValid: false o 401 en tu receptor | Tu servidor aplicó JSON.parse antes de calcular el HMAC o usó un secret distinto al configurado | Pasa los bytes crudos (express.raw({ type: 'application/json' }) en Express o request.data en Flask) a createHmac. Si perdiste el secreto, rótalo con PATCH /v1/webhooks/:id enviando un nuevo campo secret |
403 Forbidden al llamar a POST /v1/webhooks | La cuenta no está en el plan Premium o ya alcanzó el tope de 20 webhooks activos/inactivos | Revisa tu suscripción en Ajustes → Plan y Facturación o lista tus registros actuales con GET /v1/webhooks y elimina los obsoletos con DELETE /v1/webhooks/:id |
400 Bad Request al guardar la url | La URL usa http:// fuera de localhost/127.0.0.1, resuelve a una IP privada (protección SSRF) o incluye tipos de evento fuera de los 15 tipos de evento soportados | Usa un dominio público con certificado TLS válido (https://) y comprueba los nombres exactos en Tipos de evento |
Estado paused (pausedReason: "consecutive_failures") | El circuit breaker pausó el webhook tras 3 entregas fallidas consecutivas (cada una con hasta 3 intentos y timeout de 30 s) | Corrige el error 5xx/timeout en tu servidor, verifica que responde 2xx en menos de 30 s y reactívalo desde Integraciones → Webhooks o enviando PATCH /v1/webhooks/:id con {"active": true} |
5. Siguiente acción
- Conectar un agente de IA o IDE por protocolo MCP: continúa con el Servidor MCP o la guía paso a paso Conecta Frihet a Claude, Cursor o ChatGPT.
- Operar por HTTP/JSON directo: consulta los esquemas completos, paginación e idempotencia en la referencia de la API REST.
- Gestionar webhooks sin código desde la interfaz: revisa Configuración de webhooks en la aplicación.
Configuración
Puedes gestionar los webhooks desde el panel de Frihet (Integraciones → Webhooks) o de forma programática a través de la API REST.
Desde el panel
- Ve a Integraciones → Webhooks en tu cuenta de Frihet
- Pulsa Crear webhook
- Introduce un nombre descriptivo, la URL de destino y selecciona los eventos que quieres recibir
- Opcionalmente, define un secreto para la verificación HMAC (recomendado)
- Guarda el webhook
API REST de webhooks
Gestiona webhooks de forma programática con los endpoints CRUD completos.
Listar webhooks
GET /v1/webhooks
curl https://api.frihet.io/v1/webhooks \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta (200):
{
"data": [
{
"id": "wh_abc123",
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "invoice.created"],
"active": true,
"createdAt": "2026-02-01T10:00:00.000Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Obtener webhook
GET /v1/webhooks/:id
curl https://api.frihet.io/v1/webhooks/wh_abc123 \
-H "X-API-Key: fri_tu-clave-aqui"
Crear webhook
POST /v1/webhooks
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url | string | Si | URL de destino (HTTPS obligatorio en producción) |
events | string[] | Si | Lista de tipos de evento a recibir |
secret | string | No | Secreto para firma HMAC-SHA256 (recomendado) |
active | boolean | No | Estado del webhook (defecto: true) |
curl -X POST https://api.frihet.io/v1/webhooks \
-H "X-API-Key: fri_tu-clave-aqui" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "expense.created"],
"secret": "mi-secreto-hmac"
}'
Respuesta (201):
{
"id": "wh_def456",
"url": "https://mi-app.com/hook",
"events": ["invoice.paid", "expense.created"],
"active": true,
"createdAt": "2026-03-18T10:00:00.000Z"
}
Actualizar webhook
PATCH /v1/webhooks/:id
Solo necesitas enviar los campos que quieras modificar.
curl -X PATCH https://api.frihet.io/v1/webhooks/wh_def456 \
-H "X-API-Key: fri_tu-clave-aqui" \
-H "Content-Type: application/json" \
-d '{
"events": ["invoice.paid", "invoice.created", "expense.created"],
"active": false
}'
Respuesta (200): Objeto de webhook actualizado.
Eliminar webhook
DELETE /v1/webhooks/:id
curl -X DELETE https://api.frihet.io/v1/webhooks/wh_def456 \
-H "X-API-Key: fri_tu-clave-aqui"
Respuesta: 204 No Content
Requisitos de la URL:
- Debe usar HTTPS (se permite HTTP solo para
localhosty127.0.0.1durante el desarrollo) - No se permiten IPs privadas (10.x, 172.16-31.x, 192.168.x) para evitar ataques SSRF
- Debe responder con un código 2xx en menos de 30 segundos
Límites:
- Máximo 20 webhooks por cuenta
- Payload máximo de 100 KB por entrega
Tipos de evento
Frihet emite 15 tipos de evento, agrupados por recurso.
Facturas (4 eventos)
| Evento | Descripción |
|---|---|
invoice.created | Se ha creado una nueva factura |
invoice.updated | Se ha modificado una factura existente |
invoice.paid | Una factura ha sido marcada como pagada (completa) |
invoice.overdue | Una factura ha superado su fecha de vencimiento |
Pagos (1 evento)
| Evento | Descripción |
|---|---|
payment.succeeded | Un pago se ha completado correctamente (Stripe, portal de cliente) |
Presupuestos (4 eventos)
| Evento | Descripción |
|---|---|
quote.created | Se ha creado un nuevo presupuesto |
quote.updated | Se ha modificado un presupuesto existente |
quote.accepted | Un cliente ha aceptado un presupuesto (cambio de estado a accepted) |
quote.rejected | Un cliente ha rechazado un presupuesto (cambio de estado a rejected) |
Gastos (2 eventos)
| Evento | Descripción |
|---|---|
expense.created | Se ha registrado un nuevo gasto |
expense.updated | Se ha modificado un gasto existente |
Clientes (2 eventos)
| Evento | Descripción |
|---|---|
client.created | Se ha dado de alta un nuevo cliente |
client.updated | Se han modificado los datos de un cliente |
Productos (2 eventos)
| Evento | Descripción |
|---|---|
product.created | Se ha creado un nuevo producto o servicio |
product.updated | Se ha modificado un producto o servicio existente |
Estructura del payload
Cada entrega de webhook es una petición POST con cuerpo JSON. La estructura es la misma para todos los tipos de evento:
{
"event": "invoice.paid",
"timestamp": "2026-02-12T14:30:00.000Z",
"data": {
"id": "inv_abc123",
"clientName": "Acme S.L.",
"items": [{ "description": "Consultoria", "quantity": 10, "unitPrice": 75 }],
"total": 750,
"status": "paid",
"paidAt": "2026-02-12T14:29:58.000Z",
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-02-12T14:30:00.000Z"
}
}
El campo data contiene el estado completo del recurso en el momento del evento.
Cabeceras de entrega
Cada petición de webhook incluye las siguientes cabeceras:
| Cabecera | Descripción | Ejemplo |
|---|---|---|
Content-Type | Tipo de contenido | application/json |
X-Frihet-Event | Tipo de evento | invoice.paid |
X-Frihet-Delivery-Id | Identificador único de la entrega | d4e5f6a7b8c9 |
X-Frihet-Timestamp | Marca de tiempo ISO 8601 | 2026-02-12T14:30:00.000Z |
X-Frihet-Signature | Firma HMAC-SHA256 del payload | sha256=a1b2c3d4e5f6... |
La cabecera X-Frihet-Signature solo se incluye si has configurado un secreto en el webhook.
Verificación de firma
Si configuras un secreto al crear el webhook, Frihet firma cada payload con HMAC-SHA256. El proceso de verificación consiste en:
- Tomar el cuerpo de la petición en bruto (raw body)
- Calcular el HMAC-SHA256 usando el secreto como clave
- Comparar el resultado con el valor de la cabecera
X-Frihet-Signature(sin el prefijosha256=)
Con el SDK (recomendado)
import { Webhooks } from '@frihet/sdk';
app.post('/webhook/frihet', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = Webhooks.verifySignature(
req.body,
req.headers['x-frihet-signature'],
process.env.FRIHET_WEBHOOK_SECRET,
);
if (!isValid) return res.status(401).send('Firma invalida');
const event = JSON.parse(req.body.toString());
console.log(req.headers['x-frihet-event'], event.data);
res.sendStatus(200);
});
Ejemplo manual en Node.js
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) {
const expectedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const receivedSignature = signature.replace('sha256=', '');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex'),
);
}
// Uso en un servidor Express
app.post('/webhook/frihet', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-frihet-signature'];
const secret = process.env.FRIHET_WEBHOOK_SECRET;
if (!signature || !verifyWebhookSignature(req.body.toString(), signature, secret)) {
return res.status(401).send('Firma invalida');
}
const event = JSON.parse(req.body.toString());
const eventType = req.headers['x-frihet-event'];
console.log(`Evento recibido: ${eventType}`, event.data);
// Procesar el evento segun su tipo
switch (eventType) {
case 'invoice.paid':
// Actualizar tu sistema de contabilidad
break;
case 'expense.created':
// Notificar al equipo financiero
break;
}
res.status(200).send('OK');
});
Ejemplo en Python
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = 'tu-secreto-aqui'
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
received = signature.replace('sha256=', '')
return hmac.compare_digest(expected, received)
@app.route('/webhook/frihet', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Frihet-Signature', '')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
event_type = request.headers.get('X-Frihet-Event')
print(f'Evento recibido: {event_type}')
if event_type == 'invoice.paid':
# Actualizar tu sistema de contabilidad
pass
elif event_type == 'quote.accepted':
# Convertir presupuesto a factura
pass
return 'OK', 200
Importante: Usa siempre una comparación en tiempo constante (timingSafeEqual en Node.js, compare_digest en Python) para evitar ataques de timing.
Política de reintentos
Si la entrega de un webhook falla (código de respuesta no-2xx o timeout), Frihet reintenta automáticamente con backoff exponencial:
| Intento | Retardo | Tiempo acumulado |
|---|---|---|
| 1 (inicial) | inmediato | 0s |
| 2 (primer reintento) | 2 segundos | 2s |
| 3 (segundo reintento) | 4 segundos | 6s |
- Máximo 3 intentos por entrega (1 inicial + 2 reintentos)
- Retardo máximo de 30 segundos entre reintentos
- Timeout de 30 segundos por petición
- Si los 3 intentos fallan, la entrega se marca como
failed - En cada reintento se incluye la cabecera
X-Frihet-Retry-Attemptcon el número de intento
Los reintentos se procesan mediante un job que se ejecuta cada 5 minutos (hasta 50 entregas pendientes por ejecución), garantizando la fiabilidad incluso si el proceso principal se reinicia.
Circuit breaker
Si un webhook acumula 3 fallos consecutivos, Frihet lo pausa automáticamente para proteger tanto tu endpoint como el sistema de entregas.
- El webhook pasa a estado
pausedconpausedReason: "consecutive_failures" - No se envían más entregas hasta que lo reactives manualmente
- Puedes reactivar el webhook desde el panel o vía API (
PATCH /v1/webhooks/:idconactive: true)
Puedes consultar el historial de entregas de cada webhook desde el panel de Frihet, incluyendo el código de respuesta, el cuerpo de la respuesta (primeros 1000 caracteres) y los errores de cada intento.
Pruebas
Desde el panel de Frihet puedes enviar un evento de prueba a cualquier webhook configurado. El payload de prueba tiene la siguiente estructura:
{
"event": "webhook.test",
"timestamp": "2026-02-12T14:30:00.000Z",
"data": {
"message": "This is a test webhook from Frihet ERP"
}
}
Esto permite verificar que tu endpoint esta accesible, que la firma se valida correctamente y que tu sistema procesa los eventos sin errores.
Buenas practicas
Responde rápido
Tu endpoint debe responder con un código 200 lo antes posible. Si necesitas realizar un procesamiento pesado (enviar emails, actualizar bases de datos externas, etc.), acepta el evento y procésalo de forma asíncrona en una cola de trabajo.
Gestiona la idempotencia
Es posible que un mismo evento se entregue más de una vez (por ejemplo, si tu servidor respondió con un timeout pero proceso el evento). Usa el campo X-Frihet-Delivery-Id como clave de idempotencia para evitar duplicados.
Verifica la firma siempre
Nunca confíes en un webhook sin verificar la cabecera X-Frihet-Signature. Cualquier actor con acceso a tu URL podría enviar payloads falsos.
Usa HTTPS
En producción, tu endpoint debe estar protegido con HTTPS. Frihet rechaza URLs HTTP (excepto en desarrollo local).
Monitoriza los fallos
Revisa periódicamente los logs de entrega en el panel de Frihet. Si ves entregas fallidas de forma recurrente, verifica que tu endpoint esta disponible y responde en menos de 30 segundos.
Filtra los eventos
Suscríbete solo a los eventos que necesitas. Cada webhook puede escuchar uno o varios tipos de evento. Cuantos menos eventos innecesarios proceses, menor sera la carga en tu servidor.
Depuración de entregas
Frihet registra el resultado de cada entrega de webhook. Puedes consultarlo desde el panel:
- Ve a Integraciones → Webhooks
- Pulsa sobre el webhook que quieres inspeccionar
- Abre la pestaña Entregas
Cada entrada muestra:
- Código de respuesta HTTP devuelto por tu endpoint
- Tiempo de respuesta en milisegundos
- Cuerpo de la respuesta (primeros 1000 caracteres)
- Fecha y hora de cada intento (incluidos los reintentos)
- Estado final:
delivered,retryingofailed
Problemas habituales
| Síntoma | Causa probable | Solución |
|---|---|---|
| Todas las entregas fallan con timeout | Endpoint tarda más de 30s en responder | Acepta el evento con un 200 inmediato y procesa de forma asíncrona |
| Error SSL/TLS | Certificado expirado o cadena incompleta | Renueva el certificado y verifica la cadena con openssl s_client |
| Código 403 sistemático | Firewall bloqueando peticiones POST entrantes | Permite trafico desde las IPs de Google Cloud (us-central1) |
| Código 502/503 | Servidor caído o en mantenimiento | Revisa los logs de tu servidor y verifica que el proceso esta activo |
Botón de prueba
En la pantalla de detalle de cada webhook, el botón Enviar prueba envía un evento sintético webhook.test a tu URL. Usa este botón para verificar:
- Que la URL es accesible desde internet
- Que la firma HMAC se valida correctamente
- Que tu servidor responde con un código
2xx
El resultado de la prueba aparece inmediatamente en el log de entregas.
Resolución de problemas
No recibo webhooks
- Verifica que la URL del webhook es correcta y accesible desde internet
- Comprueba que el webhook esta en estado activo en el panel
- Revisa los logs de entrega para ver si hay errores
- Si usas un firewall, asegúrate de que las peticiones POST entrantes desde Google Cloud (
us-central1) están permitidas
Las firmas no coinciden
- Verifica que estas usando el raw body de la petición (antes de parsear el JSON)
- Confirma que el secreto en tu código coincide con el configurado en el panel de Frihet
- No modifiques ni reformatees el body antes de verificar la firma
- Revisa que tu framework no esta parseando automáticamente el body antes de que puedas acceder al raw
Los reintentos no llegan
- Los reintentos se procesan cada 5 minutos. Si tu endpoint estuvo caído brevemente, puede que ya se hayan agotado los intentos
- Revisa el historial de entregas para confirmar el estado de cada intento
- Si los 3 intentos fallaron, la entrega no se reintentara de nuevo automáticamente
Payload demasiado grande
Si el recurso asociado al evento es muy grande (muchas líneas en una factura, campos extensos), el payload puede superar el límite de 100 KB y la entrega sera rechazada. Simplifica los datos del recurso o contacta con soporte si necesitas un límite superior.
Relacionado: Configuración de webhooks · API REST · Servidor MCP