How to Configure Shopify Webhooks and Fix Duplicate Webhook Subscriptions
One of the most common issues developers face when building Shopify embedded apps is webhook configuration problems. These issues often manifest as webhooks not firing, duplicate webhook calls, or subscription failures. The root cause? Defining webhooks in two places at once.
This tutorial explains the common mistake of duplicating webhook definitions between shopify.app.toml
and the shopifyApp()
function, and shows you the correct way to configure webhooks using only the TOML file.
The Problem: Duplicate Webhook Definitions
Many developers starting with Shopify embedded apps naturally try to define webhooks in both configuration locations, thinking this ensures proper setup. Unfortunately, this causes conflicts and unpredictable behavior.
❌ Common Mistake - Don't Do This
Here's what not to do:
# shopify.app.toml
[[webhooks.subscriptions]]
topics = [ "orders/updated" ]
uri = "/webhooks"
AND ALSO in your app initialization:
// app.js or similar
const shopify = shopifyApp({
webhooks: {
webhooks: [
{
topic: 'orders/updated',
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/webhooks',
},
],
},
});
This creates duplicate webhook subscriptions that conflict with each other, leading to:
- Webhooks not being delivered
- Inconsistent behavior between environments
- Difficult-to-debug subscription issues
- Potential rate limit problems
The Solution: TOML-Only Configuration
The correct approach is to define your webhooks only once in the shopify.app.toml
file.
✅ Correct Implementation
Step 1: Define webhooks in shopify.app.toml
# shopify.app.toml
[webhooks]
api_version = "2024-10"
[[webhooks.subscriptions]]
topics = [ "orders/updated" ]
uri = "/webhooks" # Can be relative or absolute URL
Step 2: Initialize shopifyApp() without webhook definitions
// app.js or similar
const shopify = shopifyApp({
api: {
apiVersion: LATEST_API_VERSION,
restResources,
billing: billingConfig,
},
// NO webhooks section when using TOML
});
Why This Works
When you define webhooks in shopify.app.toml
, the Shopify CLI automatically manages webhook subscriptions during app installation and updates. The TOML configuration is the single source of truth, and Shopify's tooling handles the rest.
By removing the duplicate definition from shopifyApp()
, you:
- Eliminate configuration conflicts
- Follow Shopify's recommended best practices
- Simplify your codebase
- Make webhook management more maintainable
Key Takeaways
- Choose one configuration method: Use
shopify.app.toml
for embedded apps - Choose appropriate webhook URL format: Use relative paths for embedded apps receiving webhooks directly (e.g.,
/webhooks
), or absolute URLs when using webhook infrastructure like Hookdeck - Remove duplicate definitions: If you have webhooks defined in both places, remove them from
shopifyApp()
- Test after changes: Always test your webhook configuration in development before deploying
Common Gotchas
Even with correct configuration, there are two common issues that can cause webhook failures. Understanding these upfront can save hours of debugging.
Protected Customer Data Requirements
If you're using the orders/updated
webhook topic (or other webhooks that include customer data like orders/create
), you may encounter webhook delivery failures even when your configuration is correct. This is because Shopify requires special permissions for protected customer data.
Why this happens:
- Both
orders/create
andorders/updated
webhooks require access to protected customer data - Shopify considers certain customer information (like email addresses and phone numbers) as protected data
- Even if your app has the necessary scopes, you must explicitly request access to protected customer data
- This is separate from your app's OAuth scopes and requires additional approval
How to fix it:
- Navigate to your app's settings in the Shopify Partner Dashboard
- Request access to protected customer data via the Protected Customer Data form
- Explain your use case for accessing this data
- Wait for Shopify's review and approval
Important: You can have read_orders
scope but still lack access to protected customer data. If you're seeing webhook delivery failures or missing customer data in your webhook payloads, this is likely the cause.
Trailing Slash URL Issues
Shopify automatically adds a trailing slash (/
) to webhook URLs, which can cause 404 errors if your server isn't configured to handle both formats. This is a subtle but common source of webhook failures.
The problem:
- You configure the webhook URL as
https://yourapp.com/webhooks
- Shopify sends requests to
https://yourapp.com/webhooks/
(with trailing slash) - Your Express.js route only matches
/webhooks
(without trailing slash) - Result: 404 error and failed webhook delivery
The solution: Configure your Express.js application to handle both URL formats:
// Handle both /webhooks and /webhooks/ to account for Shopify's trailing slash
app.post(['/webhooks', '/webhooks/'], express.raw({ type: 'application/json' }), async (req, res) => {
try {
const hmac = req.get('X-Shopify-Hmac-Sha256');
const topic = req.get('X-Shopify-Topic');
const shop = req.get('X-Shopify-Shop-Domain');
// Verify the webhook is from Shopify
const verified = verifyShopifyWebhook(req.body, hmac);
if (!verified) {
console.error('Webhook verification failed');
return res.status(401).send('Unauthorized');
}
// Process the webhook
console.log(`Received ${topic} webhook from ${shop}`);
// Send success response immediately
res.status(200).send('Webhook received');
// Process webhook data asynchronously
await processWebhook(topic, req.body);
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Internal server error');
}
});
Key points:
- Use an array
['/webhooks', '/webhooks/']
to match both URL formats - This ensures your app responds successfully regardless of which format Shopify uses
- Always return a 200 status code quickly to acknowledge receipt
- Process the webhook data asynchronously after responding
Conclusion
Following this guide will help you properly configure webhook delivery from Shopify to your embedded app. By understanding the proper TOML-only configuration approach and being aware of common gotchas like protected customer data requirements and trailing slash issues, you can avoid the most common webhook pitfalls and build a more robust integration.
Simplify Shopify Webhook Management with Hookdeck
Hookdeck provides complete webhook infrastructure for Shopify apps in both development and production. Get reliable delivery, automatic retries, powerful debugging tools, local testing with the CLI, and production-grade monitoring. Get started for free to simplify your Shopify webhook integration.
Related Resources
- The Definitive Guide to Shopify Webhooks - Comprehensive guide to reliably working with Shopify webhooks over HTTPS using Hookdeck
- Getting Started with Shopify Webhooks - Introduction to Shopify webhook events and integration fundamentals
- Shopify Webhooks Documentation - Official Shopify webhook configuration and setup guide
- Shopify App Remix Template - Official Shopify Remix app template and shopifyApp() documentation
- Shopify Webhook Best Practices - Official best practices for implementing Shopify webhooks