Firma de webhooks
Verifica webhooks de Feedjolt con HMAC-SHA256 sobre el cuerpo en bruto. Formato de cabecera, algoritmo de verificación, código para Node, Python, Ruby y Go, y errores.
Cada webhook incluye una cabecera de firma. Verifícala antes de confiar en el cuerpo. Sin verificación, cualquiera que conozca tu URL de endpoint puede falsificar eventos.
Formato de la cabecera
X-Feedjolt-Signature: sha256=<hex-hmac-sha256>
X-Feedjolt-Event: status.changed
X-Feedjolt-Timestamp: 2026-04-30T12:34:56.789012+00:00La firma es HMAC-SHA256(secret, canonical_body) como hex.
El cuerpo canónico es el payload JSON serializado con claves ordenadas y sin espacios:
json.dumps(payload, sort_keys=True, separators=(",", ":"))Esa es la secuencia exacta de bytes que firmamos en nuestro lado; debes reproducirla byte a byte para verificar. La cabecera X-Feedjolt-Timestamp es informativa - no forma parte de lo que firmamos.
Algoritmo de verificación
- Obtén el cuerpo crudo de la petición tal cual. No lo parsees y re-serialices.
- Lee
X-Feedjolt-Signature; quita el prefijosha256=. - Calcula
HMAC-SHA256(secret, raw_body)como hex. - Compara con igualdad de tiempo constante. Discrepancia -> rechaza con 401.
Crucial: firmamos los bytes crudos que vas a recibir. Si tu framework parsea JSON antes de que puedas leer el cuerpo, tendrás que capturar los bytes crudos por separado (ejemplos abajo).
Node.js / TypeScript
import { createHmac, timingSafeEqual } from "crypto";
const SECRET = process.env.FEEDJOLT_WEBHOOK_SECRET!;
export function verifyFeedjolt(headerValue: string | undefined, rawBody: string): boolean {
if (!headerValue) return false;
const [scheme, sig] = headerValue.split("=");
if (scheme !== "sha256" || !sig) return false;
const expected = createHmac("sha256", SECRET).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(sig);
return a.length === b.length && timingSafeEqual(a, b);
}Express necesita captura personalizada del cuerpo crudo (el express.json() por defecto descarta los bytes):
import express from "express";
const app = express();
app.use("/feedjolt-webhook", express.json({
verify: (req, _res, buf) => {
(req as any).rawBody = buf.toString("utf8");
}
}));
app.post("/feedjolt-webhook", (req, res) => {
const ok = verifyFeedjolt(
req.header("X-Feedjolt-Signature") ?? undefined,
(req as any).rawBody
);
if (!ok) return res.status(401).end();
const event = req.body;
// procesar evento
res.status(200).end();
});Next.js Route Handlers (App Router):
// app/api/feedjolt-webhook/route.ts
import { NextResponse } from "next/server";
import { verifyFeedjolt } from "@/lib/feedjolt";
export async function POST(req: Request) {
const rawBody = await req.text();
const ok = verifyFeedjolt(req.headers.get("x-feedjolt-signature") ?? undefined, rawBody);
if (!ok) return new NextResponse("invalid signature", { status: 401 });
const event = JSON.parse(rawBody);
// procesar evento
return new NextResponse("ok");
}Python
import hashlib
import hmac
import os
SECRET = os.environ["FEEDJOLT_WEBHOOK_SECRET"]
def verify_feedjolt(header_value: str | None, raw_body: bytes) -> bool:
if not header_value:
return False
scheme, _, sig = header_value.partition("=")
if scheme != "sha256" or not sig:
return False
expected = hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Flask:
@app.post("/feedjolt-webhook")
def webhook():
raw = request.get_data() # bytes, sin parsear
if not verify_feedjolt(request.headers.get("X-Feedjolt-Signature"), raw):
abort(401)
event = request.get_json()
return "", 200FastAPI:
from fastapi import FastAPI, Request, HTTPException
@app.post("/feedjolt-webhook")
async def webhook(request: Request):
raw = await request.body()
if not verify_feedjolt(request.headers.get("x-feedjolt-signature"), raw):
raise HTTPException(401)
event = await request.json()
return {"ok": True}Ruby (Rails)
require "openssl"
SECRET = ENV.fetch("FEEDJOLT_WEBHOOK_SECRET")
def verify_feedjolt(header_value, raw_body)
return false unless header_value
scheme, sig = header_value.split("=", 2)
return false unless scheme == "sha256" && sig
expected = OpenSSL::HMAC.hexdigest("sha256", SECRET, raw_body)
Rack::Utils.secure_compare(expected, sig)
endEn un controlador de Rails (request.raw_post obtiene los bytes sin parsear):
class FeedjoltWebhooksController < ActionController::API
skip_before_action :verify_authenticity_token, raise: false
def create
raw = request.raw_post
head :unauthorized and return unless verify_feedjolt(request.headers["X-Feedjolt-Signature"], raw)
event = JSON.parse(raw)
head :ok
end
endGo
package feedjolt
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
)
func Verify(secret, headerValue string, rawBody []byte) bool {
if !strings.HasPrefix(headerValue, "sha256=") {
return false
}
sig := strings.TrimPrefix(headerValue, "sha256=")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}Errores comunes
- Parsear el cuerpo antes de verificar. Los frameworks parsean JSON por ti y pierden espacios. Firmamos los bytes exactos - calcula sobre el cuerpo crudo.
- Comparar strings con
==. Usa una comparación de tiempo constante. Si no, un atacante remoto puede extraer bytes vía timing. - Confiar en el secreto equivocado. Si tienes varios endpoints, cada uno tiene su propio secreto. Usa el correcto para el endpoint receptor.
Protección contra replay
Actualmente no incluimos una marca de tiempo en los bytes firmados, así que si un atacante captura una entrega podría en principio replayarla indefinidamente. Mitigaciones a aplicar encima:
- Trata la propia URL del endpoint como un secreto. No la registres; rótala si se filtra.
- Deduplica por
event_iden el payload - un replay lleva el mismo ID de evento, así que lo saltarás tras el primer procesado.
Un esquema de timestamp firmado (estilo Stripe t=...,v1=...) está en la hoja de ruta. Vótalo si necesitas mejor protección contra replay hoy.
Replay manual
El panel en Ajustes -> Webhooks -> [endpoint] -> Entregas tiene un botón Replay en cada entrega. Reencola el mismo payload - útil cuando arreglaste un bug y quieres reprocesar los eventos de ayer.
Payloads de webhook
Los tipos de evento de los webhooks de Feedjolt y las cabeceras que enmarcan cada envío. Forma del payload, firma, idempotencia y estabilidad de campos.
Reintentos e idempotencia de webhooks
Cómo gestiona Feedjolt los fallos de entrega de webhooks hoy: un intento, registros, reenvío manual y cómo crear handlers idempotentes y seguros ante desorden.
