How to Securely Verify Shopify Webhooks in a Next.js App
April 4, 2026How to Securely Verify Shopify Webhooks in a Next.js App
If you are building a custom Shopify app or a headless storefront using Next.js in 2026, you are almost certainly using webhooks. Whether it’s to update your local database when an order is paid or to trigger a WhatsApp message when a fulfillment is created, webhooks are essential "tech plumbing."
However, there is a major security flaw I see in many junior-level implementations: failing to verify the webhook signature. Without verification, anyone who knows your endpoint URL can send fake data to your server, potentially creating fake orders or corrupting your data.
1. The Anatomy of a Shopify Webhook
Every webhook sent by Shopify includes a
X-Shopify-Hmac-Sha2562. The Next.js Challenge: Raw Body Access
In Next.js (especially the App Router), accessing the "raw" request body can be tricky because the framework often parses it as JSON automatically. To verify an HMAC, you must have the exact, un-parsed raw string.
Here is the high-level pattern for a secure Route Handler:
import { NextRequest, NextResponse } from 'next/server'; import crypto from 'crypto'; export async function POST(req: NextRequest) { const hmac = req.headers.get('x-shopify-hmac-sha256'); const topic = req.headers.get('x-shopify-topic'); const shop = req.headers.get('x-shopify-shop-domain'); // 1. Get the raw body as a string const rawBody = await req.text(); // 2. Recreate the HMAC using your secret const generatedHash = crypto .createHmac('sha256', process.env.SHOPIFY_API_SECRET!) .update(rawBody, 'utf8') .digest('base64'); // 3. Securely compare the hashes if (generatedHash !== hmac) { return new NextResponse('Invalid signature', { status: 401 }); } // 4. Process the validated data const data = JSON.parse(rawBody); console.log(`Received ${topic} from ${shop}`); return new NextResponse('OK', { status: 200 }); }
3. Why crypto.timingSafeEqual
Matters
crypto.timingSafeEqualIn a production environment, you should actually use
crypto.timingSafeEqual4. Environment Variables and Secret Rotation
Never hardcode your
SHOPIFY_API_SECRET.env5. Testing with Ngrok
Since Shopify can't send webhooks to
localhostConclusion
Security isn't an "add-on"; it’s a foundational requirement. By verifying your Shopify webhooks, you are protecting your business and your customers' data from malicious actors. It’s a few extra lines of code that could save you from a massive data disaster.
Need help securing your headless Shopify architecture? Let's audit your endpoints.
Frequently asked questions
Why is accessing the raw request body critical in Next.js?
Next.js often parses incoming request bodies automatically, usually as JSON. However, to verify a Shopify webhook's HMAC, you must have the exact, un-parsed raw string of the request body. Any alteration, even whitespace, will cause the generated hash to differ, leading to failed verification.
How does crypto.timingSafeEqual
enhance security over a simple !==
comparison?
crypto.timingSafeEqual!==Using
crypto.timingSafeEqual!==timingSafeEqualWhat's the recommended way to handle the Shopify API secret in a Next.js app?
Always store your
SHOPIFY_API_SECRET.envWhy is Ngrok or Cloudflare Tunnel necessary for testing webhook verification locally?
Shopify cannot send webhooks to
localhost