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'

# 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: ...}}
shopify_app_api_key_overwrite CharField Per-shop API key override
shopify_app_api_secret_overwrite CharField Per-shop API secret override

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 overwrite or global setting
shop.shopify_app_api_secret   # Returns overwrite 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:

from shopify_app.decorators import app_proxy_view

@app_proxy_view
def my_proxy_view(request, *args, **kwargs):
    shop = kwargs['shop']

@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.2.4.tar.gz (17.4 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.2.4-py3-none-any.whl (21.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: django_shopify_app-2.2.4.tar.gz
  • Upload date:
  • Size: 17.4 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.2.4.tar.gz
Algorithm Hash digest
SHA256 65fd1bb61c62ad2f4a64065eace552e2e1bb57074068e7589d0bd7c903617896
MD5 0ad3f48bce6894c4549f765a21560b36
BLAKE2b-256 6a8cfe9f81e17d0ccaef1697576446838ec6073750a63351faae3f92ccb142d4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for django_shopify_app-2.2.4-py3-none-any.whl
Algorithm Hash digest
SHA256 145a33f040a719016fbcf4d8e7c29b087e1e06ef0795003dc7dd8a869fcb80e8
MD5 e6feb4935c030bc58861fcad5f0d1fb5
BLAKE2b-256 e5b892cbbb3f8e58efd481ba3e8d5bac43d9ce06825b2236b23bcd26ff45d6c1

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