Shopify App Compliance Fix: Solving GDPR Webhooks & HMAC Verification Failures

This Shopify app compliance fix guide provides a focused, step-by-step solution, emphasizing the essential code needed to pass these automated checks, regardless of your application's framework or database structure.

Views: 9

Building a public Shopify App requires meeting stringent compliance standards. Two of the most common stumbling blocks in the App Store review process are failures for Mandatory Compliance Webhooks (GDPR/CCPA) and HMAC Signature Verification.

This Shopify app compliance fix guide provides a focused, step-by-step solution, emphasizing the essential code needed to pass these automated checks, regardless of your application’s framework or database structure.


Step 1: Mandatory GDPR Webhooks Registration via shopify.app.toml

The modern, recommended way to register the mandatory GDPR webhooks (customers/data_request, customers/redact, and shop/redact) is by using the shopify.app.toml file.

1.1. Configure shopify.app.toml

Add the following configuration block to your shopify.app.toml file. This links all three mandatory compliance topics to a single, unified webhook endpoint (uri).

[webhooks]
api_version = "2025-10" # Use the latest or target API version
[[webhooks.subscriptions]]
# These are the three mandatory GDPR/CCPA compliance topics
compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
# Set this to your app's public webhook endpoint URL
uri = "YOUR_PUBLIC_WEBHOOK_URL_HERE" 

1.2. Deploy the Configuration

After saving the changes, deploy the configuration using the Shopify CLI to register the new webhook subscriptions:

shopify app deploy

This ensures the check for Provides mandatory compliance webhooks passes.


Step 2: Implement Robust HMAC Verification in Your Handler

This is the most critical step for security and compliance. Your webhook handler must correctly verify the HMAC signature and, crucially, return a 401 Unauthorized status code if the HMAC fails or if the shop sending the webhook is unknown (as the compliance test often sends a dummy shop).

2.1. The Core HMAC Verification Function (PHP)

This function takes the raw data, the HMAC header, and your app’s secret key, and performs the validation.

/* ------------------------------
   HMAC Verification Function
------------------------------ */
function codoshopify_verify_webhook_hmac_internal($data, $hmac_header, $secret) {
    // 1. Calculate HMAC using the raw POST body data and the App's Secret
    $calculated_hmac = base64_encode(hash_hmac('sha256', $data, $secret, true));
    
    // 2. Safely compare using hash_equals() to prevent timing attacks
    return hash_equals($calculated_hmac, $hmac_header);
}

2.2. The Webhook Handler Logic (Focus on Compliance Checks)

The handler must perform three essential checks before processing any data: Identify Shop, Retrieve Secret, and Verify HMAC.

PHP

/* ------------------------------
   Webhook Handler (Compliance Focus)
------------------------------ */
// Set your handler to respond to the configured webhook URL (e.g., /codoshopify-webhook/)
function handle_shopify_webhook() {

    // 1. Crucial for compliance: prevent any unwanted output or caching in case of WordPress only
    nocache_headers();
    while (ob_get_level()) ob_end_clean(); 

    // Extract necessary data from the request
    $raw   = file_get_contents('php://input'); // The unparsed POST body is REQUIRED for HMAC
    $hmac  = $_SERVER['HTTP_X_SHOPIFY_HMAC_SHA256'] ?? '';
    $topic = strtolower(str_replace('-', '_', $_SERVER['HTTP_X_SHOPIFY_TOPIC'] ?? ''));
    $shop  = $_SERVER['HTTP_X_SHOPIFY_SHOP_DOMAIN'] ?? '';

    // --- YOUR APP-SPECIFIC LOGIC START ---

    // Placeholders for your app's data retrieval.
    // Replace this section with your framework's method to find the shop and secret.
    
    $secret = YOUR_FUNCTION_TO_GET_APP_SECRET_BY_SHOP_DOMAIN($shop);

    // 2. COMPLIANCE CHECK: Shop Not Found / Secret Missing
    // Shopify compliance bot may send a random shop. If we can't find it (or the secret), 
    // we MUST return 401.
    if (empty($secret)) {
        http_response_code(401); 
        echo json_encode(['error' => 'Shop not found or secret missing, unauthorized']);
        exit;
    }

    // --- YOUR APP-SPECIFIC LOGIC END ---

    // 3. HMAC VERIFICATION
    $valid_hmac = codoshopify_verify_webhook_hmac_internal($raw, $hmac, $secret);
    
    if (!$valid_hmac) {
        // FORCE 401 UNAUTHORIZED on HMAC failure
        while (ob_get_level()) ob_end_clean();
        http_response_code(401);
        echo json_encode(['error' => 'Invalid HMAC']);
        exit;
    }

    // 4. PROCESS WEBHOOK (HMAC is valid)
    
    switch ($topic) {
        case 'app_uninstalled':
        case 'shop_redact':
        case 'customers_redact':
        case 'customers_data_request':
            // Add your topic-specific processing logic here
            break;
    }

    // 5. Send SUCCESS Response
    header('Content-Type: application/json');
    http_response_code(200);
    echo json_encode(['status' => 'ok']);
    exit;
}

2.3. Key Compliance Takeaways

  • Raw Data: You must use the raw, unparsed POST body (file_get_contents('php://input')) for HMAC calculation.
  • Forced 401: If the HMAC verification fails or you cannot retrieve the App Secret for the shop, your handler must return an HTTP 401 Unauthorized status code immediately, clearing any buffered output.
  • GDPR Topics: All three compliance topics must result in a 200 OK response if the HMAC is valid, acknowledging receipt of the request.

In case of any confusions, please feel free to ask the questions in the comments box below.

Leave a Reply