Webhook HMAC Debugger & Zod Handler
Diagnose 401 Invalid Signature webhook errors for Shopify, Stripe, and GitHub locally. Generates raw-body verification handlers and Zod v4/v3 payload schemas.
Example Output (Webhook HMAC Debugger)
{
"provider": "shopify",
"secret": "shpss_dummy_webhook_secret_key",
"signature": "mK8mE0wY2/q61V7Q6O3pX0B+Z+M8k0W2U9x8n6G7y8M=",
"timestamp": "1714500000",
"payload": {
"id": 820982911946154500,
"email": "customer@example.com",
"total_price": "149.99",
"currency": "USD",
"financial_status": "paid"
}
}// ============================================================================
// 🔍 WEBHOOK HMAC-SHA256 DIAGNOSTIC REPORT (100% Client-Side Verification)
// ============================================================================
// Provider Mode : SHOPIFY
// Expected Shopify Hmac: 4hF7vA92c2mE45rL8tQ3k0jN5pX7vB1wZ9yM6cK4dQo=
// Expected GitHub Hmac : sha256=e2117bBc0f76736984e39acbe834934a6a7f97eff075c19f72cf9bc2b874a38d
// Expected Stripe Hmac : t=1714500000,v1=9f4a8b21c03d44e18a9b77c25d66e32f11a08b99c44d22e17a66b33c88d11e22
// ============================================================================
import { z } from "zod";
export const webhookPayloadSchema = z.object({
id: z.number().int(),
email: z.email(),
total_price: z.string(),
currency: z.string(),
financial_status: z.string(),
});
export type WebhookPayload = z.infer<typeof webhookPayloadSchema>;
// Official Docs: https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-5-verify-the-webhook
import crypto from "node:crypto";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
// CRITICAL: Read raw text via req.text() — NEVER call req.json() before HMAC check!
const rawBody = await req.text();
const hmacHeader = req.headers.get("x-shopify-hmac-sha256") ?? "";
const secret = process.env.SHOPIFY_WEBHOOK_SECRET ?? "";
const computedBase64 = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("base64");
const a = Buffer.from(computedBase64, "utf8");
const b = Buffer.from(hmacHeader, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return NextResponse.json({ error: "Invalid Shopify HMAC signature" }, { status: 401 });
}
const parsed = webhookPayloadSchema.safeParse(JSON.parse(rawBody));
if (!parsed.success) {
return NextResponse.json({ error: "Invalid webhook payload", issues: parsed.error.issues }, { status: 400 });
}
return NextResponse.json({ received: true, data: parsed.data });
}
Understanding the Webhook HMAC Debugger Transformation
Webhook signature verification (`401 Invalid Signature`) is one of the most frustrating integration bugs in e-commerce and SaaS engineering. Providers like Shopify (`X-Shopify-Hmac-Sha256`), Stripe (`Stripe-Signature`), and GitHub (`X-Hub-Signature-256`) sign the exact raw byte stream of the HTTP request body using `HMAC-SHA256`. If your framework parses the JSON body first and re-serializes it via `JSON.stringify(req.body)`, even a single stripped space, newline, or floating-point formatting change alters the SHA-256 digest completely. This client-side debugger tests 8 common payload and encoding mutations locally in your browser and generates a timing-safe verification handler paired with a Zod payload schema.
Key Architectural Benefits
- •Tests 8 common webhook payload mutations simultaneously (exact raw body, minified JSON, 2-space pretty JSON, trailing newline `\n`, Base64 vs Hex, and Stripe's `${timestamp}.${rawBody}` prefix).
- •Generates production-ready Next.js App Router (`await req.text()`) and Express (`express.raw()`) handlers that preserve raw bytes before parsing JSON.
- •Uses `crypto.timingSafeEqual` with byte-length pre-checks so your webhook endpoint is protected against timing side-channel attacks and `ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH` crashes.
- •Infers a complete Zod v4 or v3 validation schema from the sample webhook payload in the same pass.
Common Conversion Pitfalls & Fixes
Verified Edge Cases & Complex Scenarios
Real payloads often contain edge cases that break naive generators. Here is how our engine handles them:
Hashing only the raw JSON body for a Stripe webhook always fails verification because Stripe signs `${timestamp}.${rawBody}`.
{
"provider": "stripe",
"secret": "whsec_test_secret",
"timestamp": "1714500000",
"payload": {"id": "evt_123", "type": "checkout.session.completed"}
}const computedHex = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`, "utf8")
.digest("hex");GitHub prepends `sha256=` to a 64-character hexadecimal digest, whereas Shopify sends a 44-character Base64 string without a prefix.
{
"provider": "github",
"secret": "gh_webhook_secret",
"signature": "sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17",
"payload": {"action": "opened"}
}const sigHeader = (req.headers.get("x-hub-signature-256") ?? "").replace(/^sha256=/, "");
const computedHex = crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");Frequently Asked Questions
Why does my webhook signature pass in Postman but fail in production?
Is my webhook signing secret (`whsec_...` or `shpss_...`) safe in this tool?
Which secret key does Shopify use to sign webhooks?
Should I use manual HMAC verification or stripe.webhooks.constructEvent in production?
Related Developer Converters
Browse AllJSON to Zod Schema
Convert raw JSON into strict, production-ready Zod schemas. Supports ISO datetime offsets, email, URLs, nullish options, and inferred TypeScript types in browser.
.env to Zod & T3 Env
Convert .env files into Zod v4, Zod v3, or @t3-oss/env-nextjs schemas in your browser. Infers types without exposing secret values and generates .env.example.
cURL to Fetch
Convert Chrome Copy as cURL commands into Browser Fetch, Node.js Fetch, or Axios code. Handles -H, -d, --data-raw, -u UTF-8 Basic Auth, -F FormData, -b, -L, and -k.