Docs/Payment-API
SPICEPAY DEVELOPER DOCS

Webhook verification

The X-Webhook-Signature-512 header contains a hexadecimal HMAC-SHA512 digest of the exact request bytes.

How it works

The X-Webhook-Signature-512 header contains a hexadecimal HMAC-SHA512 digest of the exact request bytes. Use the shop's webhook signing secret to compute the expected digest.

Implementation

Read the raw body before any JSON middleware changes it. Reject a missing or malformed signature and compare equal-length byte buffers with a constant-time comparison. Parse the JSON only after verification succeeds. The API secret and webhook signing secret serve different purposes; do not substitute one for the other. Never log the secret or complete personal payment payloads.

HTTP example

js
const crypto = require("node:crypto");
const express = require("express");
const app = express();
// Register this raw-body route before express.json() middleware.

app.post("/webhooks/spicepay", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("x-webhook-signature-512") ?? "";
  if (!/^[0-9a-f]{128}$/i.test(signature)) return res.sendStatus(401);
  const expected = crypto
    .createHmac("sha512", process.env.SPICEPAY_WEBHOOK_SECRET)
    .update(req.body) // Buffer with the raw body
    .digest();
  const received = Buffer.from(signature, "hex");

  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).send("Invalid signature");
  }

  let event;
  try { event = JSON.parse(req.body.toString("utf8")); }
  catch { return res.sendStatus(400); }
  // Persist or queue event before acknowledging it.
  // Deduplicate event_id and keep test events out of real fulfilment.
  res.sendStatus(200);
});

Request examples

js
const crypto = require("node:crypto");
const express = require("express");
const app = express();
// Register this raw-body route before express.json() middleware.

app.post("/webhooks/spicepay", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.get("x-webhook-signature-512") ?? "";
  if (!/^[0-9a-f]{128}$/i.test(signature)) return res.sendStatus(401);
  const expected = crypto
    .createHmac("sha512", process.env.SPICEPAY_WEBHOOK_SECRET)
    .update(req.body) // Buffer with the raw body
    .digest();
  const received = Buffer.from(signature, "hex");

  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).send("Invalid signature");
  }

  let event;
  try { event = JSON.parse(req.body.toString("utf8")); }
  catch { return res.sendStatus(400); }
  // Persist or queue event before acknowledging it.
  // Deduplicate event_id and keep test events out of real fulfilment.
  res.sendStatus(200);
});