django-dynamic-qr
GitHub Get Started
v1.0.0 MIT License Django 4.2+ Python 3.10+

The complete dynamic QR
toolkit for Django

A self-hosted, production-ready package that gives you everything a commercial QR platform offers — 14 content types, short-URL redirects, scan analytics, access control, and a REST API — fully owned inside your own Django project.

Why django-dynamic-qr

Commercial QR platforms charge monthly fees to do something fundamentally simple: redirect a static-looking QR code through a short URL you control, then log who scanned it. This package gives you that entire feature set as plain Django models, views, and a REST API — no subscriptions, no vendor lock-in, no external dependency on uptime you don't control.

14 QR Types
URL, vCard, vCard+, PDF, Menu, Social Links, WiFi, Email, SMS, WhatsApp, Image Gallery, Plain Text, Link List, App Store.
Built-in Analytics
Scan count, device, OS, browser, country & city geolocation, daily trend charts, CSV/XLSX export.
Access Control
Password protection, daily scan limits, scheduled activation/expiry, and country allow-lists.
REST API
Full Django REST Framework viewsets — create, update destinations dynamically, export, analyze, bulk import.

How it works

Every dynamic QR code encodes a short URL pointing back to your Django app — /qr/<slug>/. When scanned, that view runs access checks, logs the scan, then resolves to its destination. Because the printed QR pattern never changes, you can edit where it points to at any time.

scan flow
# Scan → short URL → access guard → scan log → destination
GET /qr/AbC123Xy/
  → AccessGuard.check()        # password, schedule, geo, scan limit
  → ScanLogger.log()           # device, geo, referer
  → handler.respond(request)   # redirect or landing page
Free package, free foreverSince this lives inside your own Django project, there are no per-QR pricing tiers or scan caps beyond what your own database can handle.
GET STARTED

Installation

Add django-dynamic-qr to an existing Django project in under five minutes.

1. Install the package

terminalCopy
# Core package
pip install django-dynamic-qr

# With REST API support
pip install django-dynamic-qr[api]

# With Celery async scan logging
pip install django-dynamic-qr[celery]

2. Add to INSTALLED_APPS

settings.pyCopy
INSTALLED_APPS = [
    ...
    "django_dynamic_qr",
    "rest_framework",  # optional — only if using the REST API
]

3. Mount the URLs

project/urls.pyCopy
from django.urls import path, include

urlpatterns = [
    ...
    path("", include("django_dynamic_qr.urls")),
]

This mounts /qr/<slug>/ for redirects, /qr/<slug>/export/ for image downloads, and /api/qr/... for the REST API (if DRF is installed).

4. Run migrations

terminalCopy
python manage.py migrate django_dynamic_qr
Media settings requiredLogo uploads need MEDIA_URL / MEDIA_ROOT configured, and your urls.py must serve media in development.
GET STARTED

Quickstart

Create your first dynamic QR code and watch its destination update without reprinting anything.

Create a QR code

python shellCopy
from django_dynamic_qr.models import QRCode

qr = QRCode.objects.create(
    user=request.user,
    name="My Website",
    qr_type="url",
    type_data={"url": "https://example.com"},
    fg_color="#111111",
    bg_color="#FFFFFF",
)

print(qr.get_redirect_url())
# → https://yourdomain.com/qr/AbC123Xy/

Export the image

python shellCopy
from django_dynamic_qr.engine.exporter import export_qr_code

png_bytes = export_qr_code(qr, fmt="png")
svg_bytes = export_qr_code(qr, fmt="svg")

Or fetch it directly via the built-in view:

http
GET  /qr/AbC123Xy/export/?fmt=svg

Update the destination — no reprint needed

This is the entire point of a dynamic QR code. The printed pattern, slug, and image stay byte-for-byte identical — only the database row changes.

python shellCopy
qr.type_data = {"url": "https://example.com/new-page"}
qr.save()
# Anyone scanning the same printed code now lands on the new URL.
That's the whole workflowCreate once, print once, update forever. Continue to QR Types to see the full payload schema for each of the 14 supported types.
GET STARTED

Configuration

All settings are optional with sane defaults — override only what you need in a single dict.

settings.pyCopy
DJANGO_DYNAMIC_QR = {
    "SHORT_URL_BASE": "https://yourdomain.com",
    "DEFAULT_ERROR_CORRECTION": "H",
    "DEFAULT_BOX_SIZE": 10,
    "DEFAULT_BORDER": 4,
    "LOGO_MAX_RATIO": 0.25,
    "SCAN_LOG_ASYNC": False,
    "GEO_BACKEND": "ip-api",
    "MAXMIND_DB_PATH": None,
    "ENABLE_REST_API": True,
    "UPLOAD_TO": "qr_codes/",
    "LOGO_UPLOAD_TO": "qr_logos/",
}

Settings reference

KeyDefaultDescription
SHORT_URL_BASE""Production domain used to build redirect URLs. Set this before deploying.
DEFAULT_ERROR_CORRECTION"H"L / M / Q / H — higher tolerates more logo overlay and damage.
DEFAULT_BOX_SIZE10Pixel size of each QR module in PNG output.
LOGO_MAX_RATIO0.25Maximum proportion of the QR area a center logo may occupy.
SCAN_LOG_ASYNCFalseOffload scan logging to a Celery task instead of inline writes.
GEO_BACKEND"ip-api"ip-api · maxmind · None — IP geolocation provider for scan logs.
ENABLE_REST_APITrueMount DRF viewsets at /api/qr/ when djangorestframework is installed.
CORE CONCEPTS

Architecture

A modular package structure where every QR content type is a pluggable handler.

package layout
django_dynamic_qr/
├── models/          # QRCode, ScanLog, Folder
├── types/           # 14 type handler classes + registry
├── engine/          # QR matrix generation, exporters
├── analytics/        # geo lookup, scan logger, CSV/XLSX export
├── access/          # password, schedule, scan-limit, geo guards
├── bulk/            # CSV importer, ZIP exporter
├── api/             # DRF serializers + viewsets
├── views/            # redirect view, export view
└── management/       # generate_qr command

Request lifecycle

Every scan passes through three stages before resolving to its final content:

  1. Access Guard — checks password, schedule, daily scan limit, and geo allow-list, in that order.
  2. Scan Logger — records IP-derived geo, device, OS, browser, and referer (sync or async via Celery).
  3. Type Handler — either issues an HTTP redirect (URL, PDF, WiFi) or renders a hosted landing page (vCard+, Menu, Social Links).

The handler pattern

Every QR type is a subclass of BaseQRType registered in a central dict. This is the extension point used in Custom QR Types.

types/base.py
class BaseQRType:
    type_id: str = None
    is_static: bool = False
    has_landing_page: bool = False

    def get_destination(self) -> str: ...
    def to_qr_content(self) -> str: ...
    def respond(self, request): ...
CORE CONCEPTS

Models

Three models drive the entire package: QRCode, ScanLog, and Folder.

QRCode

The central model. Holds identity, design, type-specific JSON payload, and access-control fields.

FieldTypeNotes
slugSlugFieldAuto-generated, URL-safe, unique — the printed QR content.
qr_typeCharFieldOne of the 14 registered type IDs.
type_dataJSONFieldPer-type payload — see QR Types for schema.
fg_color / bg_colorCharFieldHex colors for the QR matrix.
logoImageFieldOptional center logo overlay.
error_correctionCharFieldL / M / Q / H.
passwordCharFieldBlank = no password gate.
scan_limit_dayPositiveIntegerFieldNull = unlimited.
allowed_countriesJSONFieldISO 3166-1 alpha-2 list, e.g. ["IN","US"].

ScanLog

One row per scan, foreign-keyed to QRCode, indexed on (qr_code, scanned_at) and country for fast analytics queries.

models/scan_log.py
scanned_at, ip_address, country, city,
device, os, browser, referer, user_agent

Folder

Simple per-user grouping with a color tag, used in the admin and API for organizing large QR collections.

CORE CONCEPTS

QR Types

14 built-in content handlers, each with its own type_data payload schema.

urlURL
vcardvCard
vcard_plusvCard+
pdfPDF
menuRestaurant Menu
socialSocial Links
wifiWiFi
emailEmail
smsSMS
whatsappWhatsApp
imageImage Gallery
textPlain Text
link_listLink List
app_storeApp Store

Payload reference

Typetype_data keys
urlurl
vcardfirst_name, last_name, phone, email, company, title, address, website
vcard_plussame as vcard + photo_url, bio, socials
pdfpdf_url, title
menurestaurant_name, logo_url, categories[]
socialtitle, bio, avatar_url, links[]
wifissid, password, security, hidden
emailto, subject, body
smsphone, message
whatsappphone, message
imagetitle, images[]
texttext
link_listtitle, description, links[]
app_storeios_url, android_url, fallback_url

Static vs. dynamic types

Most types encode a short URL and resolve server-side. A few — WiFi, Email, SMS, WhatsApp, Plain Text — are marked is_static = True and encode their payload directly into the QR matrix, since their content is meant to work fully offline.

vCard+, Menu, Social Links, Image Gallery, Link Listrender hosted landing pages instead of redirecting — these set has_landing_page = True on their handler.
CORE CONCEPTS

Design Engine

Custom colors, logo overlays, and multi-format export powered by qrcode and segno.

Export formats

FormatEngineUse case
PNGqrcode + PillowDefault, supports logo overlay.
SVGsegnoInfinitely scalable, ideal for print.
PDFsegnoDirect embed into print-ready documents.
EPSsegnoProfessional print/publishing workflows.

Logos are centered and automatically scaled to LOGO_MAX_RATIO of the QR area (default 25%), paired with H error correction so the code stays scannable despite the overlay.

engine/generator.py
def generate_qr(
    content: str,
    fg_color: str = "#000000",
    bg_color: str = "#FFFFFF",
    logo_path: str = None,
    fmt: str = "png",
) -> bytes: ...

Error correction levels

LevelRecoveryRecommended for
L~7%Clean digital display, no logo.
M~15%General use.
Q~25%Print with minor wear expected.
H~30%Default — required for logo overlays.
CORE CONCEPTS

Analytics

Every scan is logged with device, geography, and referer data — exportable to CSV or XLSX.

What's captured per scan

  • Timestamp, IP address (used only for geo lookup, not stored raw beyond the field)
  • Country & city via ip-api (free) or MaxMind GeoLite2 (self-hosted DB)
  • Device class — mobile / tablet / desktop, parsed via user-agents
  • OS and browser family
  • HTTP referer, when present

Sync vs. async logging

By default scans are logged inline on the request. Set SCAN_LOG_ASYNC: True with Celery configured to offload writes to a background task and keep redirects fast under load.

tasks.py
@shared_task(ignore_result=True)
def log_scan_task(qr_pk: int, data: dict):
    qr = QRCode.objects.get(pk=qr_pk)
    ScanLog.objects.create(qr_code=qr, **data)

Exporting scan logs

python shellCopy
from django_dynamic_qr.analytics.exporters import export_scan_csv, export_scan_xlsx

response = export_scan_csv(qr)    # HttpResponse, CSV
response = export_scan_xlsx(qr)   # HttpResponse, styled XLSX
CORE CONCEPTS

Access Control

Four independent guards run in sequence before a scan is logged or resolved.

Password
Set qr.password. Shows an interstitial password page on scan.
Schedule
active_from / active_until datetimes gate availability.
Scan limit
scan_limit_day caps scans per calendar day, counted from ScanLog.
Geo fence
allowed_countries restricts access by IP-derived country code.

Evaluation order

Guards run as: activeschedulegeoscan limitpassword. The first failing guard returns a dedicated denial page (e.g. expired.html, scan_limit.html) and halts the request before any scan is logged.

access/guards.py
qr.password = "secret123"
qr.scan_limit_day = 500
qr.active_until = timezone.now() + timedelta(days=30)
qr.allowed_countries = ["IN", "US", "GB"]
qr.save()
GUIDES

Bulk Operations

Import QR codes from CSV, export selected sets as a ZIP archive.

CSV import

Required columns: name, qr_type, and a data column containing a JSON string of the type payload.

import.csv
name,qr_type,data
My Website,url,"{""url"": ""https://example.com""}"
Office WiFi,wifi,"{""ssid"": ""OfficeNet"", ""password"": ""pass123""}"
python shell
from django_dynamic_qr.bulk import import_from_csv

created, errors = import_from_csv(csv_file, user=request.user)

ZIP export

python shell
from django_dynamic_qr.bulk import export_zip

response = export_zip(qr_codes, fmt="svg")
GUIDES

Django Admin

A fully wired admin interface with live previews and bulk actions out of the box.

Features

  • Inline live QR thumbnail preview rendered as base64 PNG
  • Inline scan log table per QR code (last 20 scans)
  • Bulk actions: export PNG/ZIP, export SVG ZIP, export scan CSV, activate/deactivate
  • Collapsible access-control fieldset to keep the form uncluttered
admin.py
@admin.register(QRCode)
class QRCodeAdmin(admin.ModelAdmin):
    list_display = ["name", "qr_type", "is_active", "total_scans", "qr_preview"]
    actions = [export_png, export_svg, export_scans_csv, activate_qr, deactivate_qr]
    inlines = [ScanLogInline]
GUIDES

Custom QR Types

Register your own handler class for content types not covered out of the box.

myapp/qr_types.pyCopy
from django_dynamic_qr.types.base import BaseQRType
from django_dynamic_qr.types import registry

class CalendarEventType(BaseQRType):
    type_id = "calendar_event"
    is_static = True

    def get_destination(self):
        d = self.data
        return f"BEGIN:VEVENT\nSUMMARY:{d['title']}\nDTSTART:{d['start']}\nEND:VEVENT"

registry["calendar_event"] = CalendarEventType

Import this module inside your app's AppConfig.ready() so the registration runs at Django startup.

API REFERENCE

API Overview

A full Django REST Framework viewset mounted at /api/qr/, requiring authentication.

Authentication

Uses session or token authentication depending on your REST_FRAMEWORK settings. All endpoints are scoped to request.user — users only ever see their own QR codes.

Base URL

https://yourdomain.com/api/qr/
API REFERENCE

QR Codes

CRUD operations on QR code resources.

GET /api/qr/

List all QR codes owned by the authenticated user. Supports ?folder= and ?type= query filters.

POST /api/qr/

Create a new QR code.

request body
{
  "name": "My Website",
  "qr_type": "url",
  "type_data": { "url": "https://example.com" },
  "fg_color": "#111111"
}
GET /api/qr/<id>/

Retrieve a single QR code.

PATCH /api/qr/<id>/

Update fields — including type_data to change the redirect destination dynamically.

DELETE /api/qr/<id>/

Permanently remove a QR code and its associated scan logs.

GET /api/qr/<id>/export/?fmt=png

Download the QR image. fmt accepts png, svg, pdf, eps.

API REFERENCE

Analytics Endpoints

Pull scan summaries and raw logs for a given QR code.

GET /api/qr/<id>/analytics/

Returns total scans, today's scan count, top countries, top devices, and a 30-day daily trend.

response
{
  "total_scans": 1842,
  "scans_today": 37,
  "top_countries": [["IN", 1203], ["US", 410]],
  "top_devices": [["mobile", 1601], ["desktop", 241]],
  "daily_counts": [["2026-06-28", 52], ...]
}
GET /api/qr/<id>/analytics/export/?fmt=csv

Download raw scan logs. fmt accepts csv or xlsx.

API REFERENCE

Bulk Endpoints

Import and export many QR codes in a single request.

POST /api/qr/bulk-import/

Multipart upload with a file field containing a CSV. Returns {"created": N, "errors": [...]}.

GET /api/qr/bulk-export/?fmt=png&id=1&id=2

Returns a ZIP of QR images for the given IDs (or all owned QR codes if no id params are passed).

RESOURCES

CLI Commands

Management commands for scripting and CI workflows.

generate_qr

Export a QR code image by slug, directly from the command line.

terminalCopy
python manage.py generate_qr AbC123Xy --fmt svg --output qr.svg
FlagDefaultDescription
slugRequired positional argument.
--fmtpngpng / svg / pdf / eps
--outputstdoutFile path to save to.
RESOURCES

FAQ

Common questions about django-dynamic-qr.

Does updating a QR code's destination require reprinting?

No — that's the entire premise of a dynamic QR code. The printed pattern encodes a short URL, not the destination itself, so editing type_data changes where scans land instantly.

Can I self-host geolocation instead of using ip-api.com?

Yes — set GEO_BACKEND: "maxmind" and point MAXMIND_DB_PATH at a local GeoLite2 City database file.

Is Celery required?

No. It's entirely optional — install the celery extra and set SCAN_LOG_ASYNC: True only if you want scan logging offloaded from the request/response cycle.

Can I add my own QR content type?

Yes — see Custom QR Types. Subclass BaseQRType and register it in the type registry.

What error correction level should I use with a logo?

H (≈30% recovery) is the default and recommended minimum when overlaying a center logo, since the logo physically obscures part of the matrix.

RESOURCES

Creator

django-dynamic-qr is built and maintained by Rahul Baberwal.

Rahul Baberwal
Rahul Baberwal
Python Backend Developer & AI Engineer · Backend Developer at Groww Per Click

Background

Rahul is an MSc Computer Science student at Mohta College, MGSU University Bikaner, with a Major in Artificial Intelligence from IIT Ropar. He currently works as a Backend Developer at Groww Per Click, building scalable backend systems and REST APIs in a fintech environment, after gaining a strong IT foundation through NIELIT's O Level certification.

Specializes in

  • Python, Django, Django REST Framework, FastAPI
  • PostgreSQL, Redis, Celery, Docker
  • Machine learning — scikit-learn, Pandas, NumPy, NLP/SBERT

Other open-source packages

django-var-cms
Role-based admin control panel and CMS registry with Tailwind CSS and glassmorphic styling.
django-meta-whatsapp
Full WhatsApp CRM and messaging platform — inbox, campaigns, webhooks, REST API.
On this page