Webhooks spec
SellAuth webhook payload structure, signature verification, and HMAC-SHA256 examples in Python and TypeScript.
Payload shape
SellAuth sends a JSON body with the following top-level fields. The custom_fields object contains any extra data you configured in your SellAuth product settings.
{
"event": "purchase.completed",
"timestamp": "2026-01-15T14:32:10Z",
"order_id": "ord_9a7b3c1d",
"product_id": "prod_nimbus_lifetime",
"customer_email": "buyer@example.com",
"amount": 29.99,
"currency": "USD",
"custom_fields": {
"hwid": "abc123-def456",
"license_key": "NIMBUS-XXXX-XXXX-XXXX"
}
}Signature header
Every webhook request includes an X-SellAuth-Signature header. The value is a hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook secret.
X-SellAuth-Signature: a1b2c3d4e5f6...64hexchars
Verify HMAC-SHA256
Compare the header value against your own HMAC of the raw body. Use a constant-time comparison to prevent timing attacks.
Python
import hmac
import hashlib
WEBHOOK_SECRET = b"your-secret-here"
def verify_signature(raw_body: bytes, header_sig: str) -> bool:
computed = hmac.new(
WEBHOOK_SECRET, raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, header_sig)
# Usage in Flask / FastAPI endpoint:
# body = await request.body()
# sig = request.headers.get("X-SellAuth-Signature", "")
# if not verify_signature(body, sig):
# raise HTTPException(status_code=401)TypeScript (Edge / Node)
import { createHmac, timingSafeEqual } from "crypto";
const WEBHOOK_SECRET = "your-secret-here";
function verifySignature(
rawBody: string,
headerSig: string
): boolean {
const computed = createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const a = Buffer.from(computed);
const b = Buffer.from(headerSig);
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
// In Next.js API route or Route Handler:
// const sig = request.headers.get("x-sellAuth-signature") ?? "";
// const body = await request.text();
// if (!verifySignature(body, sig)) {
// return new Response("Unauthorized", { status: 401 });
// }Best practices
- •Always read the raw body — never parse JSON before verifying the signature.
- •Use constant-time comparison (
hmac.compare_digest/timingSafeEqual). - •Store the webhook secret in an environment variable, never in source.
- •Respond with
200 OKwithin 5 seconds to prevent SellAuth retries. - •Deduplicate by
order_id— SellAuth may redeliver on timeout.