Verifying Signatures
Every webhook delivery is signed with HMAC-SHA256 using your endpoint’s secret. Always verify the signature before processing the payload.
Signature Header
The signature is sent in the X-XQR-Signature header:
X-XQR-Signature: sha256=5d2a7b8c4e...Verification Algorithm
- Read the raw request body as bytes (do not parse JSON first)
- Compute
HMAC-SHA256(secret, raw_body)using your webhook secret - Compare the hex digest with the value after
sha256=in the header - Use constant-time comparison to prevent timing attacks
Code Examples
import crypto from "node:crypto";
function verifySignature(secret, payload, signature) { const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex");
const sig = signature.replace("sha256=", "");
return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(sig, "hex") );}
// Express middlewareapp.post("/webhooks/xqr", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["x-xqr-signature"];
if (!verifySignature(WEBHOOK_SECRET, req.body, signature)) { return res.status(401).send("Invalid signature"); }
const event = JSON.parse(req.body); console.log(`Received ${event.event}`, event.data); res.status(200).send("OK");});import hmacimport hashlib
def verify_signature(secret: str, payload: bytes, signature: str) -> bool: expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() received = signature.removeprefix("sha256=") return hmac.compare_digest(expected, received)
# FastAPI examplefrom fastapi import Request, HTTPException
@app.post("/webhooks/xqr")async def handle_webhook(request: Request): body = await request.body() signature = request.headers.get("x-xqr-signature", "")
if not verify_signature(WEBHOOK_SECRET, body, signature): raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json() print(f"Received {event['event']}", event["data"]) return {"status": "ok"}func verifySignature(secret string, payload []byte, signature string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expected := hex.EncodeToString(mac.Sum(nil)) received := strings.TrimPrefix(signature, "sha256=") return hmac.Equal([]byte(expected), []byte(received))}Retrieving Your Secret
Your webhook signing secret is generated when you create the endpoint. Retrieve it via:
- Dashboard: Developers → Webhooks → click the endpoint → Show Secret
- API:
GET /api/developer/webhooks/{id}/secret(JWT-authenticated)
Was this page helpful?
Thanks for your feedback!