Dashboard

Verificacion de Webhooks

Como verificar que los webhooks son autenticos y no han sido manipulados.

Por que verificar

Cualquiera que conozca la URL de tu webhook podria intentar enviar eventos falsos. Para evitarlo, cada webhook incluye una firma criptografica que debes verificar.

Siempre verifica las firmas

Nunca proceses un webhook sin verificar su firma, especialmente si realiza acciones importantes como crear pedidos o modificar datos.

Como funciona

  1. Al crear un webhook, se genera un secret unico
  2. Cuando enviamos un webhook, calculamos un hash HMAC-SHA256 del payload usando tu secret
  3. Incluimos este hash en el header X-SalonBookIt-Signature
  4. Tu servidor calcula el mismo hash y lo compara

Formato de la firma

X-SalonBookIt-Signature: sha256=abc123def456...

La firma se compone de:

  • sha256= - Prefijo que indica el algoritmo
  • Seguido del hash HMAC-SHA256 en hexadecimal

Obtener tu secret

  1. Ve a Configuracion → Integraciones
  2. En la seccion Webhooks, haz clic en el webhook que quieres verificar
  3. Copia el valor del campo Secret
Guarda el secret de forma segura

Usa variables de entorno o un gestor de secretos. Nunca lo incluyas en el codigo fuente.

Verificar la firma

Node.js / JavaScript

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
    // Extraer el hash de la firma
    const sigHash = signature.replace('sha256=', '');

    // Calcular el hash esperado
    const expectedHash = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');

    // Comparar de forma segura (evita timing attacks)
    return crypto.timingSafeEqual(
        Buffer.from(sigHash, 'hex'),
        Buffer.from(expectedHash, 'hex')
    );
}

// En tu endpoint
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-salonbookit-signature'];
    const payload = req.body.toString();

    if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
        console.error('Firma invalida');
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    console.log('Evento valido:', event.type);

    // Procesar el evento...

    res.json({ received: true });
});

Python

Python
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verifica la firma HMAC del webhook."""
    if not signature.startswith('sha256='):
        return False

    sig_hash = signature[7:]  # Quitar 'sha256='

    expected_hash = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Comparacion segura contra timing attacks
    return hmac.compare_digest(sig_hash, expected_hash)


@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-SalonBookIt-Signature', '')
    payload = request.get_data()

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        abort(401, 'Invalid signature')

    event = request.get_json()
    print(f"Evento valido: {event['type']}")

    # Procesar el evento...

    return {'received': True}

PHP

PHP
<?php

$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALONBOOKIT_SIGNATURE'] ?? '';

function verifySignature(string $payload, string $signature, string $secret): bool
{
    if (strpos($signature, 'sha256=') !== 0) {
        return false;
    }

    $sigHash = substr($signature, 7);
    $expectedHash = hash_hmac('sha256', $payload, $secret);

    return hash_equals($expectedHash, $sigHash);
}

if (!verifySignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($payload, true);
error_log('Evento valido: ' . $event['type']);

// Procesar el evento...

http_response_code(200);
echo json_encode(['received' => true]);

Ruby

Ruby
require 'openssl'
require 'sinatra'

WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

def verify_signature(payload, signature, secret)
  return false unless signature.start_with?('sha256=')

  sig_hash = signature[7..]
  expected_hash = OpenSSL::HMAC.hexdigest('sha256', secret, payload)

  Rack::Utils.secure_compare(expected_hash, sig_hash)
end

post '/webhook' do
  payload = request.body.read
  signature = request.env['HTTP_X_SALONBOOKIT_SIGNATURE'] || ''

  unless verify_signature(payload, signature, WEBHOOK_SECRET)
    halt 401, 'Invalid signature'
  end

  event = JSON.parse(payload)
  puts "Evento valido: #{event['type']}"

  # Procesar el evento...

  content_type :json
  { received: true }.to_json
end

Verificar timestamp (opcional)

Para mayor seguridad, puedes verificar que el webhook no sea muy antiguo usando el header X-SalonBookIt-Timestamp:

JavaScript
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutos

if (Math.abs(now - timestamp) > tolerance) {
    console.error('Webhook demasiado antiguo');
    return res.status(401).send('Timestamp expired');
}

Esto previene ataques de replay donde alguien intenta reenviar un webhook capturado anteriormente.

Errores comunes

La firma no coincide

  • Verifica que estas usando el secret correcto
  • Asegurate de usar el payload raw (sin parsear)
  • No modifiques el payload antes de verificar

El payload esta modificado

Si tu framework parsea automaticamente el JSON, obtén el body raw antes. En Express:

app.use(express.raw({ type: 'application/json' }))

Problemas de encoding

Asegurate de que el payload se trata como UTF-8 al calcular el hash.