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';