Feedjoltdocs
DesenvolupadorsWebhooks

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:00

La 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ó

  1. Obté el cos cru de la petició tal com és. No el parsegis i re-serialitzis.
  2. Llegeix X-Feedjolt-Signature; treu el prefix sha256=.
  3. Calcula HMAC-SHA256(secret, raw_body) com a hex.
  4. 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 "", 200

FastAPI:

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)
end

En 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
end

Go

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_id al 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.

On this page