Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.xquik.com/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook delivery is signed with HMAC-SHA256 over a string built from a timestamp, nonce, and the raw body. Always verify the signature AND reject stale or replayed requests before processing events.

Headers sent with every delivery

Read these headers from each webhook POST before parsing the JSON body. The signature is the trust boundary; use Content-Type and User-Agent only for handler routing and logs.

Timestamp

X-Xquik-Timestamp is Unix epoch milliseconds. Reject requests outside the 5-minute tolerance window.

Nonce

X-Xquik-Nonce is 16 random bytes in hex. Store recent values for 5 minutes and reject repeats.

Signature

X-Xquik-Signature is sha256=<hex> over <timestamp>.<nonce>.<rawBody> keyed with the endpoint secret.

Raw JSON body

Content-Type is always application/json. Verify the raw body bytes before parsing or re-serializing JSON.

Sender logs

User-Agent is xquik-webhooks/1.0 (+https://xquik.com). Log it for diagnostics, but never trust it instead of the signature.

How it works

  1. Xquik computes the signing string <timestamp>.<nonce>.<rawBody>.
  2. Xquik computes sha256= + HMAC-SHA256(webhook secret, signing string).
  3. Your server recomputes the signature with the same secret over the raw body.
  4. Reject the request if the signature does not match in constant time.
  5. Reject the request if the timestamp is older than 5 minutes (clock-skew tolerant).
  6. Reject the request if the nonce was already seen in the last 5 minutes (replay protection).

Implementation

# Generate a test signature to verify your implementation
TS=$(date +%s)000
NONCE=$(openssl rand -hex 16)
BODY='{"test":"payload"}'
SIG=$(printf "%s.%s.%s" "$TS" "$NONCE" "$BODY" | openssl dgst -sha256 -hmac "your_webhook_secret" | sed 's/.*= /sha256=/')
echo "X-Xquik-Timestamp: $TS"
echo "X-Xquik-Nonce: $NONCE"
echo "X-Xquik-Signature: $SIG"

Security checklist

Never process webhook payloads without verifying the signature first. An unverified payload could be a spoofed request.
Use timingSafeEqual (Node.js), hmac.compare_digest (Python), or hmac.Equal (Go). String equality (===) is vulnerable to timing attacks.
Compute the HMAC over the raw request body bytes, not a re-serialized JSON object. Re-serialization can alter whitespace or key ordering.
We recommend responding within 10 seconds. Process events asynchronously if your handler is slow.

Idempotency

Webhook deliveries can be retried on failure, so your endpoint may receive the same event multiple times. Use deliveryId as the webhook delivery idempotency key. Use streamEventId when your system should process one monitor event only once across webhook retries or endpoint changes. Do not hash the raw request body when deliveryId is available.
const processedDeliveries = new Set();
const processedEvents = new Set();

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const payload = req.body.toString();

  if (!verifyWebhook(req, WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(payload);

  if (processedDeliveries.has(event.deliveryId)) {
    return res.status(200).send("Delivery already processed");
  }

  processedDeliveries.add(event.deliveryId);

  if (processedEvents.has(event.streamEventId)) {
    return res.status(200).send("Event already processed");
  }

  processedEvents.add(event.streamEventId);
  handleEvent(event);

  res.status(200).send("OK");
});
The in-memory examples above work for single-process servers. In production, use a persistent store (database, Redis) to track processed delivery IDs and event IDs across restarts and multiple instances.
Last modified on May 10, 2026