Saltar al contenido principal

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

  1. Ve a Configuracion > Webhooks en tu cuenta de Frihet
  2. Pulsa Crear webhook
  3. Introduce un nombre descriptivo, la URL de destino y selecciona los eventos que quieres recibir
  4. Opcionalmente, define un secreto para la verificacion HMAC (recomendado)
  5. 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
CampoTipoRequeridoDescripcion
urlstringSiURL de destino (HTTPS obligatorio en produccion)
eventsstring[]SiLista de tipos de evento a recibir
secretstringNoSecreto para firma HMAC-SHA256 (recomendado)
activebooleanNoEstado 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 localhost y 127.0.0.1 durante 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)

EventoDescripcion
invoice.createdSe ha creado una nueva factura
invoice.updatedSe ha modificado una factura existente
invoice.generatedSe ha generado una factura automaticamente (recurrente o desde presupuesto)
invoice.one_off_createdSe ha creado una factura puntual (no recurrente)
invoice.paidUna factura ha sido marcada como pagada (completa)
invoice.overdueUna factura ha superado su fecha de vencimiento
invoice.voidedUna factura ha sido anulada
invoice.payment_status_updatedEl estado de pago ha cambiado (parcial, fallido, reembolsado)
invoice.payment_failureUn intento de cobro automatico ha fallado

Pagos (3 eventos)

EventoDescripcion
payment.succeededUn pago se ha completado correctamente (Stripe, portal de cliente)
payment.requires_actionUn pago requiere accion adicional (3D Secure, confirmacion bancaria)
payment_receipt.createdSe ha generado un recibo de pago

Facturas rectificativas (2 eventos)

EventoDescripcion
credit_note.createdSe ha creado una factura rectificativa
credit_note.generatedSe ha generado automaticamente una rectificativa

Presupuestos (5 eventos)

EventoDescripcion
quote.createdSe ha creado un nuevo presupuesto
quote.updatedSe ha modificado un presupuesto existente
quote.acceptedUn cliente ha aceptado un presupuesto (cambio de estado a accepted)
quote.rejectedUn cliente ha rechazado un presupuesto (cambio de estado a rejected)
quote.expiredUn presupuesto ha superado su fecha de validez

Gastos (2 eventos)

EventoDescripcion
expense.createdSe ha registrado un nuevo gasto
expense.updatedSe ha modificado un gasto existente

Clientes (3 eventos)

EventoDescripcion
client.createdSe ha dado de alta un nuevo cliente
client.updatedSe han modificado los datos de un cliente
client.vies_checkSe ha verificado el NIF-IVA de un cliente contra el sistema VIES de la UE

Productos (2 eventos)

EventoDescripcion
product.createdSe ha creado un nuevo producto o servicio
product.updatedSe ha modificado un producto o servicio existente

Cobros (1 evento)

EventoDescripcion
dunning.finishedEl 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:

CabeceraDescripcionEjemplo
Content-TypeTipo de contenidoapplication/json
X-Frihet-EventTipo de eventoinvoice.paid
X-Frihet-Delivery-IdIdentificador unico de la entregad4e5f6a7b8c9
X-Frihet-TimestampMarca de tiempo ISO 86012026-02-12T14:30:00.000Z
X-Frihet-SignatureFirma HMAC-SHA256 del payloadsha256=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:

  1. Tomar el cuerpo de la peticion en bruto (raw body)
  2. Calcular el HMAC-SHA256 usando el secreto como clave
  3. Comparar el resultado con el valor de la cabecera X-Frihet-Signature (sin el prefijo sha256=)

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:

IntentoRetardoTiempo acumulado
1 (inicial)inmediato0s
2 (primer reintento)2 segundos2s
3 (segundo reintento)4 segundos6s
  • 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-Attempt con 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 paused con pausedReason: "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/:id con active: 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:

  1. Ve a Ajustes > Desarrolladores > Webhooks
  2. Pulsa sobre el webhook que quieres inspeccionar
  3. 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, retrying o failed

Problemas habituales

SintomaCausa probableSolucion
Todas las entregas fallan con timeoutEndpoint tarda mas de 30s en responderAcepta el evento con un 200 inmediato y procesa de forma asincrona
Error SSL/TLSCertificado expirado o cadena incompletaRenueva el certificado y verifica la cadena con openssl s_client
Codigo 403 sistemáticoFirewall bloqueando peticiones POST entrantesPermite trafico desde las IPs de Google Cloud (us-central1)
Codigo 502/503Servidor caido o en mantenimientoRevisa 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

  1. Verifica que la URL del webhook es correcta y accesible desde internet
  2. Comprueba que el webhook esta en estado activo en el panel
  3. Revisa los logs de entrega para ver si hay errores
  4. Si usas un firewall, asegurate de que las peticiones POST entrantes desde Google Cloud (us-central1) estan permitidas

Las firmas no coinciden

  1. Verifica que estas usando el raw body de la peticion (antes de parsear el JSON)
  2. Confirma que el secreto en tu codigo coincide con el configurado en el panel de Frihet
  3. No modifiques ni reformatees el body antes de verificar la firma
  4. Revisa que tu framework no esta parseando automaticamente el body antes de que puedas acceder al raw

Los reintentos no llegan

  1. Los reintentos se procesan cada 5 minutos. Si tu endpoint estuvo caido brevemente, puede que ya se hayan agotado los intentos
  2. Revisa el historial de entregas para confirmar el estado de cada intento
  3. 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.