Firma de webhooks
Verifica webhooks de Feedjolt amb HMAC-SHA256 sobre el cos en brut. Format de capçalera, algorisme de verificació, codi per a Node, Python, Ruby i Go, i errors.
Cada webhook inclou una capçalera de firma. Verifica-la abans de confiar en el cos. Sense verificació, qualsevol que conegui la teva URL d'endpoint pot falsificar esdeveniments.
Format de la capçalera
X-Feedjolt-Signature: sha256=<hex-hmac-sha256>
X-Feedjolt-Event: status.changed
X-Feedjolt-Timestamp: 2026-04-30T12:34:56.789012+00:00La firma és HMAC-SHA256(secret, canonical_body) com a hex.
El cos canònic és el payload JSON serialitzat amb claus ordenades i sense espais:
json.dumps(payload, sort_keys=True, separators=(",", ":"))Aquesta és la seqüència exacta de bytes que firmem al nostre costat; has de reproduir-la byte a byte per verificar. La capçalera X-Feedjolt-Timestamp és informativa - no forma part del que firmem.
Algorisme de verificació
- Obté el cos cru de la petició tal com és. No el parsegis i re-serialitzis.
- Llegeix
X-Feedjolt-Signature; treu el prefixsha256=. - Calcula
HMAC-SHA256(secret, raw_body)com a hex. - Compara amb igualtat de temps constant. Discrepància -> rebutja amb 401.
Crucial: firmem els bytes crus que rebràs. Si el teu framework parseja JSON abans que puguis llegir el cos, hauràs de capturar els bytes crus per separat (exemples a baix).
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 necessita captura personalitzada del cos cru (l'express.json() per defecte descarta els 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;
// processar esdeveniment
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);
// processar esdeveniment
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, sense parsejar
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 obté els bytes sense parsejar):
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))
}Errors comuns
- Parsejar el cos abans de verificar. Els frameworks parsegen JSON per tu i perden espais. Firmem els bytes exactes - calcula sobre el cos cru.
- Comparar strings amb
==. Fes servir una comparació de temps constant. Si no, un atacant remot pot extreure bytes via timing. - Confiar en el secret equivocat. Si tens diversos endpoints, cadascun té el seu propi secret. Fes servir el correcte per a l'endpoint receptor.
Protecció contra replay
Actualment no incloem una marca de temps als bytes signats, així que si un atacant captura un lliurament podria en principi replayar-lo indefinidament. Mitigacions per aplicar a sobre:
- Tracta la pròpia URL de l'endpoint com un secret. No la registris; rota-la si es filtra.
- Deduplica per
event_idal payload - un replay porta el mateix ID d'esdeveniment, així que el saltaràs després del primer processat.
Un esquema de timestamp signat (estil Stripe t=...,v1=...) està al full de ruta. Vota'l si necessites millor protecció contra replay avui.
Replay manual
El panell a Configuració -> Webhooks -> [endpoint] -> Lliuraments té un botó Replay a cada lliurament. Reencua el mateix payload - útil quan vas arreglar un bug i vols reprocessar els esdeveniments d'ahir.
Payloads de webhook
Els tipus d'esdeveniment dels webhooks de Feedjolt i les capçaleres que emmarquen cada enviament. Forma del payload, signatura, idempotència i estabilitat.
Reintents i idempotència de webhooks
Com gestiona Feedjolt els errors d'entrega de webhooks avui: un intent, registres, reenviament manual i com crear handlers idempotents i segurs davant desordre.
