Skip to content

Webhook signatures

Every webhook request includes an X-Nuez-Signature header. Verify it before processing the event.

nuez signs each webhook with HMAC-SHA256 using the secret you provided when configuring your webhook (PUT /v1/webhook — one endpoint and one secret per user, not per API key). The signature covers the raw request body exactly as sent, before JSON parsing.

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(r *http.Request, secret string) bool {
sig := r.Header.Get("X-Nuez-Signature")
body, _ := io.ReadAll(r.Body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(body: Buffer, signature: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(body)
.digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Unlike most providers, nuez doesn’t generate this secret for you — you choose it yourself when calling PUT /v1/webhook (minimum 16 characters). Store it securely; nuez only keeps it to sign outgoing requests, it isn’t retrievable via GET /v1/webhook. See Webhook management.

Verify the signature, and also check the timestamp field in the JSON body (see Webhook events for the envelope shape) — reject events with a timestamp more than a few minutes in the past to guard against replay of an intercepted request.