Verify Shopify webhooks
Verify that a webhook really came from Shopify before acting on it.
| Identifier | 'shopify' |
| Import | import { verifyShopify } from 'verihook/shopify' |
| Headers | x-shopify-hmac-sha256x-shopify-topicx-shopify-webhook-id |
| Signature | HMAC-SHA256 of the raw body, base64-encoded. |
| Replay window | None (the provider sends no timestamp) |
result.eventType |
the x-shopify-topic header, e.g. orders/create |
Get your secret
Section titled “Get your secret”For an app, the app’s client secret (API secret key) from the Partner Dashboard or the Dev Dashboard. For webhooks created in the store admin under Settings → Notifications → Webhooks, the signing key shown on that page.
Store it in an environment variable (SHOPIFY_WEBHOOK_SECRET below) and never commit it.
Verify a request
Section titled “Verify a request”import { verifyShopify } from 'verihook/shopify';
export async function POST(request: Request) { const result = await verifyShopify(request, process.env.SHOPIFY_WEBHOOK_SECRET!);
if (!result.valid) { console.warn(result.code, result.reason, result.hint); return new Response('Invalid signature', { status: 401 }); }
console.log(result.eventType, result.event); return Response.json({ received: true });}request can be a Fetch Request or { headers, body, url? } with the raw body. verifyWebhook('shopify', request, secret) from verihook does the same. Read why the raw body matters if verification fails behind a body parser.
Framework examples
Section titled “Framework examples”import { createWebhookHandler } from 'verihook/next';
export const POST = createWebhookHandler('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!, async (payload, result) => { // Runs only for a valid signature console.log(result.eventType, result.event);});import express from 'express';import { verihookExpress, type VerihookRequestAdditions } from 'verihook/express';
const app = express();
// Register before express.json(), or keep express.json() off this routeapp.post('/webhooks/shopify', verihookExpress('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!), (req, res) => { const { eventType, event } = (req as typeof req & VerihookRequestAdditions).verihook!; res.json({ received: true });});import Fastify from 'fastify';import { verihookFastify, verihookRawBody, type VerihookFastifyRequest } from 'verihook/fastify';
const app = Fastify();await app.register(verihookRawBody);
app.post('/webhooks/shopify', { preHandler: verihookFastify('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!) }, async (request) => { const { eventType, event } = (request as typeof request & VerihookFastifyRequest).verihook!; return { received: true };});import { Hono } from 'hono';import { verihookHono, type VerihookVariables } from 'verihook/hono';
const app = new Hono<{ Bindings: { SHOPIFY_WEBHOOK_SECRET: string }; Variables: VerihookVariables }>();
app.post('/webhooks/shopify', verihookHono('shopify', (c) => c.env.SHOPIFY_WEBHOOK_SECRET), (c) => { const { eventType, event } = c.get('verihook'); return c.json({ received: true });});// main.ts: NestFactory.create(AppModule, { rawBody: true })import { Controller, Post, Req, UseGuards } from '@nestjs/common';import { createVerihookGuard, type VerihookNestRequest } from 'verihook/nestjs';
@Controller('webhooks')export class WebhooksController { @Post('shopify') @UseGuards(createVerihookGuard('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!)) handle(@Req() req: VerihookNestRequest) { const { eventType, event } = req.verihook!; }}import { createWebhookHandler } from 'verihook/h3';
export default defineEventHandler( createWebhookHandler('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!, async (payload, result) => { console.log(result.eventType); }),);import { env } from '$env/dynamic/private';import { createWebhookHandler } from 'verihook/sveltekit';
export const POST = createWebhookHandler('shopify', () => env.SHOPIFY_WEBHOOK_SECRET, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/remix';
export const action = createWebhookHandler('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/astro';
export const prerender = false;export const POST = createWebhookHandler('shopify', import.meta.env.SHOPIFY_WEBHOOK_SECRET, async (payload, result) => { console.log(result.eventType);});import { createWebhookHandler } from 'verihook/lambda';
export const handler = createWebhookHandler('shopify', process.env.SHOPIFY_WEBHOOK_SECRET!, async (payload, result) => { console.log(result.eventType);});Each adapter responds 401 to an invalid signature, 413 to a body over maxBodySize (2 MB by default) and 200 to a duplicate when you pass a dedupeStore.
Things to know
Section titled “Things to know”- Rotating the app’s client secret changes the webhook signature too. Deploy the new secret before rotating.
Testing
Section titled “Testing”signWebhook from verihook/testing builds a correctly signed Shopify request for your tests:
import { signWebhook } from 'verihook/testing';import { verifyShopify } from 'verihook/shopify';
const hook = await signWebhook('shopify', { secret: 'shopify_test_secret', payload: { id: 'evt_test' },});
const result = await verifyShopify( { headers: hook.headers, body: hook.body, url: hook.url }, hook.secret,);// result.valid === trueFrom the command line, npx verihook simulate shopify --url http://localhost:3000/webhooks/shopify sends one to your local server.
See Testing your handlers for supertest and Fetch Request examples.
Troubleshooting
Section titled “Troubleshooting”Failed results carry a code and often a hint with the likely cause. See Troubleshooting for each error code.