Solved: Persistent Shopify Compliance Webhook Failure (404/307 Error)
Problem Summary
My Shopify App deployment was consistently failing the mandatory compliance webhook check, despite the shopify.app.toml being correctly configured to use a single, generic endpoint for GDPR topics (customers/data_request, customers/redact, shop/redact).
-
Correct Configuration (in shopify.app.toml):
[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
uri = "[https://api.myapp.com/api/webhooks/shopify/webhooks](https://api.myapp.com/api/webhooks/shopify/webhooks)"
-
Initial Error: The Shopify compliance checker was failing with a 404 Not Found error.
- Reason: The checker was hitting an old, specific URL (e.g.,
.../shop/redact) instead of the new, generic URL (.../webhooks). My server had the correct generic handler, so the specific old path returned 404.
-
Subsequent Error (After Adding Redirects): To fix the 404, I temporarily added a 307 Redirect from the old specific URLs to the new generic URL. This resulted in a 307 Temporary Redirect error from the checker.
- Reason: The compliance check expects an immediate response (either
200 OK or 401 Unauthorized) indicating HMAC validation status. It fails when it receives a 307 redirect status, as it treats the redirect itself as a failure to validate the HMAC.
The Root Cause: Stubborn Server-Side Metadata
The core issue was that the automated Shopify compliance checker system was using stuck, outdated metadata and continued testing the old, specific webhook URLs (/shop/redact, /customers/redact, etc.) even after a successful shopify app deploy with the new generic URI.
The solution required forcing our server to temporarily listen on both the old, incorrect paths and the new, correct path to satisfy the checker.
The Final Fix (Code Implementation)
The fix was implemented by modifying the Express router in shopifyWebhooks.js to create specific, immediate POST handlers for the old URLs and routing them to the core handler logic.
We achieved this by defining a reusable handleWebhook function and attaching it to all necessary endpoints.
1. Unified Webhook Logic (handleWebhook function)
We extracted the core logic into a reusable function, ensuring all paths use the same robust logic:
const handleWebhook = async (req, res) => {
// ... switch statement that handles 'app/uninstalled', 'customers/redact', etc.
};
2. Dual Routing Strategy
We set up two sets of routes using the verifyShopifyWebhook middleware and the handleWebhook function:
Route Path
Purpose
/webhooks
Correct, future-proof path, matching the shopify.app.toml file.
/customers/data_request
Critical temporary path to satisfy the checker’s incorrect URL.
/customers/redact
Critical temporary path to satisfy the checker’s incorrect URL.
/shop/redact
Critical temporary path to satisfy the checker’s incorrect URL.
The Final, Working Code Snippet:
// 1. New, Correct Generic Handler (used by app/uninstalled and future versions)
router.post('/webhooks', verifyShopifyWebhook, handleWebhook);
// 2. Handlers for the Old Compliance URLs (to pass the immediate test)
// This solves the 404/307 issue by listening directly on the path the checker hits.
router.post('/customers/data_request', verifyShopifyWebhook, handleWebhook);
router.post('/customers/redact', verifyShopifyWebhook, handleWebhook);
router.post('/shop/redact', verifyShopifyWebhook, handleWebhook);
By deploying this version, the Shopify compliance checker hit the dedicated, HMAC-verified routes for /shop/redact, received the required 401 Unauthorized status (since the checker uses an invalid HMAC), and the compliance requirement was successfully met.
I recommend keeping these three specific POST handlers in your code until the compliance checker passes, as they do not interfere with normal operations.
I hope this helps someone.