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 OK within 5 seconds to prevent SellAuth retries.
  • Deduplicate by order_id — SellAuth may redeliver on timeout.