Boostigo Developers

Verify Signature

Every webhook is signed with your API key's webhook secret (whsec_…, shown as "Callback Secret" in the Client Dashboard).

X-Boostigo-Timestamp: 1788794127
X-Boostigo-Signature: sha256=<hex>

#Algorithm

signed_payload = X-Boostigo-Timestamp + "." + RAW_REQUEST_BODY
signature      = HMAC_SHA256( webhook_secret, signed_payload )   // lowercase hex
header         = "sha256=" + signature

Rules:

  1. Use the raw body bytes exactly as received. Do not re-serialize JSON before verifying — key order or whitespace changes break the signature.
  2. Compare with a constant-time comparison (hash_equals, crypto.timingSafeEqual).
  3. Reject requests whose timestamp is older than 5 minutes (replay protection).
  4. Only after the signature is valid, parse the JSON and act on X-Boostigo-Delivery idempotently.

#PHP

<?php
$secret = getenv("BOOSTIGO_WEBHOOK_SECRET");          // whsec_...
$raw    = file_get_contents("php://input");           // RAW body — before json_decode
$ts     = $_SERVER["HTTP_X_BOOSTIGO_TIMESTAMP"] ?? "";
$sig    = $_SERVER["HTTP_X_BOOSTIGO_SIGNATURE"] ?? "";

if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit("stale timestamp"); }
$expected = "sha256=" . hash_hmac("sha256", $ts . "." . $raw, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit("invalid signature"); }

$event = json_decode($raw, true);
// ... handle $event["event"], $event["data"]["order_id"], $event["data"]["status"]
http_response_code(200);
echo json_encode(["received" => true]);

#Node.js (Express)

import crypto from "node:crypto";
import express from "express";

const app = express();
// keep the RAW body for signature verification
app.post("/api/boostigo/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const secret = process.env.BOOSTIGO_WEBHOOK_SECRET;
  const ts = req.header("X-Boostigo-Timestamp") || "";
  const sig = req.header("X-Boostigo-Signature") || "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(400).send("stale timestamp");
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "." + req.body.toString("utf8")).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(sig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).send("invalid signature");
  const event = JSON.parse(req.body.toString("utf8"));
  // ... handle event.event / event.data
  res.status(200).json({ received: true });
});

#Test vector

Secret whsec_test, timestamp 1700000000, body {"event":"order.success","data":{"order_id":"BST-LKE-TEST0000"}}

sha256=02a502afc8a1697e5d6b9259b3eea2d7e54fe8934c57bce5b4b4a5c0b5dbbbd5

Reproduce it with your implementation before going live (skip the timestamp-age check for this fixed vector). If your hex differs, check that you hashed exactly timestamp + "." + raw_body with no re-serialization.