diff --git a/.env.example b/.env.example
index 088014635..dc6582eb5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,6 @@
 TWITTER_CREATOR="@vercel"
 TWITTER_SITE="https://nextjs.org/commerce"
 SITE_NAME="Next.js Commerce"
+SHOPIFY_REVALIDATION_SECRET=
 SHOPIFY_STOREFRONT_ACCESS_TOKEN=
 SHOPIFY_STORE_DOMAIN=
diff --git a/README.md b/README.md
index da03958fe..c9342d40a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&env=SHOPIFY_STOREFRONT_ACCESS_TOKEN,SHOPIFY_STORE_DOMAIN,SITE_NAME,TWITTER_CREATOR,TWITTER_SITE)
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&env=SHOPIFY_REVALIDATION_SECRET,SHOPIFY_STOREFRONT_ACCESS_TOKEN,SHOPIFY_STORE_DOMAIN,SITE_NAME,TWITTER_CREATOR,TWITTER_SITE)
 
 # Next.js Commerce
 
@@ -157,7 +157,46 @@ You can use Shopify's admin to customize these pages to match your brand and des
 
 ### Configure webhooks for on-demand incremental static regeneration (ISR)
 
-Coming soon.
+Utilizing [Shopify's webhooks](https://shopify.dev/docs/apps/webhooks), and listening for select [Shopify webhook event topics](https://shopify.dev/docs/api/admin-rest/2022-04/resources/webhook#event-topics), we can use [Next'js on-demand revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#using-on-demand-revalidation) to keep data fetches indefinitely cached until certain events in the Shopify store occur.
+
+Next.js is pre-configured to listen for the following Shopify webhook events and automatically revalidate fetches.
+
+- `collections/create`
+- `collections/delete`
+- `collections/update`
+- `products/create`
+- `products/delete`
+- `products/update` (this also includes when variants are added, updated, and removed as well as when products are purchased so inventory and out of stocks can be updated)
+
+<details>
+  <summary>Expand to view detailed walkthrough</summary>
+
+#### Setup secret for secure revalidation
+
+1. Create your own secret or [generate a random UUID](https://www.uuidgenerator.net/guid).
+1. Create a [Vercel Environment Variable](https://vercel.com/docs/concepts/projects/environment-variables) named `SHOPIFY_REVALIDATION_SECRET` and use the value from above.
+
+#### Configure Shopify webhooks
+
+1. Navigate to `https://SHOPIFY_STORE_SUBDOMAIN.myshopify.com/admin/settings/notifications`.
+1. Add webhooks for all six event topics listed above. You can add more sets for other preview urls, environments, or local development. Append `?secret=[SECRET]` to each url, where `[SECRET]` is the secret you created above.
+   ![Shopify store webhooks](https://github.com/vercel/commerce/assets/446260/3d713fd7-b642-46e2-b2ce-f2b695ff6d2b)
+   ![Shopify store add webhook](https://github.com/vercel/commerce/assets/446260/f0240a22-be07-42bc-bf6c-b97873868677)
+
+#### Testing webhooks during local development
+
+The easiest way to test webhooks while developing locally is to use [ngrok](https://ngrok.com).
+
+1. [Install and configure ngrok](https://ngrok.com/download) (you will need to create an account).
+1. Run your app locally, `npm run dev`.
+1. In a separate terminal session, run `ngrok http 3000`.
+1. Use the url generated by ngrok and add or update your webhook urls in Shopify.
+   ![ngrok](https://github.com/vercel/commerce/assets/446260/5dc09c5d-0e48-479c-ab64-de8dc9a2c4b1)
+   ![Shopify store edit webhook](https://github.com/vercel/commerce/assets/446260/13fd397d-4666-4e8d-b25f-4adc674345c0)
+1. You can now make changes to your store and your local app should receive updates. You can also use the `Send test notification` button to trigger a generic webhook test.
+   ![Shopify store webhook send test notification](https://github.com/vercel/commerce/assets/446260/e872e233-1663-446d-961f-8c9455358530)
+
+</details>
 
 ### Using Shopify as a CMS
 
diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts
new file mode 100644
index 000000000..94ddfff9b
--- /dev/null
+++ b/app/api/revalidate/route.ts
@@ -0,0 +1,37 @@
+import { TAGS } from 'lib/constants';
+import { revalidateTag } from 'next/cache';
+import { headers } from 'next/headers';
+import { NextRequest, NextResponse } from 'next/server';
+
+export const runtime = 'edge';
+
+// We always need to respond with a 200 status code to Shopify,
+// otherwise it will continue to retry the request.
+export async function POST(req: NextRequest): Promise<Response> {
+  const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
+  const productWebhooks = ['products/create', 'products/delete', 'products/update'];
+  const topic = headers().get('x-shopify-topic') || 'unknown';
+  const secret = req.nextUrl.searchParams.get('secret');
+  const isCollectionUpdate = collectionWebhooks.includes(topic);
+  const isProductUpdate = productWebhooks.includes(topic);
+
+  if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {
+    console.error('Invalid revalidation secret.');
+    return NextResponse.json({ status: 200 });
+  }
+
+  if (!isCollectionUpdate && !isProductUpdate) {
+    // We don't need to revalidate anything for any other topics.
+    return NextResponse.json({ status: 200 });
+  }
+
+  if (isCollectionUpdate) {
+    revalidateTag(TAGS.collections);
+  }
+
+  if (isProductUpdate) {
+    revalidateTag(TAGS.products);
+  }
+
+  return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
+}
diff --git a/lib/constants.tsx b/lib/constants.tsx
index 9038127e3..99711221a 100644
--- a/lib/constants.tsx
+++ b/lib/constants.tsx
@@ -20,6 +20,11 @@ export const sorting: SortFilterItem[] = [
   { title: 'Price: High to low', slug: 'price-desc', sortKey: 'PRICE', reverse: true }
 ];
 
+export const TAGS = {
+  collections: 'collections',
+  products: 'products'
+};
+
 export const HIDDEN_PRODUCT_TAG = 'nextjs-frontend-hidden';
 export const DEFAULT_OPTION = 'Default Title';
 export const SHOPIFY_GRAPHQL_API_ENDPOINT = '/api/2023-01/graphql.json';
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index 5d3972536..2e23a7f78 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -1,4 +1,4 @@
-import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT } from 'lib/constants';
+import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
 import { isShopifyError } from 'lib/type-guards';
 import {
   addToCartMutation,
@@ -52,15 +52,17 @@ const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
 type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
 
 export async function shopifyFetch<T>({
-  query,
-  variables,
+  cache = 'force-cache',
   headers,
-  cache = 'force-cache'
+  query,
+  tags,
+  variables
 }: {
-  query: string;
-  variables?: ExtractVariables<T>;
-  headers?: HeadersInit;
   cache?: RequestCache;
+  headers?: HeadersInit;
+  query: string;
+  tags?: string[];
+  variables?: ExtractVariables<T>;
 }): Promise<{ status: number; body: T } | never> {
   try {
     const result = await fetch(endpoint, {
@@ -75,7 +77,7 @@ export async function shopifyFetch<T>({
         ...(variables && { variables })
       }),
       cache,
-      next: { revalidate: 900 } // 15 minutes
+      ...(tags && { next: { tags } })
     });
 
     const body = await result.json();
@@ -249,6 +251,7 @@ export async function getCart(cartId: string): Promise<Cart | null> {
 export async function getCollection(handle: string): Promise<Collection | undefined> {
   const res = await shopifyFetch<ShopifyCollectionOperation>({
     query: getCollectionQuery,
+    tags: [TAGS.collections],
     variables: {
       handle
     }
@@ -268,6 +271,7 @@ export async function getCollectionProducts({
 }): Promise<Product[]> {
   const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
     query: getCollectionProductsQuery,
+    tags: [TAGS.collections, TAGS.products],
     variables: {
       handle: collection,
       reverse,
@@ -284,7 +288,10 @@ export async function getCollectionProducts({
 }
 
 export async function getCollections(): Promise<Collection[]> {
-  const res = await shopifyFetch<ShopifyCollectionsOperation>({ query: getCollectionsQuery });
+  const res = await shopifyFetch<ShopifyCollectionsOperation>({
+    query: getCollectionsQuery,
+    tags: [TAGS.collections]
+  });
   const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
   const collections = [
     {
@@ -311,6 +318,7 @@ export async function getCollections(): Promise<Collection[]> {
 export async function getMenu(handle: string): Promise<Menu[]> {
   const res = await shopifyFetch<ShopifyMenuOperation>({
     query: getMenuQuery,
+    tags: [TAGS.collections],
     variables: {
       handle
     }
@@ -344,6 +352,7 @@ export async function getPages(): Promise<Page[]> {
 export async function getProduct(handle: string): Promise<Product | undefined> {
   const res = await shopifyFetch<ShopifyProductOperation>({
     query: getProductQuery,
+    tags: [TAGS.products],
     variables: {
       handle
     }
@@ -355,6 +364,7 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
 export async function getProductRecommendations(productId: string): Promise<Product[]> {
   const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
     query: getProductRecommendationsQuery,
+    tags: [TAGS.products],
     variables: {
       productId
     }
@@ -374,6 +384,7 @@ export async function getProducts({
 }): Promise<Product[]> {
   const res = await shopifyFetch<ShopifyProductsOperation>({
     query: getProductsQuery,
+    tags: [TAGS.products],
     variables: {
       query,
       reverse,