⚠️ AVISO: Los webhooks están disponibles en los planes Avanzado y Total.
Cuando FacturaDirecta envía un evento a tu endpoint, incluye una firma digital en las cabeceras HTTP. Verificar esta firma te permite confirmar que la notificación proviene realmente de FacturaDirecta y que no ha sido manipulada por un tercero.
💡 Recomendación: Verifica siempre la firma antes de procesar un evento. Si la firma no coincide, descarta la petición.
Cómo funciona la firma
Cada petición webhook incluye dos cabeceras HTTP:
Cabecera | Formato | Descripción |
|
| Timestamp del envío y firma HMAC SHA256 |
|
| Timestamp del envío (en segundos) |
La firma se calcula sobre una combinación del timestamp y el body de la petición, usando tu signing secret como clave.
Verificación paso a paso
Para verificar la firma de un evento:
Extrae el timestamp y la firma de la cabecera
Webhook-Signature. El formato est=<timestamp>,v1=<firma>Construye el string a firmar concatenando el timestamp, un punto (
.) y el body completo de la petición:<timestamp>.<body>Calcula el HMAC SHA256 del string anterior usando tu signing secret como clave
Compara la firma calculada con la firma recibida usando una comparación de tiempo constante (timing-safe)
Verifica la ventana de tiempo: comprueba que el timestamp no tiene más de 5 minutos de antigüedad para protegerte contra ataques de repetición
Ejemplos de código
Node.js
const crypto = require('crypto');function verifyWebhookSignature(body, signatureHeader, secret) {
// 1. Extraer timestamp y firma
const parts = {};
signatureHeader.split(',').forEach(part => {
const [key, value] = part.split('=');
parts[key] = value;
});
const timestamp = parts['t'];
const signature = parts['v1']; // 2. Construir el string a firmar
const signedPayload = `${timestamp}.${body}`; // 3. Calcular HMAC SHA256
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex'); // 4. Comparar con timing-safe
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
); // 5. Verificar ventana de tiempo (5 minutos)
const now = Math.floor(Date.now() / 1000);
const isFresh = Math.abs(now - parseInt(timestamp)) < 300; return isValid && isFresh;
}
Python
import hmac
import hashlib
import timedef verify_webhook_signature(body, signature_header, secret):
# 1. Extraer timestamp y firma
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = parts['t']
signature = parts['v1'] # 2. Construir el string a firmar
signed_payload = f"{timestamp}.{body}" # 3. Calcular HMAC SHA256
expected = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest() # 4. Comparar con timing-safe
is_valid = hmac.compare_digest(signature, expected) # 5. Verificar ventana de tiempo (5 minutos)
is_fresh = abs(time.time() - int(timestamp)) < 300 return is_valid and is_fresh
PHP
function verifyWebhookSignature($body, $signatureHeader, $secret) {
// 1. Extraer timestamp y firma
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
list($key, $value) = explode('=', $part, 2);
$parts[$key] = $value;
}
$timestamp = $parts['t'];
$signature = $parts['v1']; // 2. Construir el string a firmar
$signedPayload = "{$timestamp}.{$body}"; // 3. Calcular HMAC SHA256
$expected = hash_hmac('sha256', $signedPayload, $secret); // 4. Comparar con timing-safe
$isValid = hash_equals($signature, $expected); // 5. Verificar ventana de tiempo (5 minutos)
$isFresh = abs(time() - intval($timestamp)) < 300; return $isValid && $isFresh;
}
Protección anti-replay
La verificación del timestamp (paso 5) protege contra ataques de repetición (replay attacks). Si alguien intercepta un evento válido e intenta reenviarlo más tarde, la ventana de 5 minutos hace que la firma se considere expirada.
Comprueba siempre que el timestamp del evento no difiere en más de 300 segundos (5 minutos) respecto a la hora actual de tu servidor.
El signing secret
El signing secret es la clave que se usa para calcular y verificar las firmas. Tiene el formato whsec_<valor_hexadecimal>.
El signing secret solo se muestra en dos momentos:
Al crear un nuevo endpoint
Al rotar el secret de un endpoint existente
En ambos casos, guárdalo en un lugar seguro (por ejemplo, en las variables de entorno de tu servidor). No podrás volver a consultarlo después.
Si pierdes el signing secret, puedes rotar el secret desde el detalle del endpoint en FacturaDirecta. Al hacerlo, se genera uno nuevo y el anterior deja de funcionar inmediatamente.
Buenas prácticas
Verifica siempre la firma: nunca proceses un evento sin antes comprobar que la firma es válida
Usa comparación timing-safe: evita comparar strings directamente (
===). Usa las funciones de comparación de tiempo constante de tu lenguaje (timingSafeEqual,compare_digest,hash_equals)Responde rápido con HTTP 200: tu servidor debe responder con un código 2xx lo antes posible. Si necesitas hacer procesamiento largo, acepta el evento primero y procésalo de forma asíncrona
Responde HTTP 410 para desuscribirte: si ya no quieres recibir eventos en un endpoint, responde con 410 Gone y FacturaDirecta lo desactivará automáticamente