API de FlavosoWebhooks y eventosComprobar las firmas
Comprobar las firmas
Cada entrega lleva Flavoso-Signature, un HMAC-SHA256 sobre la marca de tiempo y el cuerpo sin procesar. Código de comprobación para Node.js, Python, PHP, Go y C#.
El método
Cada entrega lleva la cabecera Flavoso-Signature: t=1759579200,v1=5257a8…. En ella, t es la hora en segundos desde 1970 y v1 un HMAC-SHA256, en hexadecimal y en minúsculas:
- Clave: tu secreto de firma, la cadena completa, incluido
whsec_, en UTF-8. - Mensaje:
t, un punto y el cuerpo sin procesar. Es decir,1759579200.{"id":"evt_…",…}.
Comprueba la firma antes de fiarte del contenido. Así sabes que la entrega viene de Flavoso y que no se ha modificado por el camino. Es el mismo método que usa Stripe: si ya lo has implementado allí, ya lo conoces.
Paso a paso
- 1
Tomar el cuerpo sin procesar
Exactamente los bytes que han llegado, antes de que nada los interprete como JSON.
- 2
Descomponer la cabecera
Sepárala por las comas y después por el primer
=.taparece una vez;v1, una o dos. - 3
Comprobar la antigüedad
Si
tse desvía más de 300 segundos de tu reloj, rechaza la entrega. Si no, una entrega interceptada podría volver a enviarse más tarde. - 4
Calcular el valor esperado
HMAC-SHA256 con el secreto sobre
t+.+ cuerpo, en hexadecimal. - 5
Comparar
Compara el resultado con cada
v1en tiempo constante. Si coincide uno, la entrega es auténtica.
Código de comprobación
// flavoso-signature.js
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the body exactly as received. Never JSON.stringify(req.body) again.
export function verifyFlavosoSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = header.split(",").map((part) => part.trim().split("="));
const t = Number(parts.find(([key]) => key === "t")?.[1]);
const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
if (!Number.isInteger(t) || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest("hex");
return signatures.some(
(s) => s.length === expected.length && timingSafeEqual(Buffer.from(s), Buffer.from(expected)),
);
}Obtener el cuerpo sin procesar
La mayoría de los frameworks interpretan el JSON automáticamente. Si después vuelves a convertir el cuerpo en texto, obtienes otros caracteres (espacios, orden, caracteres especiales) y una firma incorrecta. Así obtienes los bytes:
| Entorno | Cuerpo sin procesar |
|---|---|
| Express | express.raw({ type: "application/json" }) en esta ruta |
| Next.js (App Router) | await request.text() |
| Flask | request.get_data() |
| FastAPI | await request.body() |
| Django | request.body |
| PHP | file_get_contents('php://input') |
| Laravel | $request->getContent() |
| Go | io.ReadAll(r.Body) |
| ASP.NET Core | StreamReader sobre Request.Body, antes de cualquier model binding |
// app/api/flavoso/webhook/route.js (App Router)
import { verifyFlavosoSignature } from "@/lib/flavoso-signature";
export async function POST(request) {
const raw = await request.text(); // exactly as received, before any JSON parsing
const header = request.headers.get("flavoso-signature") ?? "";
if (!verifyFlavosoSignature(raw, header, process.env.FLAVOSO_WEBHOOK_SECRET)) {
return new Response("invalid signature", { status: 400 });
}
await enqueue(JSON.parse(raw)); // answer fast, work in the background
return new Response(null, { status: 200 });
}Valor de prueba
Con él puedes probar tu comprobación sin Flavoso: con este secreto, este cuerpo y t como «ahora», debe aceptar la firma, y con un carácter cambiado en el cuerpo, rechazarla.
# Your verification must accept this example with t = 1759579200 as "now".
# openssl prints the same v1 (after "SHA2-256(stdin)="):
printf '%s' '1759579200.{"id":"evt_test","type":"ping"}' \
| openssl dgst -sha256 -hmac 'whsec_test_secret'
# secret: whsec_test_secret
# body: {"id":"evt_test","type":"ping"}
# header: Flavoso-Signature: t=1759579200,v1=d5a5a8b08d9a25070a9e22c9fa06ad59b4082eb039b25cee3d83319e00d8201aCambiar el secreto
Tras «Crear un nuevo secreto de firma», Flavoso firma durante 24 horas con el secreto nuevo y el anterior a la vez, el nuevo primero: la cabecera lleva entonces dos v1. Basta con que coincida uno. Configura el secreto nuevo durante ese tiempo; después solo es válido el nuevo.
Errores frecuentes
- El cuerpo se leyó como JSON y se volvió a convertir en texto.
- El secreto equivocado: el webhook de tu app y los webhooks propios de un negocio tienen secretos distintos.
- El reloj del servidor va mal. Ajústalo con NTP.
- Se comparó el hexadecimal en mayúsculas, o no se quitaron los espacios alrededor de las partes de la cabecera.
- Se tomó
Flavoso-Event-Idcomo prueba. El identificador no es un secreto; solo la firma demuestra algo.
¿Algo no ha quedado claro? Escribe a support@flavoso.com e indica el identificador de X-Request-Id. Así podremos encontrar tu solicitud en el registro.