QueueUp / docs

Verifying webhooks

Verify QueueUp webhook signatures in your language of choice.

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.