Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Avelero/avelero/llms.txt

Use this file to discover all available pages before exploring further.

Avelero handles webhooks from integrated platforms to keep data synchronized and comply with platform requirements. This guide covers Shopify webhooks, GDPR compliance handling, and webhook security.

Shopify webhooks

Shopify sends webhooks to Avelero for GDPR compliance and data management.

Mandatory compliance webhooks

Shopify requires all apps to handle three compliance webhooks:
Purpose: Triggered when a customer requests their data from a Shopify store.Avelero’s handling: No action taken. Avelero only has read_products scope and does not store any customer data.Response: 200 OK (acknowledges receipt)Payload structure:
{
  "shop_id": 12345,
  "shop_domain": "example.myshopify.com",
  "customer": {
    "id": 67890,
    "email": "customer@example.com",
    "phone": "+1234567890"
  },
  "orders_requested": [1, 2, 3],
  "data_request": {
    "id": 111
  }
}
Purpose: Triggered when a store owner requests customer data deletion (GDPR right to erasure).Avelero’s handling: No action taken. Avelero does not store customer data.Response: 200 OK (acknowledges receipt)Payload structure:
{
  "shop_id": 12345,
  "shop_domain": "example.myshopify.com",
  "customer": {
    "id": 67890,
    "email": "customer@example.com",
    "phone": "+1234567890"
  },
  "orders_to_redact": [1, 2, 3]
}
Purpose: Sent 48 hours after a store uninstalls the Avelero app. Requires deletion of all shop data.Avelero’s handling:
  • Deletes brand integration record for the shop
  • Deletes any pending installation records
  • Product data is retained (belongs to brand, not Shopify)
Response: 200 OK (even if deletion fails to prevent retry)Payload structure:
{
  "shop_id": 12345,
  "shop_domain": "example.myshopify.com"
}

Webhook endpoint

All Shopify compliance webhooks are sent to a unified endpoint:
POST https://api.avelero.com/integrations/webhooks/compliance
Routing: The X-Shopify-Topic header determines which handler processes the webhook.

Webhook headers

Shopify includes several headers with webhook requests:
X-Shopify-Topic
string
required
Webhook topic (e.g., customers/data_request, shop/redact)
X-Shopify-Hmac-Sha256
string
required
HMAC signature for verifying webhook authenticity (base64-encoded)
X-Shopify-Shop-Domain
string
Shop domain that triggered the webhook
X-Shopify-API-Version
string
Shopify API version used to generate the webhook
Content-Type
string
required
Always application/json for compliance webhooks

HMAC verification

All Shopify webhooks must be verified to prevent spoofing and replay attacks.

How HMAC works

Shopify signs webhooks using HMAC-SHA256:
  1. Shopify generates a hash of the raw request body using the app’s client secret
  2. Shopify base64-encodes the hash and includes it in the X-Shopify-Hmac-Sha256 header
  3. Avelero receives the webhook and recomputes the hash using the same secret
  4. Avelero compares the computed hash to the header value
  5. If hashes match, the webhook is authentic; otherwise, it’s rejected

HMAC verification implementation

Avelero verifies all incoming Shopify webhooks:
Verification function
import { createHmac } from 'crypto';

function verifyShopifyWebhookHmac(
  rawBody: string,
  hmacHeader: string,
  clientSecret: string
): boolean {
  const hash = createHmac('sha256', clientSecret)
    .update(rawBody, 'utf8')
    .digest('base64');
  
  return hash === hmacHeader;
}
Key points:
  • Use the raw request body (before JSON parsing)
  • Hash encoding is base64 (not hex)
  • Use client secret (not access token)
  • Comparison must be constant-time to prevent timing attacks

Verification flow

Avelero’s webhook handler verifies requests before processing:
1

Check Content-Type

Ensure Content-Type is application/json.
2

Extract HMAC header

Get X-Shopify-Hmac-Sha256 header value.
3

Validate secret is configured

Ensure SHOPIFY_CLIENT_SECRET environment variable is set.
4

Get raw body

Capture the raw request body as a string (before parsing).
5

Compute HMAC

Generate HMAC-SHA256 hash of raw body using client secret.
6

Compare hashes

Use constant-time comparison to check if computed hash matches header.
7

Respond appropriately

  • Valid: Process webhook and return 200 OK
  • Invalid: Return 401 Unauthorized (do not process)

HMAC verification errors

Status: 401 UnauthorizedCause: Request doesn’t include X-Shopify-Hmac-Sha256 header.Resolution: This indicates a spoofed request. Reject it.
Status: 401 UnauthorizedCause: Computed hash doesn’t match header value.Possible reasons:
  • Wrong client secret configured
  • Request body was modified in transit
  • Replay attack attempt
  • Request not from Shopify
Resolution: Reject the webhook. Do not process.
Status: 500 Internal Server ErrorCause: SHOPIFY_CLIENT_SECRET environment variable not configured.Resolution: Set the client secret in environment configuration.
Status: 400 Bad RequestCause: Content-Type is not application/json.Resolution: Shopify compliance webhooks are always JSON. Reject non-JSON requests.

Security best practices

Always verify HMAC

Never process webhooks without HMAC verification. This prevents webhook spoofing and injection attacks.

Use raw body

Compute HMAC using the raw request body before JSON parsing. Body modification invalidates the signature.

Protect client secret

Store client secret in environment variables, never in code. Rotate secrets if compromised.

Constant-time comparison

Use timing-safe comparison to prevent timing attacks that could leak hash information.

Webhook handler implementation

Unified handler

Avelero uses a single endpoint for all compliance webhooks:
Webhook router
import { Hono } from 'hono';
import { verifyShopifyWebhookHmac } from '@v1/integrations';

const webhooksRouter = new Hono();

webhooksRouter.post('/', async (c) => {
  // Get topic from header
  const topic = c.req.header('x-shopify-topic');
  
  if (!topic) {
    return c.json({ error: 'Missing X-Shopify-Topic header' }, 400);
  }
  
  // Verify HMAC
  const result = await verifyAndParseWebhook(c);
  if (!result.success) return result.response;
  
  // Route based on topic
  switch (topic) {
    case 'customers/data_request':
      return handleCustomersDataRequest(c, result.payload);
    case 'customers/redact':
      return handleCustomersRedact(c, result.payload);
    case 'shop/redact':
      return handleShopRedact(c, result.payload);
    default:
      return c.json({ error: 'Unknown webhook topic' }, 400);
  }
});

Error handling

Webhook handlers must handle errors gracefully: Return 200 OK even on errors:
  • Prevents Shopify from retrying (compliance webhooks don’t need retry)
  • Logs errors for debugging without blocking Shopify
Error handling example
async function handleShopRedact(payload) {
  try {
    await deleteBrandIntegration(payload.shop_domain);
  } catch (error) {
    console.error('Failed to delete integration:', error);
    // Still return 200 to prevent retry
  }
  
  return new Response(JSON.stringify({ success: true }), {
    status: 200
  });
}

shop/redact implementation

The shop/redact webhook requires special handling:

Data deletion logic

1

Identify shop

Extract shop_domain from webhook payload.
2

Delete brand integration

Remove the brand_integrations record for this shop.
const deleted = await deleteBrandIntegrationByShopDomain(
  db,
  payload.shop_domain
);
3

Delete pending installations

Remove any pending_installations records for this shop.
await deletePendingInstallation(db, payload.shop_domain);
4

Retain product data

Important: Do not delete products. Product data belongs to the brand, not the Shopify integration.

What gets deleted

brand_integrations
record
Integration connection record linking brand to Shopify shop
pending_installations
record
Pending OAuth installations not yet claimed
integration_credentials
encrypted
Shopify access tokens (cascade delete via foreign key)

What gets retained

products
records
All product records remain in Avelero
product_variants
records
Variant data is preserved for compliance and history
Links between Shopify entities and Avelero entities (orphaned but not deleted)
Product data is retained because it represents the brand’s catalog and may be required for regulatory compliance. The integration is removed, but the data persists.

Testing webhooks

Shopify provides tools for testing webhook handling:

Using Shopify CLI

Trigger test webhook
shopify webhook trigger --topic=shop/redact \
  --api-version=2025-01 \
  --delivery-method=http \
  --address=https://api.avelero.com/integrations/webhooks/compliance

Manual testing

Generate a valid HMAC signature for testing:
Generate test HMAC
import { createHmac } from 'crypto';

const payload = JSON.stringify({
  shop_id: 12345,
  shop_domain: 'test-store.myshopify.com'
});

const hmac = createHmac('sha256', process.env.SHOPIFY_CLIENT_SECRET)
  .update(payload, 'utf8')
  .digest('base64');

console.log('HMAC:', hmac);
Use the generated HMAC in the X-Shopify-Hmac-Sha256 header.

Validation checklist

  • Generate valid HMAC signature
  • Send POST request with correct headers
  • Verify response is 200 OK
  • Check logs confirm processing
  • Send request with wrong HMAC
  • Verify response is 401 Unauthorized
  • Confirm webhook is not processed
  • Send request without X-Shopify-Hmac-Sha256
  • Verify response is 401 Unauthorized
  • Send valid shop/redact webhook
  • Verify integration record is deleted
  • Confirm products are retained

Webhook retry behavior

Shopify’s webhook retry policy:
  • First attempt: Immediate
  • Retry attempts: Up to 19 retries over 48 hours
  • Backoff: Exponential backoff between retries
  • Success criteria: HTTP 200-299 response
Avelero’s approach:
  • Always return 200 OK for compliance webhooks
  • Log errors internally without failing the webhook
  • Prevents unnecessary retries for intentional no-ops

Security considerations

Webhook spoofing prevention

1

HMAC verification

Always verify the HMAC signature before processing.
2

Client secret protection

Store client secret in environment variables, not in code or database.
3

HTTPS enforcement

Only accept webhooks over HTTPS to prevent man-in-the-middle attacks.
4

Rate limiting

Implement rate limiting to prevent webhook flooding attacks.

Replay attack prevention

While Shopify webhooks don’t include timestamps, Avelero mitigates replay attacks:
  • HMAC verification ensures request came from Shopify
  • Idempotent handlers ensure duplicate webhooks are safe
  • shop/redact handler deletes records only if they exist (no error if already deleted)

Debugging webhooks

When webhooks fail:

Check logs

Avelero logs webhook activity:
Webhook logging
console.log('Shopify webhook:', {
  topic: headers['x-shopify-topic'],
  shopDomain: payload.shop_domain,
  timestamp: new Date().toISOString()
});

Common issues

Causes:
  • Webhook URL not registered in Shopify app settings
  • Firewall blocking Shopify’s IP range
  • Server not responding to Shopify requests
Debug:
  • Check Shopify Partner dashboard webhook settings
  • Verify endpoint is publicly accessible
  • Test with curl from external server
Causes:
  • Wrong client secret configured
  • Request body modified before verification
  • Using hex encoding instead of base64
Debug:
  • Log computed hash and header hash for comparison
  • Verify client secret matches Shopify app credentials
  • Ensure raw body is used (not parsed JSON)
Causes:
  • Database query error
  • Shop domain mismatch
  • Integration already deleted
Debug:
  • Check database logs for errors
  • Verify shop_domain matches database record
  • Confirm deletion query is executed

API reference

Webhook endpoint

POST /integrations/webhooks/compliance
Headers:
  • X-Shopify-Topic (required) - Webhook topic
  • X-Shopify-Hmac-Sha256 (required) - HMAC signature
  • Content-Type: application/json (required)
Response codes:
  • 200 OK - Webhook processed successfully
  • 400 Bad Request - Missing topic or invalid content type
  • 401 Unauthorized - Invalid HMAC signature
  • 500 Internal Server Error - Server configuration error

Database operations

Delete integration:
import { deleteBrandIntegrationByShopDomain } from '@v1/db/queries/integrations';

const deleted = await deleteBrandIntegrationByShopDomain(db, shopDomain);
Delete pending installation:
import { deletePendingInstallation } from '@v1/db/queries/integrations';

const deleted = await deletePendingInstallation(db, shopDomain);

Next steps

Shopify integration

Learn how to set up and configure the Shopify integration.

GDPR compliance

Understand data privacy and GDPR compliance in Avelero.

Build docs developers (and LLMs) love