1. Registra tu endpoint
En el panel de Isaak, sección Webhooks, pulsa "Añadir endpoint". Introduce tu URL HTTPS y selecciona los eventos que quieras recibir. Isaak te dará un signing secret (whsec_…) que necesitarás en el paso 3.
Recibe eventos en tiempo real (invoice.issued, certificate.expiring…) y verifica la firma HMAC-SHA256 con código de ejemplo en Node, Python y Go.
En el panel de Isaak, sección Webhooks, pulsa "Añadir endpoint". Introduce tu URL HTTPS y selecciona los eventos que quieras recibir. Isaak te dará un signing secret (whsec_…) que necesitarás en el paso 3.
Tu endpoint recibe un POST application/json con el evento. Devuelve 2xx en menos de 5 segundos. Si tardas más o devuelves >= 400, Isaak reintenta con backoff exponencial (1m, 5m, 30m, 2h, 12h, 24h) hasta 6 veces.
{
"id": "evt_2026_05_28_abc123",
"type": "invoice.issued",
"createdAt": "2026-05-28T10:23:45.000Z",
"tenantId": "tnt_xxx",
"data": {
"invoiceId": "inv_2026_0042",
"number": "2026/0042",
"amount": 1210.00,
"verifactuHash": "a1b2c3d4..."
}
}Cada POST trae las cabeceras x-isaak-signature y x-isaak-timestamp. Calcula HMAC-SHA256 sobre "<timestamp>.<raw_body>" con tu signing secret y compara con timingSafeEqual. Si el timestamp tiene más de 5 minutos, rechaza el evento (anti-replay).
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.ISAAK_WEBHOOK_SECRET;
// IMPORTANTE: necesitas el cuerpo crudo (Buffer), NO el JSON parseado
app.post('/webhooks/isaak',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('x-isaak-signature');
const ts = req.header('x-isaak-timestamp');
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send('timestamp out of window');
}
const payload = `${ts}.${req.body.toString('utf8')}`;
const expected = crypto.createHmac('sha256', SECRET).update(payload).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
console.log('event', event.type, event.id);
res.status(200).send('ok');
}
);import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['ISAAK_WEBHOOK_SECRET'].encode()
@app.post('/webhooks/isaak')
def isaak_webhook():
sig = request.headers.get('X-Isaak-Signature', '')
ts = request.headers.get('X-Isaak-Timestamp', '0')
if abs(time.time() - float(ts)) > 300:
abort(400, 'timestamp out of window')
payload = f"{ts}.{request.get_data(as_text=True)}".encode()
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401, 'invalid signature')
event = request.get_json()
print('event', event['type'], event['id'])
return ('', 200)Isaak puede reintentar un evento ya entregado (ej. si tu servidor no respondió a tiempo). Usa el campo id del evento como clave de idempotencia: guárdalo en BBDD con UNIQUE constraint y descarta duplicados con 200 OK.
model ProcessedWebhook {
id String @id // evt_… del payload
type String
createdAt DateTime @default(now())
}
// En el handler:
try {
await prisma.processedWebhook.create({ data: { id: event.id, type: event.type } });
} catch (e) {
if (e.code === 'P2002') {
// Duplicate — ya procesado. Responde 200 igualmente.
return res.status(200).send('ok');
}
throw e;
}Genera tú mismo una firma válida y simula un evento contra tu propio endpoint local. Te ahorra esperar a que Isaak emita uno real.
SECRET="whsec_xxx"
TS=$(date +%s)
BODY='{"id":"evt_test","type":"invoice.issued","data":{}}'
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST http://localhost:3000/webhooks/isaak \
-H "Content-Type: application/json" \
-H "x-isaak-timestamp: $TS" \
-H "x-isaak-signature: $SIG" \
-d "$BODY"