How do I verify that a webhook really came from Bead?
Every webhook request carries a cryptographic signature in the x-webhook-signature
header.
Verifying this signature ensures the payload was sent by Bead and was not altered in transit.
1 — Header format
x-webhook-signature: t=1752067200,s=9d6309b739a8e5b87e3c2b8d1d4fbc17e5e3c7f3c5b5a9d3e…
t
Unix-epoch timestamp (seconds) when Bead generated the event
s
HMAC-SHA256 hex digest of the raw request body, using the terminal’s signingSecret
2 — Verification steps
Parse the header – extract
t
ands
.Check timestamp drift – reject if the event is more than 5 minutes old (protects against replay).
Compute your own digest
expected = HMAC-SHA256(signingSecret, rawBody)
Constant-time compare
expected
to the receiveds
.Only if both checks pass, queue or process the payload.
3 — Code example (Node.js)
import crypto from "crypto";
import express from "express";
const app = express();
// Raw body needed for HMAC
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
const SIGNING_SECRET = process.env.BEAD_SIGNING_SECRET;
app.post("/bead/webhooks", (req, res) => {
const sigHeader = req.get("x-webhook-signature") || "";
const [tsPart, sigPart] = sigHeader.split(",").map(p => p.split("=")[1]);
const timestamp = Number(tsPart);
const signature = sigPart;
// ❶ Timestamp tolerance (5 min)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return res.status(400).send("Stale webhook");
}
// ❷ Compute expected signature
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
// ❸ Constant-time compare
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(400).send("Invalid signature");
}
// ✅ Verified — handle the event
const event = req.body;
// ...business logic...
res.sendStatus(200);
});
app.listen(3000);
4 — Common pitfalls
Parsing JSON before capturing the raw body
Use middleware that gives you the unparsed req.rawBody
.
Treating hex strings as UTF-8
Compare as byte buffers (timingSafeEqual
).
Ignoring timestamp
Attackers could replay old events. Always enforce a skew limit.
Wrong secret
Each terminal has its own signingSecret
. Use the one returned when you set the webhook URL.
5 — Testing your implementation
In sandbox, set the terminal webhook to your dev endpoint.
Trigger a test payment.
Log the received header, calculated digest, and comparison result.
Tamper with the payload or header to confirm your handler rejects invalid signatures.
Need help or sample code in another language? Email [email protected].
Last updated