Webhooks
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 enviara un POST con los datos del evento en el momento en que se produzca.
Configuracion
Puedes gestionar los webhooks desde el panel de Frihet (Configuracion > Webhooks) o de forma programatica a traves de la API REST.
Desde el panel
- Ve a Configuracion > 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 verificacion HMAC (recomendado)
- Guarda el webhook
API REST de webhooks
Gestiona webhooks de forma programatica 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 | Descripcion |
|---|---|---|---|
url | string | Si | URL de destino (HTTPS obligatorio en produccion) |
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 codigo 2xx en menos de 30 segundos
Limites:
- Maximo 20 webhooks por cuenta
- Payload maximo de 100 KB por entrega
Tipos de evento
Frihet emite 27 tipos de evento, agrupados por recurso.
Facturas (9 eventos)
| Evento | Descripcion |
|---|---|
invoice.created | Se ha creado una nueva factura |
invoice.updated | Se ha modificado una factura existente |
invoice.generated | Se ha generado una factura automaticamente (recurrente o desde presupuesto) |
invoice.one_off_created | Se ha creado una factura puntual (no recurrente) |
invoice.paid | Una factura ha sido marcada como pagada (completa) |
invoice.overdue | Una factura ha superado su fecha de vencimiento |
invoice.voided | Una factura ha sido anulada |
invoice.payment_status_updated | El estado de pago ha cambiado (parcial, fallido, reembolsado) |
invoice.payment_failure | Un intento de cobro automatico ha fallado |
Pagos (3 eventos)
| Evento | Descripcion |
|---|---|
payment.succeeded | Un pago se ha completado correctamente (Stripe, portal de cliente) |
payment.requires_action | Un pago requiere accion adicional (3D Secure, confirmacion bancaria) |
payment_receipt.created | Se ha generado un recibo de pago |
Facturas rectificativas (2 eventos)
| Evento | Descripcion |
|---|---|
credit_note.created | Se ha creado una factura rectificativa |
credit_note.generated | Se ha generado automaticamente una rectificativa |
Presupuestos (5 eventos)
| Evento | Descripcion |
|---|---|
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) |
quote.expired | Un presupuesto ha superado su fecha de validez |
Gastos (2 eventos)
| Evento | Descripcion |
|---|---|
expense.created | Se ha registrado un nuevo gasto |
expense.updated | Se ha modificado un gasto existente |
Clientes (3 eventos)
| Evento | Descripcion |
|---|---|
client.created | Se ha dado de alta un nuevo cliente |
client.updated | Se han modificado los datos de un cliente |
client.vies_check | Se ha verificado el NIF-IVA de un cliente contra el sistema VIES de la UE |
Productos (2 eventos)
| Evento | Descripcion |
|---|---|
product.created | Se ha creado un nuevo producto o servicio |
product.updated | Se ha modificado un producto o servicio existente |
Cobros (1 evento)
| Evento | Descripcion |
|---|---|
dunning.finished | El proceso de cobro automatico (dunning) ha finalizado para una factura |
Estructura del payload
Cada entrega de webhook es una peticion 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 peticion de webhook incluye las siguientes cabeceras:
| Cabecera | Descripcion | Ejemplo |
|---|---|---|
Content-Type | Tipo de contenido | application/json |
X-Frihet-Event | Tipo de evento | invoice.paid |
X-Frihet-Delivery-Id | Identificador unico 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.
Verificacion de firma
Si configuras un secreto al crear el webhook, Frihet firma cada payload con HMAC-SHA256. El proceso de verificacion consiste en:
- Tomar el cuerpo de la peticion 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 comparacion en tiempo constante (timingSafeEqual en Node.js, compare_digest en Python) para evitar ataques de timing.
Politica de reintentos
Si la entrega de un webhook falla (codigo de respuesta no-2xx o timeout), Frihet reintenta automaticamente con backoff exponencial:
| Intento | Retardo | Tiempo acumulado |
|---|---|---|
| 1 (inicial) | inmediato | 0s |
| 2 (primer reintento) | 2 segundos | 2s |
| 3 (segundo reintento) | 4 segundos | 6s |
- Maximo 3 intentos por entrega (1 inicial + 2 reintentos)
- Retardo maximo de 30 segundos entre reintentos
- Timeout de 30 segundos por peticion
- Si los 3 intentos fallan, la entrega se marca como
failed - En cada reintento se incluye la cabecera
X-Frihet-Retry-Attemptcon el numero de intento
Los reintentos se procesan mediante un job que se ejecuta cada 5 minutos (hasta 50 entregas pendientes por ejecucion), garantizando la fiabilidad incluso si el proceso principal se reinicia.
Circuit breaker
Si un webhook acumula 3 fallos consecutivos, Frihet lo pausa automaticamente para proteger tanto tu endpoint como el sistema de entregas.
- El webhook pasa a estado
pausedconpausedReason: "consecutive_failures" - No se envian mas entregas hasta que lo reactives manualmente
- Puedes reactivar el webhook desde el panel o via API (
PATCH /v1/webhooks/:idconactive: true)
Puedes consultar el historial de entregas de cada webhook desde el panel de Frihet, incluyendo el codigo 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 rapido
Tu endpoint debe responder con un codigo 200 lo antes posible. Si necesitas realizar un procesamiento pesado (enviar emails, actualizar bases de datos externas, etc.), acepta el evento y procesalo de forma asincrona en una cola de trabajo.
Gestiona la idempotencia
Es posible que un mismo evento se entregue mas de una vez (por ejemplo, si tu servidor respondio 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 confies en un webhook sin verificar la cabecera X-Frihet-Signature. Cualquier actor con acceso a tu URL podria enviar payloads falsos.
Usa HTTPS
En produccion, tu endpoint debe estar protegido con HTTPS. Frihet rechaza URLs HTTP (excepto en desarrollo local).
Monitoriza los fallos
Revisa periodicamente 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
Suscribete 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.
Depuracion de entregas
Frihet registra el resultado de cada entrega de webhook. Puedes consultarlo desde el panel:
- Ve a Ajustes > Desarrolladores > Webhooks
- Pulsa sobre el webhook que quieres inspeccionar
- Abre la pestana Entregas
Cada entrada muestra:
- Codigo 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
| Sintoma | Causa probable | Solucion |
|---|---|---|
| Todas las entregas fallan con timeout | Endpoint tarda mas de 30s en responder | Acepta el evento con un 200 inmediato y procesa de forma asincrona |
| Error SSL/TLS | Certificado expirado o cadena incompleta | Renueva el certificado y verifica la cadena con openssl s_client |
| Codigo 403 sistemático | Firewall bloqueando peticiones POST entrantes | Permite trafico desde las IPs de Google Cloud (us-central1) |
| Codigo 502/503 | Servidor caido o en mantenimiento | Revisa los logs de tu servidor y verifica que el proceso esta activo |
Boton de prueba
En la pantalla de detalle de cada webhook, el boton Enviar prueba envia un evento sintetico webhook.test a tu URL. Usa este boton para verificar:
- Que la URL es accesible desde internet
- Que la firma HMAC se valida correctamente
- Que tu servidor responde con un codigo
2xx
El resultado de la prueba aparece inmediatamente en el log de entregas.
Resolucion 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, asegurate de que las peticiones POST entrantes desde Google Cloud (
us-central1) estan permitidas
Las firmas no coinciden
- Verifica que estas usando el raw body de la peticion (antes de parsear el JSON)
- Confirma que el secreto en tu codigo 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 automaticamente el body antes de que puedas acceder al raw
Los reintentos no llegan
- Los reintentos se procesan cada 5 minutos. Si tu endpoint estuvo caido 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 automaticamente
Payload demasiado grande
Si el recurso asociado al evento es muy grande (muchas lineas en una factura, campos extensos), el payload puede superar el limite de 100 KB y la entrega sera rechazada. Simplifica los datos del recurso o contacta con soporte si necesitas un limite superior.