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.
Headers sent
Section titled “Headers sent”| Header | Content |
|---|---|
X-Afriflow-Timestamp |
Unix timestamp (seconds) of the delivery |
X-Afriflow-Signature |
t=<timestamp>,v1=<HMAC-SHA256 in hexadecimal> |
Algorithm
Section titled “Algorithm”-
Get the raw request body, exactly as received (not the JSON parsed and then re-serialized).
-
Concatenate
<timestamp>.<raw body>. -
Compute the HMAC-SHA256 of that string with your secret key
af_live_sec_.... -
Compare in constant time with the
v1value of the header. Reject if the timestamp is more than 5 minutes old (replay protection).
Examples
Section titled “Examples”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.jsonapp.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});$brut = file_get_contents("php://input");$ts = $_SERVER["HTTP_X_AFRIFLOW_TIMESTAMP"] ?? "";$sig = "";foreach (explode(",", $_SERVER["HTTP_X_AFRIFLOW_SIGNATURE"] ?? "") as $partie) { if (str_starts_with($partie, "v1=")) $sig = substr($partie, 3);}$attendu = hash_hmac("sha256", $ts . "." . $brut, getenv("CARTFLOX_SECRET_KEY"));if ($ts === "" || $sig === "" || abs(time() - (int) $ts) > 300 || !hash_equals($attendu, $sig)) { http_response_code(400); exit("invalid signature");}$evt = json_decode($brut, true);if ($evt["event"] === "payment.completed") { // Order paid: $evt["data"]["metadata"]["order_id"]}http_response_code(200);import hmac, hashlib, os, timefrom flask import Flask, request, abort
app = Flask(__name__)SECRET = os.environ["CARTFLOX_SECRET_KEY"].encode()
@app.post("/webhooks/cartflox")def webhook(): brut = request.get_data() # raw body, never request.json ts = request.headers.get("X-Afriflow-Timestamp", "") parties = dict(p.split("=", 1) for p in request.headers.get("X-Afriflow-Signature", "").split(",") if "=" in p) attendu = hmac.new(SECRET, f"{ts}.".encode() + brut, hashlib.sha256).hexdigest() if not ts or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(attendu, parties.get("v1", "")): abort(400) evt = request.get_json(force=True) if evt["event"] == "payment.completed": pass # order paid: evt["data"]["metadata"]["order_id"] return "", 200