Skip to content

Verify the signature

Every webhook is signed with your secret key. Verify the signature before processing anything: otherwise, anyone who knows your URL could make you believe a payment happened.

Header Content
X-Afriflow-Timestamp Unix timestamp (seconds) of the delivery
X-Afriflow-Signature t=<timestamp>,v1=<HMAC-SHA256 in hexadecimal>
  1. Get the raw request body, exactly as received (not the JSON parsed and then re-serialized).

  2. Concatenate <timestamp>.<raw body>.

  3. Compute the HMAC-SHA256 of that string with your secret key af_live_sec_....

  4. Compare in constant time with the v1 value of the header. Reject if the timestamp is more than 5 minutes old (replay protection).

Express
import crypto from "node:crypto";
import express from "express";
const app = express();
function verifierSignature(corpsBrut, entetes, secret) {
const ts = entetes["x-afriflow-timestamp"];
const sig = (entetes["x-afriflow-signature"] || "")
.split(",").find((p) => p.startsWith("v1="))?.slice(3);
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes
const attendu = crypto.createHmac("sha256", secret).update(`${ts}.${corpsBrut}`).digest("hex");
return attendu.length === sig.length
&& crypto.timingSafeEqual(Buffer.from(attendu), Buffer.from(sig));
}
// The body must stay RAW: express.raw, not express.json
app.post("/webhooks/cartflox", express.raw({ type: "application/json" }), (req, res) => {
const brut = req.body.toString("utf8");
if (!verifierSignature(brut, req.headers, process.env.CARTFLOX_SECRET_KEY)) {
return res.status(400).send("invalid signature");
}
const evt = JSON.parse(brut);
if (evt.event === "payment.completed") {
// Mark order evt.data.metadata.order_id as paid.
// Make this processing idempotent: the same event can arrive twice.
}
res.sendStatus(200); // respond quickly, process the rest in the background
});