---
title: "Receive Shopify webhooks on Flask API | WebhookRelay"
meta:
  "og:description": "How to get Shopify webhooks working on your local Flask app, with proper signature verification"
  "og:title": "Receive Shopify webhooks on Flask API"
  description: "How to get Shopify webhooks working on your local Flask app, with proper signature verification"
---

![Stripes](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/images/stripes.svg)

# **Receive Shopify webhooks on Flask API**

How to get Shopify webhooks working on your local Flask app, with proper signature verification

If you're building a Shopify integration, you'll need to receive webhooks. The problem is Shopify can't reach your laptop. This post shows how to use Webhook Relay to forward webhooks to your local Flask app, with proper HMAC verification so you don't get burned by fake requests in production.

## [What you'll need](#what-youll-need)

- A [Webhook Relay account](https://my.webhookrelay.com/register)
- Python 3.8+
- The [relay CLI](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/docs/installation/cli)

## [Set up the Flask project](#set-up-the-flask-project)

I'm using `uv` here because it's fast, but pip works fine too.

```
curl -LsSf https://astral.sh/uv/install.sh | sh

mkdir flask-webhook-server
cd flask-webhook-server

uv venv
source .venv/bin/activate
uv pip install flask
```

## [The Flask app](#the-flask-app)

Here's the full app. The important part is verifying the HMAC signature before you trust anything in the payload. Shopify signs every webhook with your store's secret, and you should check it.

Create `app.py`:

```
import hmac
import hashlib
import base64
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

# Grab this from Shopify admin: Settings > Notifications > Webhooks
# Don't commit it to your repo
SHOPIFY_WEBHOOK_SECRET = os.environ.get('SHOPIFY_WEBHOOK_SECRET', '')

def verify_shopify_webhook(data: bytes, hmac_header: str) -> bool:
    """Check if the webhook actually came from Shopify."""
    if not SHOPIFY_WEBHOOK_SECRET:
        print("ERROR: SHOPIFY_WEBHOOK_SECRET not set. Please set it:")
        print("  export SHOPIFY_WEBHOOK_SECRET='your-secret-from-shopify-admin'")
        return False
    
    calculated_hmac = base64.b64encode(
        hmac.new(
            SHOPIFY_WEBHOOK_SECRET.encode('utf-8'),
            data,
            hashlib.sha256
        ).digest()
    ).decode('utf-8')
    
    return hmac.compare_digest(calculated_hmac, hmac_header)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Get raw body BEFORE parsing JSON - you need the exact bytes for HMAC
    data = request.get_data()
    
    hmac_header = request.headers.get('X-Shopify-Hmac-Sha256', '')
    topic = request.headers.get('X-Shopify-Topic', 'unknown')
    shop_domain = request.headers.get('X-Shopify-Shop-Domain', 'unknown')
    
    if not verify_shopify_webhook(data, hmac_header):
        print(f"Bad signature from {shop_domain}")
        return jsonify({"error": "Invalid signature"}), 401
    
    payload = request.get_json()
    
    print(f"Got {topic} webhook from {shop_domain}")
    print(f"Payload: {payload}")
    
    # Do something with it
    if topic == 'orders/create':
        print(f"New order #{payload.get('order_number')} - ${payload.get('total_price')}")
    
    return jsonify({"status": "received"}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=4444, debug=True)
```

A couple things to note:

- You have to read the raw request body before Flask parses it. The HMAC is computed over the exact bytes Shopify sent.
- `hmac.compare_digest` prevents timing attacks. Regular string comparison leaks information about which characters matched.

## [Run the server](#run-the-server)

```
export SHOPIFY_WEBHOOK_SECRET="your-secret-from-shopify-admin"
python app.py
```

You'll see:

```
 * Running on http://0.0.0.0:4444
```

## [Forward webhooks with Webhook Relay](#forward-webhooks-with-webhook-relay)

Open another terminal and run:

```
relay login -k your-token-key -s your-token-secret
relay forward -b shopify-webhooks http://localhost:4444/webhook
```

You'll get a public URL like `https://xxx.hooks.webhookrelay.com`. Copy that.

## [Configure Shopify](#configure-shopify)

1. Go to your Shopify admin
2. Settings > Notifications > Webhooks
3. Copy the signing secret at the top (that's your `SHOPIFY_WEBHOOK_SECRET`)
4. Click "Create webhook"
5. Pick an event, set format to JSON, paste your Webhook Relay URL
6. Save

## [Test it](#test-it)

You can use Shopify's "Send test notification" button, or hit it with curl:

```
curl -X POST https://xxx.hooks.webhookrelay.com \
  -H "Content-Type: application/json" \
  -H "X-Shopify-Topic: orders/create" \
  -H "X-Shopify-Shop-Domain: your-store.myshopify.com" \
  -d '{"id": 12345, "order_number": "1001", "total_price": "99.99"}'
```

Your Flask server should print the payload.

## [Going to production](#going-to-production)

For production, you'll want:

- `gunicorn` instead of the Flask dev server
- The Webhook Relay agent running as a service (see our [Docker guide](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/docs/installation/docker))
- Or just point the Webhook Relay output directly at your public server