Every webhook delivery is signed with HMAC-SHA256 using the secret you saved when the integration was created. Verifying the signature on every request is non-negotiable: without it, anyone who knows your URL can forge events.
What gets signed
QueueUp computes:
signature = hex( HMAC_SHA256( secret, "<timestamp>.<raw_body>" ) )
and sends it as X-QueueUp-Signature: v1=<hex>. The timestamp is the unix-seconds value sent in X-QueueUp-Timestamp. The body is the exact bytes of the request body. Verify against the raw bytes, not a re-serialized JSON value.
Why the timestamp
Including the timestamp in the signed string defeats replay attacks. If an attacker captures a signed request and re-sends it later, you can detect it by comparing X-QueueUp-Timestamp against the current time and rejecting anything older than your tolerance window (5 minutes is a sane default).
Node.js
import crypto from 'node:crypto';
function verifyWebhook(secret, headers, rawBody) {
const sig = headers['x-queueup-signature']; // "v1=<hex>"
const ts = headers['x-queueup-timestamp'];
// 5-minute tolerance against replay
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
// Constant-time compare
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
In Express, mount with express.raw({ type: 'application/json' }) so req.body is a Buffer you can pass as the raw body.
Python
import hashlib, hmac, time
def verify_webhook(secret: str, headers: dict, raw_body: bytes) -> bool:
sig = headers.get("x-queueup-signature", "")
ts = headers.get("x-queueup-timestamp", "")
if abs(time.time() - int(ts)) > 300:
return False
expected = "v1=" + hmac.new(
secret.encode(),
f"{ts}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(sig, expected)
Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
func Verify(secret string, headers map[string]string, body []byte) bool {
sig := headers["X-QueueUp-Signature"]
ts := headers["X-QueueUp-Timestamp"]
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil {
return false
}
if abs(time.Now().Unix()-tsInt) > 300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(ts))
mac.Write([]byte("."))
mac.Write(body)
expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
func abs(n int64) int64 { if n < 0 { return -n }; return n }
Idempotency
Always dedupe on the envelope’s id field (e.g. evt_42). Deliveries are at-least-once: a network blip while you’re sending the response can cause a retry. Storing the id and checking before processing is enough.