Author picture Phil Leggetter

The Definitive Guide to Reliably Working with Shopify Webhooks over HTTPS Using Hookdeck

Published


Introduction: Why Hookdeck for Shopify HTTPS Webhooks

Shopify recommends that engineering teams use or build highly reliable and scalable webhook event ingestion infrastructure. This ensures they never miss an event and their platforms and businesses are not negatively impacted.

For very high-volume workloads, Shopify highlights managed event buses like GCP Pub/Sub and Amazon EventBridge. These platforms offer durability and horizontal scale, but adopting those options usually means working with AWS or GCP and re-architecting how your application ingests and processes events.

Many teams prefer to keep the simplicity, direct integration capabilities, and lower operational overhead of HTTPS webhooks. Shopify's official HTTPS webhook guide outlines the production requirements for HTTPS delivery. These include secure endpoints, timely acknowledgements, strong signature verification, and queuing patterns.

Across other documentation such as Shopify webhook best practices, they outline approaches to duplicate detection, troubleshooting, and monitoring.

The Hookdeck Event Gateway gives you the best of both worlds. It provides the reliability and scale of a managed event platform while preserving the developer experience of HTTPS webhooks. An Event Gateway acts as an intermediary layer that receives, processes, and reliably delivers webhooks to your application, adding enterprise-grade features like retry logic, monitoring, and deduplication.

Hookdeck implements all of Shopify's HTTPS webhook production requirements. These include fast acknowledgements/connection handling, robust retry policies, deduplication, HMAC signature verification, HTTPS termination, and observability. It then extends them with production-ready controls.

Beyond simply meeting Shopify's checklist, Hookdeck provides additional capabilities that reduce engineering effort and risk. These include a 60-second default delivery window, extended retry windows, configurable deduplication (recommended using Shopify headers), centralized signature verification, durable queues with dead-letter handling, searchable logs with event retention (up to 30 days), manual/bulk retries, routing and transformations for migrations or fan-out, rate limiting and traffic filtering, and granular monitoring and alerting.

These features let teams continue using HTTPS webhooks while gaining the operational guarantees of a managed event bus.

This guide shows step-by-step how to set up Hookdeck for Shopify HTTPS webhooks. You'll learn how to use these features to build production-ready integrations that satisfy, and extend, Shopify's reliability standards.

Shopify's HTTPS Webhook Recommendations and Hookdeck's Solutions

Shopify's recommendations require building sophisticated retry systems, queues, observability infrastructure, and deduplication logic. Here's how Hookdeck addresses each requirement:

Shopify RecommendationWhy It MattersShopify's DefaultHow Hookdeck Helps
Respond quicklyShopify expects a 200 OK within 5 seconds or it retries. Slow endpoints risk duplicate deliveries and backlogs.5-second timeout before retry.Hookdeck acknowledges immediately and gives your app a 60-second delivery window, reducing false retries during slightly longer processing.
Handle retriesPrevents lost events if your app is down.8 retries over 4 hours; if all fail and subscription was created via Admin API, it's deleted.Highly configurable retry policies, no subscription deletion risk, and retries continue beyond Shopify's limit until success or dead-lettered.
Prevent duplicatesShopify retries + network instability can cause duplicates, requiring idempotency.No native deduplication.Configurable deduplication detects and drops duplicates. For Shopify, using the X-Shopify-Event-Id header as the identifier is recommended.
Verify signaturesEnsures authenticity of payload.Requires custom HMAC logic in your app.Hookdeck verifies Shopify's signature for you (when you provide the webhook secret). Your app only needs to verify Hookdeck's own signature, one implementation works across multiple platforms.
Secure endpointsPrevent unwanted requests.HTTPS only.All traffic terminates over HTTPS. You can implement IP-based filtering using Hookdeck Transformations and Filters to drop unwanted requests before delivery.
Monitor and alertSpot failures early.Basic delivery logs in Shopify admin.Searchable logs with retention (up to 30 days), manual/bulk retries, metrics, and alerting from the Hookdeck dashboard.
Handle version changesAPI version changes can break parsing.API version pinned at subscription creation.Route events by version or topic; fan out to old/new consumers during migrations.

How Hookdeck Improves on Shopify's Default HTTPS Webhook Behavior

Persistent Queue and Reconciliation

Why It Matters: Webhook reliability is fundamental to building robust e-commerce integrations. Any lost event could mean missed sales notifications, inventory updates, or customer actions that directly impact business operations.

Shopify's Approach: While Shopify does implement a retry mechanism (8 retries over 4 hours with exponential backoff), events can still be permanently lost. This happens if your endpoint remains unavailable throughout this entire window, or if the webhook subscription gets automatically deleted after consecutive failures. Additionally, during extended outages, events can pile up and overwhelm your system once it comes back online.

Hookdeck's Solution: The Hookdeck Event Gateway implements a guaranteed ingestion model. It immediately acknowledges receipt from Shopify (preventing unnecessary retries), persists each event in a durable queue, and handles delivery to your application separately. This separation reduces duplicate processing, minimizes backlogs, and extends reliability beyond Shopify's ~4-hour retry window.

Timeout Flexibility

Modern applications often need more processing time than Shopify's standard timeout provides, especially when handling complex business logic, external API calls, or database transactions that can't be easily rushed.

Why It Matters: Applications performing inventory synchronization, customer data enrichment, or third-party integrations frequently require more than Shopify's tight deadlines. These operations might involve multiple database queries, external service calls, or complex calculations. These simply cannot be compressed into such a narrow window.

Shopify's Constraints: Shopify enforces strict timing requirements with a 1-second connection timeout (your server must accept connections within 1 second) and a 5-second total timeout for the entire request lifecycle. Any response taking longer triggers an automatic retry, regardless of whether your application is still successfully processing the original request. This creates unnecessary duplicate processing and can lead to race conditions.

Hookdeck's Solution: Hookdeck provides a 60-second delivery window for your application. This gives legitimate processing more time than Shopify's 5-second limit. Because Hookdeck acknowledges Shopify immediately, your handler can complete within this window without triggering unnecessary Shopify retries.

Retry Policy Flexibility

Transient failures are inevitable in distributed systems, but losing webhook subscriptions due to temporary issues can disrupt business operations and require manual intervention to restore functionality.

Why It Matters: Production applications face numerous failure scenarios: deployment downtime, database maintenance windows, network partitions, or temporary service degradation. These issues shouldn't result in permanent data loss or subscription termination, yet Shopify's retry policy has specific boundaries that can lead to these outcomes.

Shopify's Approach: Shopify attempts ~8 retries over a ~4-hour window using exponential backoff. If your subscription was created via the Admin API and delivery repeatedly fails, Shopify may disable or delete the subscription. Notifications are sent to the Partner account's emergency developer email.

Hookdeck's Advantages: The Hookdeck configurable webhook retries functionality offers:

  • Subscription preservation — webhook connections remain active indefinitely, regardless of delivery failures
  • Flexible retry scheduling — choose exponential backoff, linear intervals, or custom timing that matches your application's recovery patterns
  • Extended retry windows — continue attempts far beyond Shopify's 4-hour limit
  • Dead letter handling — problematic events are flagged for investigation rather than discarded, ensuring no data is permanently lost

Note on Failed Event Management: Unlike traditional dead letter queues that move failed events to separate storage, Hookdeck keeps failed events in the same system with full observability and manual/bulk retry capabilities. This provides DLQ-like reliability while maintaining complete event recovery control.

Configurable Deduplication

Duplicate webhook processing can corrupt application state, trigger double charges, send multiple notifications, or create inconsistent data across systems. This makes Deduplication (the configurable process of identifying and filtering out duplicate or noisy webhook events to ensure each event is processed only once) a critical but complex requirement for production applications.

Why It Matters: Network instability, Shopify's retry logic, and application timeouts can all contribute to duplicate webhook deliveries. Without proper deduplication, your application might process the same order update multiple times. This leads to inventory discrepancies, duplicate customer notifications, or inconsistent analytics data.

Shopify's Approach: Shopify provides no built-in deduplication mechanism. Developers must implement custom idempotency logic in their applications. This typically involves using database constraints, caching layers, or complex state tracking across multiple webhook events.

Hookdeck's Deduplication: Hookdeck can be configured to deduplication events before they reach your application. Configure the X-Shopify-Event-Id header as the deduplication key to ensure each unique webhook is processed once. This works regardless of how many times Shopify attempts delivery. Pair this with application-level idempotency for defense in depth.

Simplified Signature Verification

Webhook authenticity verification is essential for security, but implementing cryptographic signature validation across multiple programming languages and handling edge cases creates unnecessary development overhead and potential security vulnerabilities.

Why It Matters: Without proper signature verification, your webhook endpoints are vulnerable to spoofed requests, replay attacks, and malicious payloads that could compromise application security or corrupt business data. However, implementing HMAC validation correctly requires careful handling of encoding, timing attacks, and various edge cases.

Shopify's Requirements: Every application must implement custom HMAC-SHA256 signature verification using the app's client secret. This means writing cryptographic code, handling base64 encoding, managing secrets securely, and ensuring timing-safe comparison operations across different programming languages and frameworks.

Hookdeck's Streamlined Approach: Hookdeck supports numerous methods of source authentication. When you provide your webhook secret during connection setup, Hookdeck automatically verifies Shopify's signatures before forwarding events. Your application only needs to verify Hookdeck's own signature using its standardized approach, which works identically across all supported webhook platforms. This reduces implementation complexity while improving security consistency.

Enhanced Security and Filtering

Production webhook endpoints need sophisticated security measures beyond basic HTTPS to protect against malicious traffic, DDoS attempts, and unwanted requests that could impact application performance or security.

Why It Matters: Public webhook endpoints are attractive targets for automated attacks, malicious bot traffic, and resource exhaustion attempts. Applications need granular control over which requests are processed and which are blocked before they consume server resources.

Shopify's Basic Protection: The platform requires HTTPS for webhook delivery but provides no additional filtering, rate limiting, or traffic shaping capabilities. Your application endpoints are directly exposed to all incoming traffic.

Hookdeck's Security Layer: Hookdeck terminates all traffic over HTTPS and provides:

  • IP allowlist/blocklist filtering using Transformations and Filters to block suspicious traffic sources
  • Request rate limiting set a delivery rate for your endpoints to prevent overwhelming your application
  • Content filtering using Filters to block malformed or suspicious payloads

Comprehensive Monitoring and Alerting

Webhook failures can silently break critical business processes, making comprehensive observability essential for maintaining reliable integrations and quickly identifying issues before they impact customers.

Why It Matters: When webhook processing fails, the effects often aren't immediately visible to end users but can cascade into significant business problems: missed order notifications, stale inventory levels, or broken automation workflows. Early detection and rich debugging information are crucial for maintaining system reliability.

Shopify's Monitoring: The platform provides basic delivery logs in the admin dashboard showing success/failure status, offers focused debugging information, and sends warning emails to the Partner account's emergency developer email when subscriptions are failing. However, these alerts are limited to critical subscription-level issues rather than individual event failures. Troubleshooting webhook issues benefits from additional context and tools.

Hookdeck's Observability: Hookdeck's monitoring platform includes:

  • Searchable event logs with full payload and metadata retention; search requests, and events and attempts for debugging
  • Real-time metrics tracking success rates, latency, and failure patterns. See Metrics.
  • Manual and bulk retries for testing fixes against retained historical events. See Retries.
  • Customizable alerting via email, Slack, PagerDuty, and webhooks. See Issues & Notifications.

Hookdeck retains requests, events, and delivery attempts for up to 30 days. During this period you can perform manual or bulk retries to support failure management and recovery workflows. See Retries for details on retry limits, strategies, and APIs.

API Version Migration Support

Shopify's API evolution requires careful coordination between old and new webhook formats, making version migrations complex and risky without proper routing and gradual rollout capabilities.

Why It Matters: When Shopify releases new API versions with different webhook schemas, applications must handle the transition carefully to avoid service disruption. The migration process often involves running multiple application versions simultaneously while gradually shifting traffic.

Shopify's Constraints: Webhook subscriptions are locked to the API version at creation time, requiring applications to manage multiple subscriptions and handle different payload formats during migration periods. This creates complex coordination challenges.

Hookdeck's Migration Tools: The routing system enables:

  • Version-based routing to direct webhooks to appropriate application endpoints based on API version using Filters
  • Fan-out delivery to send the same webhook to multiple destinations during migration periods
  • Schema transformation to normalize different API versions into consistent formats using Transformations

Step-by-Step: Setting Up Shopify HTTPS Webhooks with Hookdeck

Create a Hookdeck Account and Get API Key

Sign up for a Hookdeck Event Gateway account and obtain your project API secret from the dashboard under SettingsSecrets.

Create a Hookdeck Connection

Create a Connection that includes a Shopify Source and an HTTP Destination .

First, get your Shopify app's client secret: Before creating the connection, you'll need your app's client secret for webhook signature verification. The Client secret is found the Overview section of your app within the Shopify Partner Dashboard (HomeAppsYOUR APPClient credentialsClient secret).

Next, create a connection that maps the inbound events from Shopify to the endpoint that handles Shopify events. As part of this, you can set:

  • Retry rule to control how Hookdeck retries failed deliveries to your endpoints
  • Deduplication rule configuration to remove duplicate Shopify events from being delivered to your endpoints
  • Destination rate limiting to control the flow of events and prevent overwhelming your application

Here's how to create the connection:

Use the following examples to create a Connection within your Hookdeck project:

curl -X POST "https://api.hookdeck.com/2027-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify-to-Production-App",
    "source": {
      "name": "shopify",
      "type": "SHOPIFY",
      "config": {
        "auth": {
          "webhook_secret_key": "your-app-client-secret"
        }
      }
    },
    "rules": [
      {
        "type": "retry",
        "count": 20,
        "interval": 300,
        "strategy": "exponential"
      },
      {
        "type": "deduplicate",
        "window": 300000,
        "include_fields": ["headers.x-shopify-event-id"]
      }
    ],
    "destination": {
      "name": "Production-App",
      "type": "HTTP",
      "config": {
        "url": "https://yourapp.com/webhooks/shopify"
      }
    }
  }'
const response = await fetch('https://api.hookdeck.com/2027-07-01/connections', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKDECK_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Shopify-to-Production-App',
    source: {
      name: 'shopify',
      type: 'SHOPIFY',
      config: {
        auth: {
          webhook_secret_key: 'your-app-client-secret'
        }
      }
    },
    destination: {
      name: 'Production-App',
      type: 'HTTP',
      config: {
        url: 'https://yourapp.com/webhooks/shopify'
      }
    },
    rules: [
      {
        type: 'retry',
        count: 20,
        interval: 300,
        strategy: 'exponential'
      },
      {
        type: 'deduplicate',
        window: 300000,
        include_fields: ['headers.x-shopify-event-id']
      }
    ]
  })
});

const connection = await response.json();
console.log('Connection created:', connection);
<?php
require_once 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client();

$response = $client->post('https://api.hookdeck.com/2027-07-01/connections', [
    'headers' => [
        'Authorization' => 'Bearer ' . $_ENV['HOOKDECK_API_KEY'],
        'Content-Type' => 'application/json'
    ],
    'json' => [
        'name' => 'Shopify-to-Production-App',
        'source' => [
            'name' => 'shopify',
            'type' => 'SHOPIFY',
            'config' => [
                'auth' => [
                    'webhook_secret_key' => 'your-app-client-secret'
                ]
            ]
        ],
        'destination' => [
            'name' => 'Production-App',
            'type' => 'HTTP',
            'config' => [
                'url' => 'https://yourapp.com/webhooks/shopify'
            ]
        ],
        'rules' => [
            [
                'type' => 'retry',
                'count' => 20,
                'interval' => 300,
                'strategy' => 'exponential'
            ],
            [
                'type' => 'deduplicate',
                'window' => 300000,
                'include_fields' => ['headers.x-shopify-event-id']
            ]
        ]
    ]
]);

$connection = json_decode($response->getBody(), true);
echo 'Connection created: ' . json_encode($connection) . "\n";
?>
require 'faraday'
require 'json'

# Create connection using Faraday
conn = Faraday.new(url: 'https://api.hookdeck.com') do |f|
  f.request :json
  f.response :json
  f.adapter Faraday.default_adapter
end

payload = {
  name: 'Shopify-to-Production-App',
  source: {
    name: 'shopify',
    type: 'SHOPIFY',
    config: {
      auth: {
        webhook_secret_key: 'your-app-client-secret'
      }
    }
  },
  destination: {
    name: 'Production-App',
    type: 'HTTP',
    config: {
      url: 'https://yourapp.com/webhooks/shopify'
    }
  },
  rules: [
    {
      type: 'retry',
      count: 20,
      interval: 300,
      strategy: 'exponential'
    },
    {
      type: 'deduplicate',
      window: 300000,
      include_fields: ['headers.x-shopify-event-id']
    }
  ]
}

response = conn.post('/2027-07-01/connections') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['HOOKDECK_API_KEY']}"
  req.body = payload
end

connection = response.body
puts "Connection created: #{connection}"
import httpx
import asyncio
import os

async def create_connection():
    async with httpx.AsyncClient() as client:
        response = await client.post(
            'https://api.hookdeck.com/2027-07-01/connections',
            headers={
                'Authorization': f'Bearer {os.environ["HOOKDECK_API_KEY"]}',
                'Content-Type': 'application/json'
            },
            json={
                'name': 'Shopify-to-Production-App',
                'source': {
                    'name': 'shopify',
                    'type': 'SHOPIFY',
                    'config': {
                        'auth': {
                            'webhook_secret_key': 'your-app-client-secret'
                        }
                    }
                },
                'destination': {
                    'name': 'Production-App',
                    'type': 'HTTP',
                    'config': {
                        'url': 'https://yourapp.com/webhooks/shopify'
                    }
                },
                'rules': [
                    {
                        'type': 'retry',
                        'count': 20,
                        'interval': 300,
                        'strategy': 'exponential'
                    },
                    {
                        'type': 'deduplicate',
                        'window': 300000,
                        'include_fields': ['headers.x-shopify-event-id']
                    }
                ]
            },
            timeout=30.0
        )
        response.raise_for_status()
        
        connection = response.json()
        print('Connection created:', connection)
        return connection

# Usage
asyncio.run(create_connection())
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

public class HookdeckConnectionCreator {
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    public static void createConnection() throws Exception {
        // Build JSON payload
        ObjectNode payload = objectMapper.createObjectNode();
        payload.put("name", "Shopify-to-Production-App");
        
        ObjectNode source = objectMapper.createObjectNode();
        source.put("name", "shopify");
        source.put("type", "SHOPIFY");
        
        ObjectNode sourceConfig = objectMapper.createObjectNode();
        ObjectNode auth = objectMapper.createObjectNode();
        auth.put("webhook_secret_key", "your-app-client-secret");
        sourceConfig.set("auth", auth);
        source.set("config", sourceConfig);
        payload.set("source", source);
        
        ObjectNode destination = objectMapper.createObjectNode();
        destination.put("name", "Production-App");
        ObjectNode destConfig = objectMapper.createObjectNode();
        destConfig.put("type", "HTTP");
        destConfig.put("url", "https://yourapp.com/webhooks/shopify");
        destination.set("config", destConfig);
        payload.set("destination", destination);
        
        ArrayNode rules = objectMapper.createArrayNode();
        
        ObjectNode retryRule = objectMapper.createObjectNode();
        retryRule.put("type", "retry");
        retryRule.put("count", 20);
        retryRule.put("interval", 300);
        retryRule.put("strategy", "exponential");
        rules.add(retryRule);
        
        ObjectNode timeoutRule = objectMapper.createObjectNode();
        timeoutRule.put("type", "timeout");
        timeoutRule.put("timeout", 30000);
        rules.add(timeoutRule);
        
        ObjectNode dedupeRule = objectMapper.createObjectNode();
        ObjectNode dedupeRule = objectMapper.createObjectNode();
        dedupeRule.put("type", "deduplicate");
        dedupeRule.put("window", 300000);
        ArrayNode includeFields = objectMapper.createArrayNode();
        includeFields.add("headers.x-shopify-event-id");
        dedupeRule.set("include_fields", includeFields);
        rules.add(dedupeRule);
        
        payload.set("rules", rules);
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.hookdeck.com/2027-07-01/connections"))
                .header("Authorization", "Bearer " + System.getenv("HOOKDECK_API_KEY"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
                .timeout(Duration.ofSeconds(30))
                .build();
        
        HttpResponse<String> response = httpClient.send(request,
                HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            System.out.println("Connection created: " + response.body());
        } else {
            throw new RuntimeException("Request failed with status: " + response.statusCode());
        }
    }
    
    public static void main(String[] args) {
        try {
            createConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
using System.Text;
using System.Text.Json;

// Using HttpClient with dependency injection (recommended)
public class HookdeckService
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

    public HookdeckService(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _apiKey = configuration["HOOKDECK_API_KEY"];
        
        _httpClient.BaseAddress = new Uri("https://api.hookdeck.com/2027-07-01/");
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
    }

    public async Task<object> CreateConnectionAsync()
    {
        var payload = new
        {
            name = "Shopify-to-Production-App",
            source = new
            {
                name = "shopify",
                type = "SHOPIFY",
                config = new
                {
                    auth = new
                    {
                        webhook_secret_key = "your-app-client-secret"
                    }
                }
            },
            destination = new
            {
                name = "Production-App",
                type = "HTTP",
                config = new
                {
                    url = "https://yourapp.com/webhooks/shopify"
                }
            },
            rules = new[]
            {
                new { type = "retry", count = 20, interval = 300, strategy = "exponential" },
                new { type = "timeout", timeout = 30000 },
                new { type = "deduplicate", window = 300000, include_fields = new[] { "headers.x-shopify-event-id" } }
            }
        };

        var json = JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        using var response = await _httpClient.PostAsync("connections", content);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Connection created: {result}");
        
        return JsonSerializer.Deserialize<object>(result);
    }
}

// Usage in Program.cs or controller
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<HookdeckService>();

var app = builder.Build();
var hookdeckService = app.Services.GetRequiredService<HookdeckService>();
await hookdeckService.CreateConnectionAsync();
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type ConnectionPayload struct {
    Name        string      `json:"name"`
    Source      Source      `json:"source"`
    Destination Destination `json:"destination"`
    Rules       []Rule      `json:"rules"`
}

type Source struct {
    Name   string       `json:"name"`
    Type   string       `json:"type"`
    Config SourceConfig `json:"config"`
}

type SourceConfig struct {
    Auth Auth `json:"auth"`
}

type Auth struct {
    WebhookSecretKey string `json:"webhook_secret_key"`
}

type Destination struct {
    Name   string          `json:"name"`
    Type   string          `json:"type"`
    Config DestinationConfig `json:"config"`
}

type DestinationConfig struct {
    URL string `json:"url"`
}

type Rule struct {
    Type     string `json:"type"`
    Count    *int   `json:"count,omitempty"`
    Interval *int   `json:"interval,omitempty"`
    Strategy *string `json:"strategy,omitempty"`
    Timeout  *int   `json:"timeout,omitempty"`
    Window       *int      `json:"window,omitempty"`
    IncludeFields *[]string `json:"include_fields,omitempty"`
}

func createConnection() error {
    exponential := "exponential"
    retryCount := 20
    timeout := 30000
    window := 300000
    includeFields := []string{"headers.x-shopify-event-id"}

    payload := ConnectionPayload{
        Name: "Shopify-to-Production-App",
        Source: Source{
            Name: "shopify",
            Type: "SHOPIFY",
            Config: SourceConfig{
                Auth: Auth{
                    WebhookSecretKey: "your-app-client-secret",
                },
            },
        },
        Destination: Destination{
            Name: "Production-App",
            Type: "HTTP",
            Config: DestinationConfig{
                URL: "https://yourapp.com/webhooks/shopify",
            },
        },
        Rules: []Rule{
            {
                Type:     "retry",
                Count:    &retryCount,
                Interval: &interval,
                Strategy: &exponential,
            },
            {
                Type:    "timeout",
                Timeout: &timeout,
            },
            {
                Type:          "deduplicate",
                Window:        &window,
                IncludeFields: &includeFields,
            },
        },
    }
    interval := 300
    timeout := 30000
    payload := ConnectionPayload{
        Name: "Shopify-to-Production-App",
        Source: Source{
            Name: "shopify",
            Type: "SHOPIFY",
            Config: SourceConfig{
                Auth: Auth{
                    WebhookSecretKey: "your-app-client-secret",
                },
            },
        },
        Destination: Destination{
            Name: "Production-App",
            Type: "HTTP",
            Config: DestinationConfig{
                URL: "https://yourapp.com/webhooks/shopify",
            },
        },
        Rules: []Rule{
            {
                Type:     "retry",
                Count:    &retryCount,
                Interval: &interval,
                Strategy: &exponential,
            },
            {
                Type:    "timeout",
                Timeout: &timeout,
            },
            {
                Type:          "deduplicate",
                Window:        &window,
                IncludeFields: &includeFields,
            },
        },
    }
    
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("error marshaling JSON: %v", err)
    }
    
    client := &http.Client{
        Timeout: 30 * time.Second,
    }
    
    req, err := http.NewRequest("POST", "https://api.hookdeck.com/2027-07-01/connections", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("error creating request: %v", err)
    }
    
    req.Header.Set("Authorization", "Bearer "+os.Getenv("HOOKDECK_API_KEY"))
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("error reading response: %v", err)
    }
    
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
    }
    
    fmt.Printf("Connection created: %s\n", string(body))
    return nil
}

func main() {
    if err := createConnection(); err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }
}

The connection resource will contain a source.url property that is the URL of your Shopify source (e.g., https://hkdk.events/src_123abc).

Navigate to Connections in your dashboard and click Create Connection.

Set Source Name: shopify and Source type: Shopify.

Set Destination Name: Production-App, Destination type of HTTP, and Destination URL: https://yourapp.com/webhooks/shopify.

Add Rules: Retry Rule (Exponential, 20 attempts, 300 second interval) and Deduplication Rule (5 minute window, include field headers.x-shopify-event-id).

In Source Authentication, enter your app's client secret from the Partner Dashboard and click Create.

The connection will be created and your source URL will be displayed (e.g., https://hkdk.events/src_123abc).

Create Shopify Webhook with Hookdeck Source URL

Use the source URL from step 2 to create your Shopify webhook.

Use the following examples to create a webhook subscription using your Hookdeck source URL:

curl -X POST "https://your-shop.myshopify.com/admin/api/2024-01/graphql.json" \
  -H "X-Shopify-Access-Token: YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { webhookSubscription { id callbackUrl } userErrors { field message } } }",
    "variables": {
      "topic": "ORDERS_CREATE",
      "webhookSubscription": {
        "callbackUrl": "https://hkdk.events/src_123abc",
        "format": "JSON"
      }
    }
  }'
const response = await fetch('https://your-shop.myshopify.com/admin/api/2024-01/graphql.json', {
  method: 'POST',
  headers: {
    'X-Shopify-Access-Token': process.env.SHOPIFY_ACCESS_TOKEN,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    query: `
      mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
        webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
          webhookSubscription {
            id
            callbackUrl
          }
          userErrors {
            field
            message
          }
        }
      }
    `,
    variables: {
      topic: 'ORDERS_CREATE',
      webhookSubscription: {
        callbackUrl: 'https://hkdk.events/src_123abc',
        format: 'JSON'
      }
    }
  })
});

const result = await response.json();
console.log('Webhook created:', result);
<?php
require_once 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client();

$response = $client->post('https://your-shop.myshopify.com/admin/api/2024-01/graphql.json', [
    'headers' => [
        'X-Shopify-Access-Token' => $_ENV['SHOPIFY_ACCESS_TOKEN'],
        'Content-Type' => 'application/json'
    ],
    'json' => [
        'query' => '
          mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
            webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
              webhookSubscription {
                id
                callbackUrl
              }
              userErrors {
                field
                message
              }
            }
          }
        ',
        'variables' => [
            'topic' => 'ORDERS_CREATE',
            'webhookSubscription' => [
                'callbackUrl' => 'https://hkdk.events/src_123abc',
                'format' => 'JSON'
            ]
        ]
    ]
]);

$result = json_decode($response->getBody(), true);
echo 'Webhook created: ' . json_encode($result) . "\n";
?>
require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://your-shop.myshopify.com') do |f|
  f.request :json
  f.response :json
  f.adapter Faraday.default_adapter
end

payload = {
  query: '
    mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
      webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
        webhookSubscription {
          id
          callbackUrl
        }
        userErrors {
          field
          message
        }
      }
    }
  ',
  variables: {
    topic: 'ORDERS_CREATE',
    webhookSubscription: {
      callbackUrl: 'https://hkdk.events/src_123abc',
      format: 'JSON'
    }
  }
}

response = conn.post('/admin/api/2024-01/graphql.json') do |req|
  req.headers['X-Shopify-Access-Token'] = ENV['SHOPIFY_ACCESS_TOKEN']
  req.body = payload
end

result = response.body
puts "Webhook created: #{result}"
import httpx
import asyncio
import os

async def create_webhook():
    async with httpx.AsyncClient() as client:
        response = await client.post(
            'https://your-shop.myshopify.com/admin/api/2024-01/graphql.json',
            headers={
                'X-Shopify-Access-Token': os.environ['SHOPIFY_ACCESS_TOKEN'],
                'Content-Type': 'application/json'
            },
            json={
                'query': '''
                  mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
                    webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
                      webhookSubscription {
                        id
                        callbackUrl
                      }
                      userErrors {
                        field
                        message
                      }
                    }
                  }
                ''',
                'variables': {
                    'topic': 'ORDERS_CREATE',
                    'webhookSubscription': {
                        'callbackUrl': 'https://hkdk.events/src_123abc',
                        'format': 'JSON'
                    }
                }
            }
        )
        
        result = response.json()
        print('Webhook created:', result)

# Run the async function
asyncio.run(create_webhook())
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class ShopifyWebhookCreator {
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    public static void createWebhook() throws Exception {
        ObjectNode payload = objectMapper.createObjectNode();
        
        String query = "mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { webhookSubscription { id callbackUrl } userErrors { field message } } }";
        payload.put("query", query);
        
        ObjectNode variables = objectMapper.createObjectNode();
        variables.put("topic", "ORDERS_CREATE");
        
        ObjectNode webhookSubscription = objectMapper.createObjectNode();
        webhookSubscription.put("callbackUrl", "https://hkdk.events/src_123abc");
        webhookSubscription.put("format", "JSON");
        variables.set("webhookSubscription", webhookSubscription);
        
        payload.set("variables", variables);
        
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://your-shop.myshopify.com/admin/api/2024-01/graphql.json"))
                .header("X-Shopify-Access-Token", System.getenv("SHOPIFY_ACCESS_TOKEN"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
                .timeout(Duration.ofSeconds(30))
                .build();
        
        HttpResponse<String> response = httpClient.send(request,
                HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            System.out.println("Webhook created: " + response.body());
        } else {
            throw new RuntimeException("Request failed with status: " + response.statusCode());
        }
    }
    
    public static void main(String[] args) {
        try {
            createWebhook();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
using System.Text;
using System.Text.Json;

public class ShopifyWebhookService
{
    private readonly HttpClient _httpClient;
    private readonly string _accessToken;

    public ShopifyWebhookService(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _accessToken = configuration["SHOPIFY_ACCESS_TOKEN"];
    }

    public async Task<object> CreateWebhookAsync(string shopDomain)
    {
        var payload = new
        {
            query = "mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { webhookSubscription { id callbackUrl } userErrors { field message } } }",
            variables = new
            {
                topic = "ORDERS_CREATE",
                webhookSubscription = new
                {
                    callbackUrl = "https://hkdk.events/src_123abc",
                    format = "JSON"
                }
            }
        };

        var json = JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        using var request = new HttpRequestMessage(HttpMethod.Post, $"https://{shopDomain}.myshopify.com/admin/api/2024-01/graphql.json")
        {
            Content = content
        };
        request.Headers.Add("X-Shopify-Access-Token", _accessToken);

        using var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Webhook created: {result}");
        
        return JsonSerializer.Deserialize<object>(result);
    }
}

// Usage
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<ShopifyWebhookService>();

var app = builder.Build();
var webhookService = app.Services.GetRequiredService<ShopifyWebhookService>();
await webhookService.CreateWebhookAsync("your-shop");
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

type WebhookPayload struct {
    Query     string    `json:"query"`
    Variables Variables `json:"variables"`
}

type Variables struct {
    Topic              string             `json:"topic"`
    WebhookSubscription WebhookSubscription `json:"webhookSubscription"`
}

type WebhookSubscription struct {
    CallbackURL string `json:"callbackUrl"`
    Format      string `json:"format"`
}

func createWebhook() error {
    payload := WebhookPayload{
        Query: "mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) { webhookSubscription { id callbackUrl } userErrors { field message } } }",
        Variables: Variables{
            Topic: "ORDERS_CREATE",
            WebhookSubscription: WebhookSubscription{
                CallbackURL: "https://hkdk.events/src_123abc",
                Format:      "JSON",
            },
        },
    }
    
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("error marshaling JSON: %v", err)
    }
    
    client := &http.Client{
        Timeout: 30 * time.Second,
    }
    
    req, err := http.NewRequest("POST", "https://your-shop.myshopify.com/admin/api/2024-01/graphql.json", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("error creating request: %v", err)
    }
    
    req.Header.Set("X-Shopify-Access-Token", os.Getenv("SHOPIFY_ACCESS_TOKEN"))
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("error reading response: %v", err)
    }
    
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
    }
    
    fmt.Printf("Webhook created: %s\n", string(body))
    return nil
}

func main() {
    if err := createWebhook(); err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }
}

1. Configure webhooks in your shopify.app.toml file:

[webhooks]
subscriptions = [
  { topics = ["orders/create"], uri = "https://hkdk.events/src_123abc" },
  { topics = ["orders/updated"], uri = "https://hkdk.events/src_123abc" }
]

2. Deploy your app:

shopify app deploy

Handle the Webhook and Verify Signatures

With Hookdeck handling Shopify signature verification, your application only needs to verify Hookdeck's signature. This simplifies your code and provides a consistent verification method across all webhook platforms.

js
const crypto = require('crypto');
const express = require('express');
const app = express();

const HOOKDECK_WEBHOOK_SECRET = process.env.HOOKDECK_WEBHOOK_SECRET;

app.use(
  express.json({
    // Store the rawBody buffer on the request
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  }),
);

app.post("/webhooks/shopify", async (req, res) => {
  // Extract x-hookdeck-signature and x-hookdeck-signature-2 headers from the request
  const hmacHeader = req.get("x-hookdeck-signature");
  const hmacHeader2 = req.get("x-hookdeck-signature-2");

  // Create a hash based on the raw body
  const hash = crypto
    .createHmac("sha256", HOOKDECK_WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest("base64");

  // Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
  if (hash === hmacHeader || (hmacHeader2 && hash === hmacHeader2)) {
    console.log("Webhook is originating from Hookdeck");
    
    // Parse webhook data
    const shopifyEvent = req.body;
    const eventType = req.headers['x-shopify-topic'];
    
    console.log(`Received ${eventType} event:`, shopifyEvent);
    
    // Process the webhook
    switch (eventType) {
      case 'orders/create':
        handleOrderCreated(shopifyEvent);
        break;
      case 'orders/updated':
        handleOrderUpdated(shopifyEvent);
        break;
      default:
        console.log(`Unhandled event type: ${eventType}`);
    }
    
    res.sendStatus(200);
  } else {
    console.log("Signature is invalid, rejected");
    res.sendStatus(403);
  }
});

function handleOrderCreated(order) {
  console.log(`New order #${order.order_number} for ${order.total_price}`);
  // Your order processing logic here
}

function handleOrderUpdated(order) {
  console.log(`Order #${order.order_number} updated`);
  // Your order update logic here
}

app.listen(3000, () => {
  console.log('Shopify webhook handler listening on port 3000');
});
php
<?php
// Hookdeck webhook secret from environment
$hookdeck_webhook_secret = $_ENV['HOOKDECK_WEBHOOK_SECRET'];

// Get request data
$headers = getallheaders();
$signature = $headers['x-hookdeck-signature'] ?? '';
$signature2 = $headers['x-hookdeck-signature-2'] ?? '';
$payload = file_get_contents('php://input');

// Create a hash based on the raw body
$hash = base64_encode(hash_hmac('sha256', $payload, $hookdeck_webhook_secret, true));

// Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
if ($hash === $signature || ($signature2 && $hash === $signature2)) {
    error_log("Webhook is originating from Hookdeck");
    
    // Parse webhook data
    $shopify_event = json_decode($payload, true);
    $event_type = $headers['x-shopify-topic'] ?? '';
    
    error_log("Received $event_type event: " . json_encode($shopify_event));
    
    // Process the webhook
    switch ($event_type) {
        case 'orders/create':
            handleOrderCreated($shopify_event);
            break;
        case 'orders/updated':
            handleOrderUpdated($shopify_event);
            break;
        default:
            error_log("Unhandled event type: $event_type");
    }
    
    // Return success response
    http_response_code(200);
    echo json_encode(['success' => true]);
} else {
    error_log("Signature is invalid, rejected");
    http_response_code(403);
    echo json_encode(['error' => 'Invalid signature']);
}

function handleOrderCreated($order) {
    error_log("New order #{$order['order_number']} for \${$order['total_price']}");
    // Your order processing logic here
}

function handleOrderUpdated($order) {
    error_log("Order #{$order['order_number']} updated");
    // Your order update logic here
}
?>
ruby
require 'sinatra'
require 'json'
require 'openssl'
require 'base64'

# Hookdeck webhook secret from environment
HOOKDECK_WEBHOOK_SECRET = ENV['HOOKDECK_WEBHOOK_SECRET']

post '/webhooks/shopify' do
  # Get raw payload for signature verification
  payload = request.body.read
  request.body.rewind
  
  # Get signature headers
  signature = request.env['HTTP_X_HOOKDECK_SIGNATURE']
  signature2 = request.env['HTTP_X_HOOKDECK_SIGNATURE_2']
  
  # Create a hash based on the raw body
  hash = Base64.encode64(
    OpenSSL::HMAC.digest('sha256', HOOKDECK_WEBHOOK_SECRET, payload)
  ).strip
  
  # Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
  if hash == signature || (signature2 && hash == signature2)
    puts "Webhook is originating from Hookdeck"
    
    # Parse webhook data
    shopify_event = JSON.parse(payload)
    event_type = request.env['HTTP_X_SHOPIFY_TOPIC']
    
    puts "Received #{event_type} event: #{shopify_event}"
    
    # Process the webhook
    case event_type
    when 'orders/create'
      handle_order_created(shopify_event)
    when 'orders/updated'
      handle_order_updated(shopify_event)
    else
      puts "Unhandled event type: #{event_type}"
    end
    
    { success: true }.to_json
  else
    puts "Signature is invalid, rejected"
    halt 403, { error: 'Invalid signature' }.to_json
  end
end

def handle_order_created(order)
  puts "New order ##{order['order_number']} for $#{order['total_price']}"
  # Your order processing logic here
end

def handle_order_updated(order)
  puts "Order ##{order['order_number']} updated"
  # Your order update logic here
end
python
import base64
import hashlib
import hmac
import json
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

# Hookdeck webhook secret from your environment
HOOKDECK_WEBHOOK_SECRET = os.environ.get('HOOKDECK_WEBHOOK_SECRET')

@app.route('/webhooks/shopify', methods=['POST'])
def handle_shopify_webhook():
    # Get signature headers
    signature = request.headers.get('x-hookdeck-signature')
    signature2 = request.headers.get('x-hookdeck-signature-2')
    
    # Get raw payload
    payload = request.get_data()
    
    # Create a hash based on the raw body
    hash_value = base64.b64encode(
        hmac.new(
            HOOKDECK_WEBHOOK_SECRET.encode(),
            payload,
            hashlib.sha256
        ).digest()
    ).decode()
    
    # Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
    if hash_value == signature or (signature2 and hash_value == signature2):
        print("Webhook is originating from Hookdeck")
        
        # Get webhook data
        shopify_event = request.get_json()
        event_type = request.headers.get('x-shopify-topic')
        
        print(f"Received {event_type} event: {shopify_event}")
        
        # Process the webhook
        if event_type == 'orders/create':
            handle_order_created(shopify_event)
        elif event_type == 'orders/updated':
            handle_order_updated(shopify_event)
        else:
            print(f"Unhandled event type: {event_type}")
        
        return jsonify({'success': True}), 200
    else:
        print("Signature is invalid, rejected")
        return jsonify({'error': 'Invalid signature'}), 403

def handle_order_created(order):
    print(f"New order #{order['order_number']} for ${order['total_price']}")
    # Your order processing logic here

def handle_order_updated(order):
    print(f"Order #{order['order_number']} updated")
    # Your order update logic here

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000, debug=True)
java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Value;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;

@RestController
@RequestMapping("/webhooks")
public class ShopifyWebhookController {

    @Value("${HOOKDECK_WEBHOOK_SECRET}")
    private String hookdeckWebhookSecret;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/shopify")
    public ResponseEntity<?> handleShopifyWebhook(
            @RequestHeader(value = "x-hookdeck-signature", required = false) String signature,
            @RequestHeader(value = "x-hookdeck-signature-2", required = false) String signature2,
            @RequestHeader("x-shopify-topic") String eventType,
            @RequestBody String payload,
            HttpServletRequest request) throws IOException {

        // Create a hash based on the raw body
        String hash = createSignature(payload);
        
        // Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
        if (hash.equals(signature) || (signature2 != null && hash.equals(signature2))) {
            System.out.println("Webhook is originating from Hookdeck");
            
            // Parse webhook data
            JsonNode shopifyEvent = objectMapper.readTree(payload);
            
            System.out.println("Received " + eventType + " event: " + payload);

            // Process the webhook
            switch (eventType) {
                case "orders/create":
                    handleOrderCreated(shopifyEvent);
                    break;
                case "orders/updated":
                    handleOrderUpdated(shopifyEvent);
                    break;
                default:
                    System.out.println("Unhandled event type: " + eventType);
            }

            return ResponseEntity.ok(Map.of("success", true));
        } else {
            System.out.println("Signature is invalid, rejected");
            return ResponseEntity.status(403).body(Map.of("error", "Invalid signature"));
        }
    }

    private String createSignature(String payload) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                hookdeckWebhookSecret.getBytes(StandardCharsets.UTF_8),
                "HmacSHA256"
            );
            mac.init(secretKeySpec);
            
            byte[] hashBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hashBytes);
            
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            System.err.println("Error creating signature: " + e.getMessage());
            return null;
        }
    }

    private void handleOrderCreated(JsonNode order) {
        String orderNumber = order.get("order_number").asText();
        String totalPrice = order.get("total_price").asText();
        System.out.println("New order #" + orderNumber + " for $" + totalPrice);
        // Your order processing logic here
    }

    private void handleOrderUpdated(JsonNode order) {
        String orderNumber = order.get("order_number").asText();
        System.out.println("Order #" + orderNumber + " updated");
        // Your order update logic here
    }
}
csharp
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;

public class WebhookService
{
    private readonly string _hookdeckWebhookSecret;

    public WebhookService(IConfiguration configuration)
    {
        _hookdeckWebhookSecret = configuration["HOOKDECK_WEBHOOK_SECRET"];
    }

    public async Task<bool> ProcessWebhookAsync(HttpRequest request)
    {
        using var reader = new StreamReader(request.Body);
        string payload = await reader.ReadToEndAsync();
        
        string signature = request.Headers["x-hookdeck-signature"];
        string signature2 = request.Headers["x-hookdeck-signature-2"];
        
        // Create a hash based on the raw body
        string hash = CreateSignature(payload);
        
        // Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
        if (hash == signature || (!string.IsNullOrEmpty(signature2) && hash == signature2))
        {
            Console.WriteLine("Webhook is originating from Hookdeck");
            
            // Parse webhook data
            var shopifyEvent = JsonSerializer.Deserialize<JsonElement>(payload);
            var eventType = request.Headers["x-shopify-topic"];

            Console.WriteLine($"Received {eventType} event: {payload}");

            // Process the webhook
            switch (eventType)
            {
                case "orders/create":
                    HandleOrderCreated(shopifyEvent);
                    break;
                case "orders/updated":
                    HandleOrderUpdated(shopifyEvent);
                    break;
                default:
                    Console.WriteLine($"Unhandled event type: {eventType}");
                    break;
            }

            return true;
        }
        else
        {
            Console.WriteLine("Signature is invalid, rejected");
            return false;
        }
    }

    private string CreateSignature(string payload)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_hookdeckWebhookSecret));
        var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return Convert.ToBase64String(hashBytes);
    }

    private void HandleOrderCreated(JsonElement order)
    {
        var orderNumber = order.GetProperty("order_number").GetString();
        var totalPrice = order.GetProperty("total_price").GetString();
        Console.WriteLine($"New order #{orderNumber} for ${totalPrice}");
        // Your order processing logic here
    }

    private void HandleOrderUpdated(JsonElement order)
    {
        var orderNumber = order.GetProperty("order_number").GetString();
        Console.WriteLine($"Order #{orderNumber} updated");
        // Your order update logic here
    }
}
go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type ShopifyEvent map[string]interface{}

var hookdeckWebhookSecret = os.Getenv("HOOKDECK_WEBHOOK_SECRET")

func createSignature(payload string) string {
    mac := hmac.New(sha256.New, []byte(hookdeckWebhookSecret))
    mac.Write([]byte(payload))
    return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func handleShopifyWebhook(w http.ResponseWriter, r *http.Request) {
    // Read the payload
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()
    
    payload := string(body)
    signature := r.Header.Get("x-hookdeck-signature")
    signature2 := r.Header.Get("x-hookdeck-signature-2")
    
    // Create a hash based on the raw body
    hash := createSignature(payload)
    
    // Compare the created hash with the value of the x-hookdeck-signature and x-hookdeck-signature-2 headers
    if hash == signature || (signature2 != "" && hash == signature2) {
        fmt.Println("Webhook is originating from Hookdeck")
        
        // Parse webhook data
        var shopifyEvent ShopifyEvent
        if err := json.Unmarshal(body, &shopifyEvent); err != nil {
            http.Error(w, "Error parsing JSON", http.StatusBadRequest)
            return
        }
        
        eventType := r.Header.Get("x-shopify-topic")
        fmt.Printf("Received %s event: %v\n", eventType, shopifyEvent)
        
        // Process the webhook
        switch eventType {
        case "orders/create":
            handleOrderCreated(shopifyEvent)
        case "orders/updated":
            handleOrderUpdated(shopifyEvent)
        default:
            fmt.Printf("Unhandled event type: %s\n", eventType)
        }
        
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"success": true}`))
    } else {
        fmt.Println("Signature is invalid, rejected")
        http.Error(w, `{"error": "Invalid signature"}`, http.StatusForbidden)
    }
}

func handleOrderCreated(order ShopifyEvent) {
    orderNumber := order["order_number"]
    totalPrice := order["total_price"]
    fmt.Printf("New order #%v for $%v\n", orderNumber, totalPrice)
    // Your order processing logic here
}

func handleOrderUpdated(order ShopifyEvent) {
    orderNumber := order["order_number"]
    fmt.Printf("Order #%v updated\n", orderNumber)
    // Your order update logic here
}

func main() {
    http.HandleFunc("/webhooks/shopify", handleShopifyWebhook)
    
    fmt.Println("Shopify webhook handler listening on port 3000")
    if err := http.ListenAndServe(":3000", nil); err != nil {
        fmt.Printf("Error starting server: %v\n", err)
        os.Exit(1)
    }
}

Test Shopify Webhooks Locally with Hookdeck CLI

Forward events to your local development environment:

Install the Hookdeck CLI.

  npm install hookdeck-cli -g
  
  
  yarn global add hookdeck-cli
  
  
    brew install hookdeck/hookdeck/hookdeck
    
    
  1.     scoop bucket add hookdeck https://github.com/hookdeck/scoop-hookdeck-cli.git
        
        
  2.   scoop install hookdeck
      
      
  1. Download the latest release's tar.gz file.

  2.     tar -xvf hookdeck_X.X.X_linux_x86_64.tar.gz
        
        
  3.   ./hookdeck
      
      

Login and listen for webhook events:

# authenticate CLI
hookdeck login

# Forward events to local development server
hookdeck listen 3000 shopify

Running hookdeck listen creates a second connection consisting of the Shopify Source and a new CLI Destination and allows you to test your webhook handling code locally without affecting your production environment.

CLI destinations do not support delivery rate limiting.

Bookmark Events for Testing

Use Hookdeck's bookmarking feature to save specific Shopify events for testing and debugging. When developing your webhook handlers, bookmark representative events (orders, customer updates, inventory changes) to create a library of test scenarios. This allows you to replay real Shopify payloads during development without triggering new events from your store.

Configure Webhook Delivery Alerts with Issue Notifications

The Hookdeck Event Gateway will automatically create Issues for failed webhook deliveries. You will receive email notifications whenever there are issues with your webhook deliveries.

Issue typeBehavior
DeliveryA delivery issue is opened when an attempt results in failures, according to the connection's issue trigger. Delivery issues display a histogram of the associated events, as well as the option to bulk retry all the associated failures.
TransformationA transformation issue is opened when one of your transformations has failed to properly apply, either due to uncaught exceptions, thrown errors, or invalid data.
BackpressureA transformation issue is caused when the estimated queue time (600,000ms, or 10 minutes, by default) exceeds the configured delay threshold.

You can also configure issues notifications to:

  • Trigger webhooks
  • Create PagerDuty incidents

For more information, see the issue notifications documentation.

Monitor and Analyze Shopify Events

Monitor your Shopify webhook events to maintain integration health and quickly identify issues.

Event Flow Overview

Hookdeck processes webhooks through three stages: Request (initial HTTP from Shopify) → Event (processed webhook data) → Attempt (delivery tries to your endpoint). This flow helps trace issues from source to delivery. See Events documentation for details.

Search and Filter Events

Use the Events dashboard to focus on Shopify-specific monitoring:

Key Filters:

  • Status: Failed, Successful, Pending, On hold
  • Source: Select your Shopify connection
  • Time Range: Analyze specific periods
  • Headers: Filter by X-Shopify-Topic (event type) or X-Shopify-Shop-Domain (store)

For advanced filtering and payload searches, see Event filtering documentation.

Event Inspection

Shopify Headers to Monitor:

  • X-Shopify-Topic: Event type (orders/create, customers/update)
  • X-Shopify-Shop-Domain: Source shop (useful for multi-store setups)
  • X-Shopify-Event-Id: Unique event identifier (used for deduplication)
  • X-Shopify-API-Version: API version used

Common Issues:

  • 401/403: Authentication problems
  • Timeout: Exceeds 60-second delivery window
  • 5xx Errors: Application server issues
  • Connection Refused: Network connectivity problems

Custom Views

Create custom event views for focused monitoring:

  1. Navigate to Events
  2. Apply filters (e.g., Status: Failed, search for order-related webhooks)
  3. Save as "Critical Shopify Events"

For multi-store setups, create separate views filtered by X-Shopify-Shop-Domain.

Key Metrics

Monitor these essential KPIs on your Sources, Connections, and Destinations pages:

  • Success Rate: Target >99% for critical webhooks
  • Response Latency: Benchmark <5 seconds
  • Volume Trends: Monitor "Events rate" for traffic patterns
  • Queue Depth: Watch "Pending events" for backpressure

Set up issue triggers for automated alerts. For detailed metrics analysis, see Metrics documentation.

Best Practices

Daily Monitoring:

  1. Check failed events from overnight
  2. Verify response times for critical webhooks (orders, payments)
  3. Review error patterns

Issue Response:

  1. Inspect event details for root cause
  2. Check if issues affect multiple events
  3. Monitor recovery after fixes
  4. Use manual retries to test fixes

For comprehensive monitoring strategies, see the Observability guide.

Retry Shopify Events

Retry events for errors discovered in production, or for testing and debugging:

Use the following examples to retry a specific event by ID:

# Retry specific event by ID
curl -X POST "https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY"
const response = await fetch('https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKDECK_API_KEY}`
  }
});

const result = await response.json();
console.log('Event retried:', result);
<?php
require_once 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client();

$response = $client->post('https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry', [
    'headers' => [
        'Authorization' => 'Bearer ' . $_ENV['HOOKDECK_API_KEY']
    ]
]);

$result = json_decode($response->getBody(), true);
echo 'Event retried: ' . json_encode($result) . "\n";
?>
require 'faraday'
require 'json'

conn = Faraday.new(url: 'https://api.hookdeck.com') do |f|
  f.request :json
  f.response :json
  f.adapter Faraday.default_adapter
end

response = conn.post('/2027-07-01/events/evt_789ghi/retry') do |req|
  req.headers['Authorization'] = "Bearer #{ENV['HOOKDECK_API_KEY']}"
end

result = response.body
puts "Event retried: #{result}"
import httpx
import asyncio
import os

async def retry_event():
    async with httpx.AsyncClient() as client:
        response = await client.post(
            'https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry',
            headers={
                'Authorization': f'Bearer {os.environ["HOOKDECK_API_KEY"]}'
            }
        )
        
        result = response.json()
  print('Event retried:', result)

# Run the async function
asyncio.run(retry_event())
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;

public class EventRetry {
    private static final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    
  public static void retryEvent() throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry"))
                .header("Authorization", "Bearer " + System.getenv("HOOKDECK_API_KEY"))
                .POST(HttpRequest.BodyPublishers.noBody())
                .timeout(Duration.ofSeconds(30))
                .build();
        
        HttpResponse<String> response = httpClient.send(request,
                HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            System.out.println("Event retried: " + response.body());
        } else {
            throw new RuntimeException("Request failed with status: " + response.statusCode());
        }
    }
    
    public static void main(String[] args) {
        try {
            retryEvent();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
using System.Text.Json;

public class EventRetryService
{
    private readonly HttpClient _httpClient;
    private readonly string _apiKey;

  public EventRetryService(HttpClient httpClient, IConfiguration configuration)
    {
        _httpClient = httpClient;
        _apiKey = configuration["HOOKDECK_API_KEY"];
        
        _httpClient.BaseAddress = new Uri("https://api.hookdeck.com/2027-07-01/");
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
    }

  public async Task<object> RetryEventAsync(string eventId)
    {
  using var response = await _httpClient.PostAsync($"events/{eventId}/retry", null);
        response.EnsureSuccessStatusCode();
        
        var result = await response.Content.ReadAsStringAsync();
  Console.WriteLine($"Event retried: {result}");
        
        return JsonSerializer.Deserialize<object>(result);
    }
}

// Usage
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient<EventRetryService>();

var app = builder.Build();
var retryService = app.Services.GetRequiredService<EventRetryService>();
await retryService.RetryEventAsync("evt_789ghi");
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

func retryEvent() error {
    client := &http.Client{
        Timeout: 30 * time.Second,
    }
    
  req, err := http.NewRequest("POST", "https://api.hookdeck.com/2027-07-01/events/evt_789ghi/retry", nil)
    if err != nil {
        return fmt.Errorf("error creating request: %v", err)
    }
    
    req.Header.Set("Authorization", "Bearer "+os.Getenv("HOOKDECK_API_KEY"))
    
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("error making request: %v", err)
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("error reading response: %v", err)
    }
    
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body))
    }
    
  fmt.Printf("Event retried: %s\n", string(body))
    return nil
}

func main() {
  if err := retryEvent(); err != nil {
        fmt.Printf("Error: %v\n", err)
        os.Exit(1)
    }
}

Go to Events in your dashboard and find the event you want to retry.

Click the Retry button next to the event.

The event will be resent to your destination with the same payload and headers.

Advanced Shopify Webhook Patterns with Hookdeck

Multi-Tenant Routing

Route webhooks to different applications based on shop domain by creating multiple connections. Each connection uses a filter rule that identifies the shop domain.

For example, to route webhooks from different Shopify stores to different destinations:

Connection 1 - Premium Store:

{
  "x-shopify-shop-domain": "premium-store.myshopify.com"
}

Connection 2 - Basic Store:

{
  "x-shopify-shop-domain": "basic-store.myshopify.com"
}

Connection 3 - Enterprise Store with advanced filtering:

{
  "$and": [
    {
      "x-shopify-shop-domain": "enterprise-store.myshopify.com"
    },
    {
      "x-shopify-topic": {
        "$in": "orders"
      }
    }
  ]
}

Each connection would have a different destination URL corresponding to the appropriate application instance for that tenant.

The complete syntax for filters can be found in the Filters documentation.

Fan-Out Pattern

Send the same webhook to multiple destinations by creating multiple connections that use the same Source :

# Create first connection
curl -X POST "https://api.hookdeck.com/2027-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify-to-App1",
    "source_id": "src_123abc",
    "destination": {
      "name": "App1",
      "type": "http",
      "config": {
        "url": "https://app1.com/webhooks"
      }
    }
  }'

# Create second connection  
curl -X POST "https://api.hookdeck.com/2027-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify-to-Analytics",
    "source_id": "src_123abc",
    "destination": {
      "name": "Analytics",
      "type": "http",
      "config": {
        "url": "https://analytics.com/webhooks"
      }
    }
  }'

Payload Transformation

Shopify webhook payloads can be complex and contain more data than your downstream services need. Payload transformation allows you to optimize these webhooks for your specific use case.

Key Transformation Benefits:

  • Simplify payloads: Remove unnecessary fields to reduce bandwidth and processing overhead
  • Standardize data formats: Convert Shopify's string prices to numbers for easier mathematical operations
  • Extract key information: Pull out only the essential data points your application needs
  • Ensure compatibility: Reformat payloads to match your existing API schemas
  • Add computed fields: Include derived values like order totals or customer segments

Transform Shopify payloads before delivery:

// Simplify order payload for downstream services
addHandler("transform", (request, context) => {
  const webhook = request.body;
  
  return {
    ...request,
    body: {
      "order_id": webhook.id,
      "customer_email": webhook.customer.email,
      "total": parseFloat(webhook.total_price),
      "currency": webhook.currency,
      "line_items": webhook.line_items.map(item => ({
        "product_id": item.product_id,
        "quantity": item.quantity,
        "price": parseFloat(item.price)
      }))
    }
  };
});

For more information, see Transformations.

Plan API Version Migrations

Use multiple connections with a {ref entity="Filter"} based on the Shopify API version to handle API version upgrades:

# Create connection for new API version
curl -X POST "https://api.hookdeck.com/2027-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify-to-App-v2",
    "source_id": "src_123abc",
    "destination": {
      "name": "App-v2",
      "config": {
        "url": "https://yourapp.com/webhooks/v2"
      }
    },
    "rules": [
      {
        "type": "filter",
        "filter": {
          "headers": {
            "x-shopify-api-version": "2024-01"
          }
        }
      }
    ]
  }'

# Keep connection for legacy versions
curl -X POST "https://api.hookdeck.com/2027-07-01/connections" \
  -H "Authorization: Bearer $HOOKDECK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shopify-to-App-v1",
    "source_id": "src_123abc",
    "destination": {
      "name": "App-v1",
      "config": {
        "url": "https://yourapp.com/webhooks/v1"
      }
    },
    "rules": [
      {
        "type": "filter",
        "filter": {
          "headers": {
            "x-shopify-api-version": "2023-10"
          }
        }
      }
    ]
  }'

Implement Idempotency

Even with Deduplication enabled, implement idempotency in your application:

const processedEvents = new Set();

app.post('/webhooks/shopify', verifyHookdeckSignature, (req, res) => {
  const eventId = req.headers['x-hookdeck-eventid'];
  
  // Check if already processed
  if (processedEvents.has(eventId)) {
    console.log(`Event ${eventId} already processed, skipping`);
    return res.status(200).json({ success: true });
  }
  
  // Process event
  processEvent(req.body);
  
  // Mark as processed
  processedEvents.add(eventId);
  
  res.status(200).json({ success: true });
});

Conclusion and Next Steps

Hookdeck helps teams operate Shopify HTTPS webhooks reliably at scale. By implementing Shopify's recommended best practices, including deduplication, Hookdeck removes much of the complexity of building reliable webhook infrastructure from scratch.

This comprehensive approach ensures your webhook system meets production requirements while providing the operational guarantees needed for business-critical integrations.

Key benefits of using Hookdeck with Shopify webhooks

  • Zero webhook loss through persistent queuing and guaranteed ingestion
  • Configurable deduplication using Shopify's unique webhook event identifiers to prevent duplicate processing
  • Simplified signature verification with one implementation for all platforms
  • Extended processing time with 60-second delivery windows instead of Shopify's 5-second limit
  • Comprehensive monitoring and alerting for proactive issue resolution
  • Flexible retry policies that preserve webhook subscriptions and extend beyond Shopify's limits

Next Steps

  1. Start with the setup guide to implement your first Shopify webhook connection
  2. Explore advanced patterns for multi-tenant routing and payload transformation
  3. Implement monitoring strategies to maintain system health and performance
  4. Review high-volume scenarios for scaling your webhook infrastructure

By following this guide, you'll have a production-ready Shopify webhook system that can handle your business requirements while providing the reliability and observability needed for long-term success.

  • Advanced retry policies that surpass Shopify's default behavior
  • Comprehensive monitoring and debugging tools for production operations
  • Flexible routing and transformation for complex integration patterns

Ready to get started?

  1. Sign up for Hookdeck and create your first Shopify source
  2. Install the CLI for local development and testing
  3. Read the full documentation for advanced configuration options
  4. Join the community for support and best practice discussions

With Hookdeck handling the reliability concerns, you can focus on building exceptional Shopify applications that scale with confidence.


Author picture

Phil Leggetter

Head of Developer Relations

Phil is Head of Developer Relations at Hookdeck with expertise in real-time web technologies, co-author of 'Realtime Web Apps', and creator of the AAARRRP Developer Relations strategy framework.