Developer docs
Verify webhook signatures
Every webhook is signed with HMAC-SHA256 over the raw request body. Reject unsigned or mismatched payloads.
Algorithm
HTTP
HMAC-SHA256(secret, rawBody) === x-harrenapay-signature- Use the raw bytes — do not re-serialize JSON before verifying.
- Compare with a constant-time equality check.
- Respond 2xx quickly; do heavy work async after ack.
Node.js
Node
import crypto from "crypto";
export function verifyHarrenaSignature(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signatureHeader || "", "utf8");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express: use express.raw({ type: "application/json" }) for this route
app.post("/webhooks/harrenapay", (req, res) => {
const ok = verifyHarrenaSignature(
req.body, // Buffer
req.get("x-harrenapay-signature"),
process.env.HARRENAPAY_WEBHOOK_SECRET,
);
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// Flat payload: event.event, event.reference, event.amount, event.currency
res.status(200).send("ok");
});PHP
PHP
<?php
function verify_harrenapay_signature(string $rawBody, ?string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature ?? '');
}
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_HARRENAPAY_SIGNATURE'] ?? '';
if (!verify_harrenapay_signature($raw, $sig, getenv('HARRENAPAY_WEBHOOK_SECRET'))) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($raw, true);
// handle $event
http_response_code(200);
echo 'ok';