Shopify webhook verification uses HMAC-SHA256 but encodes the result as Base64, not hex. This is different from Stripe and GitHub. Using .digest('hex') instead of .digest('base64') causes every verification to fail.
// app/api/webhooks/shopify/route.ts
import { createHmac, timingSafeEqual } from 'crypto';
import { NextResponse } from 'next/server';
const secret = process.env.SHOPIFY_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text();
const sig = request.headers.get('x-shopify-hmac-sha256');
if (!sig) {
return NextResponse.json({ error: 'Missing X-Shopify-Hmac-Sha256' }, { status: 400 });
}
// Shopify uses Base64, not hex
const computed = createHmac('sha256', secret).update(body, 'utf8').digest('base64');
const sigBuf = Buffer.from(sig);
const compBuf = Buffer.from(computed);
if (sigBuf.length !== compBuf.length || !timingSafeEqual(sigBuf, compBuf)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const payload = JSON.parse(body);
const topic = request.headers.get('x-shopify-topic');
switch (topic) {
case 'orders/create':
// handle new order
break;
case 'orders/cancelled':
// handle cancellation
break;
case 'customers/create':
// handle new customer
break;
}
return NextResponse.json({ received: true });
}
X-Shopify-Hmac-Sha256 as a Base64-encoded HMAC. Using .digest('hex') (like Stripe or GitHub) produces the wrong value and every verification fails.# Shopify Admin → Settings → Notifications → Webhooks # Each webhook has its own signing secret shown under the endpoint # Or via Admin API: GET /admin/api/webhooks.json
Generate the complete Shopify webhook handler for your framework.
Open WebhookFix →