Skip to main content

A django app with all the tools required to make a Shopify app

Project description

django-shopify-app

A reusable Django package for building Shopify apps. Handles OAuth, token exchange, webhook management, session token validation (JWT), and Shopify API interactions.

Installation

pip install django-shopify-app

Add the app in settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'shopify_app',
    'shops',
]

Settings

# Required
SHOPIFY_API_KEY = config('SHOPIFY_API_KEY')
SHOPIFY_API_SECRET = config('SHOPIFY_API_SECRET')
SHOPIFY_APP_HOST = 'https://your-app.com'
SHOPIFY_SHOP_MODEL = 'shops.Shop'              # Must inherit from ShopBase
SHOPIFY_WEBHOOK_CALLBACK = 'shops.webhooks.webhook_entry'
SHOPIFY_GDPR_WEBHOOK_CALLBACK = 'shops.webhooks.gdpr_webhook_entry'

# Optional
SHOPIFY_APP_SCOPES = ['read_products', 'read_orders']
SHOPIFY_WEBHOOK_TOPICS = ['products/update', 'app/uninstalled']
SHOPIFY_WEBHOOK_HOST = 'https://your-app.com'  # Defaults to SHOPIFY_APP_HOST
SHOPIFY_API_VERSION = '2025-04'                # Defaults to '2022-04'

# Token exchange (recommended for embedded apps)
SHOPIFY_TOKEN_EXCHANGE = False                 # Default: False
SHOPIFY_DASHBOARD_PATH = '/dashboard'          # Default: '/dashboard'

# Per-shop credential overrides (replaces DB overwrite fields)
SHOPIFY_CREDENTIALS_OVERRIDES = {              # Default: {}
    'shop.myshopify.com': {
        'api_key': '...',
        'api_secret': '...',
    },
}

# Staff bypass
SHOPIFY_STAFF_BYPASS = False                   # Default: False
SHOPIFY_STAFF_BYPASS_METHODS = None            # Default: None (all methods)
SHOPIFY_STAFF_SHOP_ATTR = 'admin_shop'         # Default: 'admin_shop'

ShopBase model

Your app must define a Shop model that inherits from ShopBase:

from shopify_app.models import ShopBase

class Shop(ShopBase):
    # Add your custom fields
    plan_name = models.CharField(max_length=50, default='')

    def installed(self, request=None):
        """Called when the app is installed on a shop for the first time."""
        self.update_webhooks()
        self.crm_on_install()

    def on_user_login(self, user_data, request=None):
        """Called when a Shopify user opens the app.

        user_data contains: id, first_name, last_name, email,
        email_verified, account_owner, locale, collaborator
        """
        self.crm_on_login(user_data)

Fields

Field Type Description
shopify_domain CharField The shop's .myshopify.com domain
shopify_token CharField Offline access token
access_scopes CharField Granted OAuth scopes
seen_users JSONField Tracks user logins {user_id: {last_login: ...}}

API methods

All REST methods replace api_version in the path automatically:

# REST API
shop.get('/admin/api/api_version/products.json')
shop.post('/admin/api/api_version/products.json', data={...})
shop.put('/admin/api/api_version/products/123.json', data={...})
shop.patch('/admin/api/api_version/products/123.json', data={...})
shop.delete_request('/admin/api/api_version/products/123.json')

# GraphQL
response = shop.graphql(query='{ shop { name } }')
response = shop.graphql(query=mutation, variables={...}, api_version='2025-04')

# GraphQL via shopify library (uses session context manager)
result = shop.graph(operation_name='GetShop', variables={}, operations_document=query)

Webhook management

shop.update_webhooks()     # Deactivate all + reactivate from SHOPIFY_WEBHOOK_TOPICS
shop.activate_webhooks()   # Alias for update_webhooks()
shop.deactivate_webhooks() # Remove all registered webhooks

CRM events

Built-in hooks for CRM lifecycle events (via django-crm-events):

shop.crm_on_install()
shop.crm_on_uninstall(users=[])
shop.crm_on_login(user_data)
shop.crm_on_billing_plan_change(plan_price)

Other properties

shop.shopify_app_api_key      # Returns per-shop override or global setting
shop.shopify_app_api_secret   # Returns per-shop override or global setting
shop.api_version              # From SHOPIFY_API_VERSION setting
shop.host                     # Base64-encoded admin URL for App Bridge
shop.get_shop_data()          # Fetch shop details from Shopify REST API

HMAC validation

shop.app_proxy_request_is_valid(request)     # Validate app proxy HMAC
shop.request_shopify_hmac_is_valid(request)   # Validate standard Shopify HMAC

Authorization

The package supports two authorization flows: token exchange (recommended for embedded apps) and authorization code grant (legacy / non-embedded apps).

Token exchange (recommended)

Token exchange eliminates OAuth redirects. The backend exchanges the session token from App Bridge for an access token via a server-side POST to Shopify. No page reloads or flicker.

Scopes are managed via shopify.app.toml and deployed with Shopify CLI (shopify app deploy). Shopify handles installation and scope updates automatically.

Add to settings.py:

SHOPIFY_TOKEN_EXCHANGE = True           # Enable token exchange
SHOPIFY_DASHBOARD_PATH = '/dashboard'   # Where to redirect from app root

Set up your URLs:

from django.urls import path, include
from shopify_app.views import AppRootView

urlpatterns = [
    path('', AppRootView.as_view()),
    path('shopify/', include('shopify_app.urls')),
    # your dashboard urls...
]

When a merchant opens your app, AppRootView redirects to the dashboard. The first API request from the dashboard triggers token exchange automatically via ShopSessionMixin / shop_session, storing the access token for subsequent requests.

User login tracking is handled automatically: on the first request from a new user (or after 24 hours), an online token exchange fetches user details and fires shop.on_user_login().

Authorization code grant (legacy)

For non-embedded apps or apps that don't use Shopify managed installation.

from django.urls import path
from shopify_app.views import AppRootView, EndTokenRequestView

app_name = 'my_shopify_app'

urlpatterns = [
    path(
        '',
        AppRootView.as_view(
            redirect_path_name='my_shopify_app:end-token-request',
        ),
    ),
    path(
        'confirm/',
        EndTokenRequestView.as_view(
            redirect_path_name='embed_admin:dashboard',
        ),
        name='end-token-request'
    ),
]

With SHOPIFY_TOKEN_EXCHANGE = False (default), AppRootView falls back to the OAuth authorization code grant flow.

Webhook URLs

Include the package URLs for webhook and GDPR endpoints:

from django.urls import path, include

urlpatterns = [
    path('shopify/', include('shopify_app.urls')),
]

This registers:

  • shopify/webhooks — Main webhook receiver
  • shopify/gdpr-webhooks/customer-data-request — GDPR customer data request
  • shopify/gdpr-webhooks/customer-data-erasure — GDPR customer data erasure
  • shopify/gdpr-webhooks/shop-data-erasure — GDPR shop data erasure

ShopSessionMixin

A mixin that authenticates requests against a valid Shopify shop session (JWT). Use it with any APIView or DRF generic view:

from rest_framework.views import APIView
from shopify_app.mixins import ShopSessionMixin

class MyView(ShopSessionMixin, APIView):
    def get(self, request, *args, **kwargs):
        shop = request.shop
        ...

Staff bypass

Staff users can skip Shopify JWT validation if they have a shop associated with their user model. Enable it globally in settings:

SHOPIFY_STAFF_BYPASS = True  # Default: False
SHOPIFY_STAFF_BYPASS_METHODS = ['GET', 'HEAD', 'OPTIONS']  # Default: None (all methods)
SHOPIFY_STAFF_SHOP_ATTR = 'admin_shop'  # Default: 'admin_shop'

SHOPIFY_STAFF_BYPASS_METHODS restricts which HTTP methods are allowed through the bypass. When None (default), all methods are allowed. When set, unlisted methods (e.g. POST, PUT, DELETE) will require Shopify JWT validation even for staff users.

Or per-view:

class MyView(ShopSessionMixin, APIView):
    allow_staff_bypass = True  # Overrides the global setting

When enabled, if the request user is authenticated, is staff, and has a truthy value on the configured attribute (admin_shop by default), the mixin sets request.shop from that attribute and skips JWT validation.

Decorators

@shop_session

Validates the Shopify session token (JWT) from the request header. Injects shop, shopify_domain, and shopify_user_id into the view kwargs:

from shopify_app.decorators import shop_session

@shop_session
def my_view(request, *args, **kwargs):
    shop = kwargs['shop']
    user_id = kwargs['shopify_user_id']

@shopify_embed

Adds Content-Security-Policy frame-ancestors header for embedded app views:

from shopify_app.decorators import shopify_embed

@shopify_embed
def my_view(request, **kwargs):
    ...

@known_shop_required

Validates that a shop query parameter is present and the shop exists in the database. Returns 401 if invalid:

from shopify_app.decorators import known_shop_required

@known_shop_required
def my_view(request, *args, **kwargs):
    shop = kwargs['shop']

@app_proxy_view

Validates HMAC for Shopify app proxy requests. Fetches the shop from the database:

from shopify_app.decorators import app_proxy_view

@app_proxy_view
def my_proxy_view(request, *args, **kwargs):
    shop = kwargs['shop']                          # Shop model instance (from DB)
    domain = kwargs['shopify_domain']              # e.g. 'example.myshopify.com'
    customer_id = kwargs['logged_in_customer_id']  # Customer ID or None

@app_proxy_view_lite

Validates HMAC without hitting the database. Use this when you don't need the full shop record:

from shopify_app.decorators import app_proxy_view_lite

@app_proxy_view_lite
def my_proxy_view(request, *args, **kwargs):
    domain = kwargs['shopify_domain']              # e.g. 'example.myshopify.com'
    customer_id = kwargs['logged_in_customer_id']  # Customer ID or None
    # No kwargs['shop'] — use get_shop() if you need the DB record

@latest_access_scopes_required

Checks if the shop's stored scopes match the configured scopes. Sets scope_changes_required=True in kwargs if they differ:

from shopify_app.decorators import latest_access_scopes_required

@shop_session
@latest_access_scopes_required
def my_view(request, *args, **kwargs):
    if kwargs.get('scope_changes_required'):
        # handle scope update

Utilities

from shopify_app.utils import (
    get_shop_model,          # Resolve shop model from SHOPIFY_SHOP_MODEL setting
    get_shop,                # Get shop by domain (raises DoesNotExist)
    get_auth_shop,           # Get shop or unsaved instance for auth lookup
    webhook_request_is_valid, # Validate webhook HMAC
    liquid_render,           # Render template with Liquid content-type
    app_proxy_url_builder,   # Build full URL for app proxy requests
    app_proxy_redirect,      # Redirect to app proxy URL
)

Management commands

# Activate webhooks for a shop by subdomain
python manage.py activate_shop_webhooks <shopify_subdomain>
# e.g. activate_shop_webhooks example-store → targets example-store.myshopify.com

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_shopify_app-2.3.0.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_shopify_app-2.3.0-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

Details for the file django_shopify_app-2.3.0.tar.gz.

File metadata

  • Download URL: django_shopify_app-2.3.0.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for django_shopify_app-2.3.0.tar.gz
Algorithm Hash digest
SHA256 41d3781bc6336267f48712ca5ab3a7bf04a0f5c34c088ce591a8fb59ceae01c8
MD5 1fdb5669d034c68c82926e732e9e9f03
BLAKE2b-256 9a9301685933ef418c6baa9af08bb7f09b508093acf171cec62aeb28501adf64

See more details on using hashes here.

File details

Details for the file django_shopify_app-2.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_shopify_app-2.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ffaf3094b0b2860a6ad22e933064b581ca6d959d440eb4d565c8479a19b49321
MD5 548e134ea6414fcf86cf6c5702e69fb4
BLAKE2b-256 68a6c4e13630ab836157d1096c2ae17a7057f789a9904246931bb999aed67f9d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page