Cómo comprobar que un webhook viene de verdad de Safina

Safina puede firmar cada webhook que envía. Aprende a activar la firma, a comprobar la cabecera X-Safina-Signature en Node.js o Python y a verificar las llamadas en n8n, Zapier y Make.

Cómo comprobar que un webhook viene de verdad de Safina Guías
Karsten Kreh Karsten Kreh
Actualizado

Después de cada llamada, Safina envía el resumen a tu URL de webhook. Tu CRM, tu flujo de n8n o tu Zap crea con él un lead o una tarea. Pero una URL de webhook es solo una dirección. Quien la conozca puede enviarle un evento falso de «llamada terminada».

Una firma resuelve el problema. Safina firma cada solicitud con un secreto que solo conocen tú y Safina. Tu receptor comprueba la firma y sabe dos cosas: Safina envió la solicitud y nadie cambió el contenido por el camino. Además, una marca de tiempo en la firma impide que alguien vuelva a enviar una solicitud antigua.

¿Eres nuevo con los webhooks? Lee primero ¿Qué es un webhook?. La configuración en la app está en la guía Conectar Safina AI con tu CRM mediante Webhook.

Lo que necesitas

RequisitoDetalles
Plan de SafinaPro o superior
Un webhook en la app de SafinaCon secreto de firma (ver abajo)
Un receptorTu propio servidor, n8n, Zapier o Make

Creas y firmas los webhooks en la app de Safina. También puedes crearlos con la API REST de Safina (POST /webhooks). La API REST y las claves de API están en beta: el acceso anticipado está disponible bajo petición en info@safina.ai, y las claves de API necesitan el plan Pro o superior.

Paso 1: Activa la firma

La firma es opcional. Safina firma las solicitudes de un webhook solo cuando tiene un secreto de firma. Lo obtienes de tres formas:

  • Webhook nuevo en la app: En la app de Safina, abre la pestaña Safina AI, luego «Sincronización CRM», y añade un webhook. Activa Firmar los envíos antes de guardar.
  • Webhook existente en la app: Toca Crear secreto en el webhook. Los webhooks anteriores a esta función siguen sin firmar hasta que lo hagas.
  • API REST (beta): Envía "create_signing_secret": true con POST /webhooks. La respuesta contiene signing_secret.

El secreto empieza por whsec_. Safina lo muestra una sola vez. Cópialo y guárdalo en tu receptor, como una contraseña.

Un webhook sin secreto sigue funcionando y envía solicitudes sin firmar, como antes. También puedes usar la autenticación del webhook (auth_type): Safina envía entonces un token bearer o una clave de API en una cabecera con cada solicitud. Eso demuestra que el remitente conoce el token. Una firma demuestra además que el contenido no cambió y que la solicitud es nueva.

Paso 2: Entiende la cabecera

Cada solicitud firmada lleva esta cabecera:

X-Safina-Signature: t=1794569912,v1=5f2c…e91a
  • t es la hora del intento de entrega, en segundos Unix.
  • v1 es el HMAC-SHA256 del texto <t>.<body sin procesar>, con tu secreto de firma como clave, en hexadecimal.

Tu receptor comprueba tres cosas:

  1. Hora: Rechaza la solicitud si t está más de 300 segundos (5 minutos) antes o después de tu reloj.
  2. Firma: Calcula el HMAC sobre el body sin procesar, exactamente como llegó, antes de analizar el JSON. Si analizas el JSON y lo vuelves a escribir, pueden cambiar los espacios o el orden de las claves, y la firma ya no coincide.
  3. Comparación: Compara tu valor con cada valor v1 en tiempo constante. Basta con una coincidencia.

¿Por qué puede haber más de un v1? Cuando tocas Crear nuevo secreto, el secreto anterior sigue siendo válido durante 24 horas. En ese tiempo, la cabecera tiene un valor v1 por cada secreto válido. Así puedes actualizar tu receptor sin perder eventos.

Cada intento de entrega tiene una marca de tiempo y una firma nuevas. Un reintento envía el mismo event_id, así que omite un event_id que ya hayas procesado.

Paso 3: Comprueba la firma en tu código

Estos ejemplos son los mismos que en la referencia de la API (en inglés). Probamos los dos con firmas reales del código de firma de Safina.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a Buffer or a string)
const verifySafinaSignature = (header, rawBody, secret) => {
  const parts = header.split(",").map((part) => part.trim().split("="));
  const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const expected = Buffer.from(
    createHmac("sha256", secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest("hex")
  );
  return parts.some(([key, value]) => {
    const candidate = Buffer.from(value ?? "");
    return (
      key === "v1" &&
      candidate.length === expected.length &&
      timingSafeEqual(candidate, expected)
    );
  });
};

Con Express, lee el body de esta ruta como bytes sin procesar, por ejemplo con express.raw({ type: "application/json" }). Entonces req.body es un Buffer. Analiza el JSON solo después de la comprobación:

app.post("/safina-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Safina-Signature") ?? "";
  if (!verifySafinaSignature(header, req.body, process.env.SAFINA_SIGNING_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString("utf8"));
  // … process the event
  res.status(200).end();
});

Python

import hashlib
import hmac
import time


# raw_body: the request body exactly as received, as bytes
def verify_safina_signature(header: str, raw_body: bytes, secret: str) -> bool:
    parts = [part.strip().split("=", 1) for part in header.split(",")]
    values = [(part[0], part[1]) for part in parts if len(part) == 2]
    timestamp = next((value for key, value in values if key == "t"), "")
    if not (timestamp.isascii() and timestamp.isdigit()):
        return False
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return any(
        key == "v1" and hmac.compare_digest(value.encode(), expected.encode())
        for key, value in values
    )

En Flask, request.get_data() te da los bytes sin procesar. En FastAPI, usa await request.body().

Comprueba la firma en herramientas de automatización

n8n

n8n comprueba la firma con un nodo Code. Su JavaScript tiene el módulo crypto de Node.js: en n8n Cloud está disponible, y en un n8n autoalojado lo permites con la variable de entorno NODE_FUNCTION_ALLOW_BUILTIN=crypto (documentación de n8n).

  1. En el nodo Webhook, elige POST y activa la opción Raw Body. n8n guarda entonces el body como datos binarios. Busca el nombre del campo binario en la salida del nodo (normalmente data).
  2. Añade después un nodo Code en el modo «Run Once for All Items» con este código:
const crypto = require("crypto");

const secret = "whsec_…"; // your signing secret
const header = $input.first().json.headers["x-safina-signature"] ?? "";
const rawBody = await this.helpers.getBinaryDataBuffer(0, "data");

const parts = header.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
  crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex")
);
const valid =
  Number.isInteger(timestamp) &&
  Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
  parts.some(
    ([key, value]) =>
      key === "v1" &&
      value?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(value), expected)
  );

if (!valid) {
  throw new Error("The Safina signature is not valid");
}
return [{ json: JSON.parse(rawBody.toString("utf8")) }];

Si la firma es incorrecta, el flujo se detiene. Si es correcta, los nodos siguientes reciben el evento analizado. El nodo Crypto de n8n también puede calcular un HMAC (acción Hmac, tipo SHA256, codificación HEX), pero primero tienes que construir el texto <t>.<body sin procesar> y después comparar los valores en un nodo IF. El nodo Code lo hace todo en un paso.

Zapier

En Zapier, usa el trigger Webhooks by Zapier → Catch Raw Hook. A diferencia de Catch Hook, te da el body sin analizar y las cabeceras de la solicitud (ayuda de Zapier). Webhooks by Zapier necesita el plan Professional o superior.

  1. Añade un paso Code by Zapier (Run JavaScript). En Input Data, asigna rawBody al body sin procesar del trigger y signature a la cabecera X-Safina-Signature.
  2. Pega este código. La biblioteca estándar de Node.js, y con ella crypto, está disponible en Code by Zapier (ayuda de Zapier):
const crypto = require("crypto");

const secret = "whsec_…"; // your signing secret
const parts = inputData.signature.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
  crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(inputData.rawBody).digest("hex")
);
const valid =
  Number.isInteger(timestamp) &&
  Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
  parts.some(
    ([key, value]) =>
      key === "v1" &&
      value?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(value), expected)
  );

output = { valid };
  1. Añade un paso Filter: continúa solo si valid es true.

El secreto está en el código del paso, así que todos los que pueden editar el Zap lo ven. Catch Raw Hook no divide el JSON en campos. Para asignar campos sueltos en pasos posteriores, analiza el body en el mismo paso de código y devuelve los campos que necesitas.

Make

Make puede hacer la comprobación con funciones integradas. La función sha256(text; [encoding]; [key]) devuelve un HMAC cuando le das una clave (ayuda de Make). En el módulo Custom webhook, activa JSON pass-through (recibes el JSON original como texto) y, en la configuración avanzada, Get request headers (ayuda de Make). Después añade un filtro tras el webhook con dos condiciones:

  • t de la cabecera está como máximo a 300 segundos de formatDate(now; "X") (segundos Unix).
  • La cabecera X-Safina-Signature contiene v1= seguido de sha256(<t>.<body sin procesar>; hex; <tu secreto>).

Funciona, pero el filtro no compara en tiempo constante, y tienes que sacar t de la cabecera con split y get. Para la mayoría de los escenarios de Make, la autenticación del webhook es la opción más sencilla: Make puede proteger un custom webhook con una clave de API, que espera en la cabecera x-make-apikey (ayuda de Make). En Safina, elige la autenticación «Clave de API», la ubicación «Encabezado» y el nombre del encabezado x-make-apikey, e introduce la misma clave.

Errores frecuentes

  • La firma nunca coincide: Tu framework analizó el JSON antes de tu comprobación. Usa el body sin procesar.
  • La firma coincide, pero falla la comprobación de la hora: El reloj de tu servidor va mal. Sincronízalo con NTP.
  • Dejó de funcionar tras un secreto nuevo: El secreto anterior solo es válido 24 horas. Guarda el secreto nuevo en tu receptor.
  • El mismo evento llega dos veces: Es un reintento tras un timeout o un error. Omite un event_id que ya hayas procesado.

Resumen

Activa Firmar los envíos en cada webhook, guarda el secreto una vez y comprueba X-Safina-Signature antes de procesar un evento: hora, body sin procesar, comparación en tiempo constante. Así ninguna solicitud falsa llega a tu CRM. ¿Preguntas? Escríbenos a info@safina.ai.

9:41

Safina gestionó 51 llamadas esta semana

46

De confianza

4

Sospechoso

1

Peligroso

Últimos 7 días
Filter
EM
Emma Martin 67s 15:30

Quiere hablar sobre la oferta de la nueva campaña y tiene preguntas sobre el calendario.

LS
Laura Sánchez 54s 14:45

Pregunta por el estado del pedido y cuándo llegará la entrega.

TH
Tomás Herrera 34s 13:10

Concertar una reunión para la revisión del proyecto la próxima semana.

Desconocido 44s 11:30

Promesa de premio: probablemente spam.

SM
Sofía Martínez 10s 09:15

Reclamación sobre el último pedido, solicita que le devuelvan la llamada.

MN
Martín Navarro 95s 13 dic

Quiere hablar sobre una posible colaboración.

AR
Ana Rodríguez 85s 13 dic

Es su compañera y quiere hablar sobre el proyecto.

JC
Javier Campos 42s 12 dic

Pregunta por las citas disponibles la próxima semana.

LB
Lucía Bermejo 68s 12 dic

Tiene preguntas sobre la factura y pide aclaración.

Llamadas
Safina
Contactos
Perfil
9:41
Llamada de Emma Martin
12 dic
11:30
67s
+34612345678

Quiere hablar sobre la oferta de la nueva campaña y tiene preguntas sobre el calendario.

Puntos clave

  • Devolver llamada a Emma Martin
  • Aclarar dudas sobre plazos y precios
Devolver llamada
Editar contacto

Perspectivas IA

Estado de ánimo del llamante Muy bueno

La persona que llamó fue colaboradora y proporcionó la información necesaria.

Urgencia Baja

La persona puede esperar una respuesta.

Audio y transcripción

0:16

Hola, soy Safina AI, la asistente digital de Peter. ¿En qué puedo ayudarle?

Hola Safina, soy Emma Martin. Quería hablar sobre la oferta y el calendario.

Gracias, Emma. ¿Están valorando principalmente el paquete Standard o el Pro para el lanzamiento?

Exacto. Necesitamos el paquete Pro y nos gustaría empezar el próximo mes si la incorporación puede hacerse en la primera semana.

Diga adiós a su buzón de voz anticuado.

Pruebe Safina gratis y empiece a gestionar sus llamadas de forma inteligente.

Safina Support Bot
Safina Support Bot

¡Hola! 👋 Soy el asistente de soporte de Safina. Pregúntame lo que necesites sobre configuración, desvío de llamadas, planes o tu cuenta. Aviso: tus mensajes se tratan conforme a nuestra política de privacidad.

Tus mensajes se procesan según nuestra política de privacidad